JAVA之多线程

2年前 (2022) 程序员胖胖胖虎阿
299 0 0

文章目录

  • 进程与线程
  • 线程的运行机制
  • 线程的创建和启动
    • 继承java.lang.Thread类
    • 实现Runnable接口
  • 线程的状态转换
    • 新建状态(New)
    • 就绪状态(Runnable)
    • 运行状态(Running)
    • 阻塞状态
    • 死亡状态(Dead)
  • 线程的调度
  • 设置线程的优先级
  • 线程睡眠
  • 线程让步
  • 后台线程
  • 定时器
  • 共享资源竞争
  • 原子操作
  • 生产者/消费者范例
  • 线程同步
  • 线程通信
  • Lock锁

进程与线程

  1. 进程是指运行中的应用程序,每一个进程都有自己独立的内存空间。
  2. 线程是指进程中的一个执行流程,有时也称为执行情景。
  3. 一个进程可以由多个线程组成,即在一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。
  4. 当进程内的多个线程同时运行,这种运行方式称为并发运行。

线程的运行机制

每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack):
程序计数器:也称为PC寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令。
方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈桢。每当线程调用一个方法,就会向方法栈压入一个新桢,桢用来存储方法的参数、局部变量和运算过程中的临时数据。

eg:栈区先是为空,当调用main方法时,便将main方法压入栈,称为栈帧,后调用method方法,就将method方法加入栈,成为栈帧;
JAVA之多线程

线程运行中需要使用的:
JAVA之多线程

线程的创建和启动

创建线程有两种方式:

  1. 继承java.lang.Thread类 :extends
  2. 实现Runnable接口:implements

继承java.lang.Thread类

Thread类代表线程类,它的最主要的两个方法是:
run():包含线程运行时所执行的代码。
start():用于启动线程。

package extendth;
public class Machine extends Thread{
  public void run(){
    for(int a=0;a<50;a++)
      System.out.println(a);
  }
  
  public static void main(String args[]){
    Machine machine=new Machine();
    machine.start();  //启动machine线程
  }	
}

实现Runnable接口

package runimpl;
public class Machine implements Runnable{
  private int a=0;
  public void run(){
    for(a=0;a<50;a++){
      System.out.println(Thread.currentThread().getName()+":"+a);
      try{
        Thread.sleep(100);
      }catch(InterruptedException e){throw new RuntimeException(e);}
    }
  }
  public static void main(String args[]){
    Machine machine=new Machine();
    Thread t1=new Thread(machine);
    Thread t2=new Thread(machine);
    t1.start();
    t2.start();
  }
}

线程的状态转换

JAVA之多线程

新建状态(New)

用new语句创建的线程对象处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。

就绪状态(Runnable)

当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待CPU的使用权。

运行状态(Running)

处于这个状态的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。如果计算机有多个CPU,那么同一时刻可以让几个线程占用不同的CPU,使它们都处于运行状态。

阻塞状态

指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为三种:

  1. 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中。
  2. 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其它线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。
  3. 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求,就会进入这个状态。

死亡状态(Dead)

当线程退出run()方法,就进入死亡状态,该线程结束生命周期。线程有可能是正常执行完run()方法而退出,也有可能是遇到异常而退出。

线程的调度

  1. Java虚拟机采用抢占式调度模型,它是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中线程的优先级相同,那么就随机地选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。一个线程会因为以下原因而放弃CPU:
    1)Java虚拟机让当前线程暂时放弃CPU,转到就绪状态,使其他线程获得运行机会。
    2)当前线程因为某些原因而进入阻塞状态。
    3)线程运行结束。
  2. 如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下办法之一:
    调整各个线程的优先级。
    让处于运行状态的线程调用Thread.sleep()方法。
    让处于运行状态的线程调用Thread.yield()方法。
    让处于运行状态的线程调用另一个线程的join()方法。

设置线程的优先级

Thread类的setPriority(int)和getPriority()方法分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10,Thread类有三个静态常量:
MAX_PRIORITY:取值为10,表示最高优先级。
MIN_PRIORITY:取值为1,表示最低优先级。
NORM_ PRIORITY:取值为5,表示默认的优先级。

Thread t1=...;
t1.setPriority(Thread.MAX_PRIORITY);

线程睡眠

当一个线程在运行中执行了sleep()方法,它就会放弃CPU,转到阻塞状态。

线程让步

  1. 当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级或者更高优先级的其他线程处于就绪状态,yield()方法将把当前运行的线程放到可运行池中并使另一个线程运行。
    如果没有相同或更高优先级的可运行线程,yield()方法什么都不做。

2.当前运行的线程可以调用另一个线程的join()方法,当前运行的线程将转到阻塞状态,直至另一个线程运行结束,它才会恢复运行。

后台线程

后台线程是指为其他线程提供服务的线程,也称为守护线程。
后台线程与前台线程相伴相随,只有当所有前台线程结束生命周期,还在运行的后台线程才会被Java虚拟机终止生命周期。只要有一个前台线程还没有运行结束,运行中的后台线程就不会被Java虚拟机终止生命周期。
主线程默认情况下是前台线程,由前台线程创建的线程默认情况下也是前台线程。

调用Thread类的setDaemon(true)方法,就能把一个线程设置为后台线程。
Thread类的isDaemon()方法用来判断一个线程是否是后台线程。

package withdaemon;
public class Machine extends Thread{
  private int a;
  private static int count;
   public void start(){
    super.start();
    //匿名线程类
    Thread deamon=new Thread(){
       public void run(){
        while(true){  //无限循环
          reset();
          try{
            sleep(50);
          }catch(InterruptedException e){
             throw new RuntimeException(e);}
        }
      }
    };
 
    deamon.setDaemon(true);
    deamon.start();
  }
  public void reset(){a=0;}

 public void run(){
    while(true){
      System.out.println(getName()+":"+a++);
      if(count++==100)break;
      yield();
    }
  }

  public static void main(String args[])
              throws Exception{
    Machine machine=new Machine();
    machine.start();
  }
}

定时器

在JDK的java.util包中提供了一个实用类Timer,它能够定时执行特定的任务。
TimerTask类表示定时器执行的一项任务。

package usetimer;
import java.util.Timer;
import java.util.TimerTask;
public class Machine extends Thread{
  private int a;
 
  public void start(){
    super.start();
    //把与Timer关联的线程设为后台线程
    Timer timer=new Timer(true) ; 
 //匿名类
    TimerTask task=new TimerTask(){  
      public void run(){
        reset();
      }
    };
    //设置定时任务
    timer.schedule(task,10,50); 
}
public void reset(){a=0;}
  public void run(){
    for(int i=0;i<1000;i++){
      System.out.println(getName()+":"+a++);
      yield();
    }
  }
  public static void main(String args[])
              throws Exception{
    Machine machine=new Machine();
    machine.start();
  }
}

Timer类的schedule(TimerTask task, long delay, long period)方法用来设置定时器需要定时执行的任务。task参数表示任务,delay参数表示延迟执行的时间,以毫秒为单位,period参数表示每次执行任务的间隔时间,以毫秒为单位。
timer.schedule(task,10,50);

以上代码表明定时器将在10毫秒以后开始执行task任务(即执行TimerTask实例的run()方法),以后每隔50毫秒重复执行一次task任务。

共享资源竞争

线程之间会进行资源竞争:如CPU

原子操作

a+=1
a-=1
打印a
以上操作被称为原子操作。原子操作由业务逻辑上相关的一组操作完成。
如果这些操作操纵与其他线程共享的资源,就可能会造成并发问题。为了保证得到正确的运算结果,一个线程在执行原子操作的期间,应该采取措施使得其他线程不能操纵共享资源,这里的共享资源是指Machine对象的实例变量a。

生产者/消费者范例

线程同步

为了保证每个线程能正常执行原子操作,Java引入了同步机制,具体作法是在代表原子操作的程序代码前加上synchronized标记,这样的代码被称为同步代码块:

public synchronized String pop() {…}

等价于:

public String pop() {
synchronized(this){…}
}

线程通信

java.lang.Object类中提供了两个用于线程通信的方法:
wait():执行该方法的线程释放对象的锁,Java虚拟机把该线程放到该对象的等待池中。该线程等待其他线程将它唤醒。
notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程。Java虚拟机从对象的等待池中随机地选择一个线程,把它转到对象的锁池中。

通信过程:
1)当线程t1执行对象s的一个同步代码块时,线程t1持有对象s的锁,线程t2在对象s的锁池中等待。
(2)线程t1在同步代码块中执行s.wait()方法,线程t1释放对象s的锁,进入对象s的等待池。
(3)在对象s的锁池中等待锁的t2线程获得了对象s的锁,执行对象s的另一个同步代码块。
(4)线程t2在同步代码块中执行s.notify()方法,Java虚拟机把线程t1从对象s的等待池移到对象s的锁池中,在那里等待获得锁。
(5)线程t2执行完同步代码块,释放锁。线程t1获得锁,继续执行同步代码块。
JAVA之多线程
用Condition进行线程通信:
java.lang.concurrent.locks.Condition条件接口用于线程之间的通信。Lock接口的newCondition()方法返回实现了Condition接口的实例。
Condition接口中有以下方法:
await():作用和Object类的wait()方法相似。当前线程释放外部锁,进入等待池中,等待其他线程将它唤醒。
await(long time, TimeUnit unit):当前线程释放外部锁,进入等待池中,等待其他线程将它唤醒。如果在参数设定的时间范围内没有被唤醒,就不再等待,直接返回false,否则返回true。
signal():作用和Object类的notify()方法相似。当前线程唤醒等待池中的一个线程。
signalAll():作用和Object类的notifyAll()方法相似。当前线程唤醒等待池中的所有线程。

Lock锁

lock():当前线程获得同步代码块的锁,如果锁被其他线程占用,那就进入阻塞状态。这种处理机制和Java对象内部锁是一样的。
tryLock():当前线程试图获得同步代码块的锁,如果锁被其他线程占用,那就立即返回false,否则返回true。
tryLock(long time,TimeUnit unit):该方法和上面的不带参数的tryLock()方法的作用相似。区别在于本方法设定了时间限制。如果锁被其他线程占用,当前线程会先进入阻塞状态。如果在时间限定范围内获得了锁,那就返回true;如果超过时间限定范围还没有获得锁,那就返回false。例如“lock.tryLock(50L, TimeUnit.SECONDS)”表示设定的时间限制为50秒。
和以上lock()方法以及tryLock()方法对应,Lock接口的unlock()方法用于释放线程所占用的同步代码块的锁。

Lock接口有一个实现类ReentrantLock,它有以下构造方法:
ReentrantLock():默认构造方法,创建一个常规的锁。
ReentrantLock(boolean fair):如果fair参数为true,会创建一个带有公平策略的锁。否则就创建一个常规的锁。所谓公平策略,是指会保证让阻塞较长时间的线程有机会获得锁。使用公平锁时,有两个注意事项:一、公平锁的公平机制是以降低运行性能为代价的。二、公平锁依赖于底层线程调度的实现,不能完全保证公平。

版权声明:程序员胖胖胖虎阿 发表于 2022年9月10日 下午10:16。
转载请注明:JAVA之多线程 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...