Java线程的状态

image.png

  1. 初始

    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态

  2. 就绪

    • 就绪状态只是说有资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    • 调用线程的start()方法,此线程进入就绪状态。
    • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    • 锁池里的线程拿到对象锁后,进入就绪状态。
  3. 运行中

    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式

  4. 阻塞

    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  5. 等待

    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态

  6. 超时等待

    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒

  7. 终止

    • 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
    • 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常

几个方法的比较

  1. Thread.sleep(long millis),**一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。**作用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。 作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
  3. thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
  4. Object.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
  5. Object.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
  6. LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒

线程创建有三种方式:

1 继承Thread类重写run方法

public static class MyThread extends Thread {
  @Override
  public void run(){
    //todo
  }
}
    
public static void main(String[] args){
  MyThread thread = new MyThread();
  //线程启动
  thread.start();
}

好处在于在run()方法内直接使用this就能获取当前线程。不好的是Java不支持多继承

2 实现Runnable接口的run方法

public static class RunnableTask implements Runnable{
  @Override
  public void run(){
    //todo
  }
}
public static void mian(String[] args){
  RunnableTask task = new RunnableTask();
  new Thread(task).start();
  new Thread(task).start();
}

3 使用FutureTask 即实现Callable接口

public static class CallerTask implements Callable<String> {
  @Override
  public String call() throw Exception {
    return "hello";
  }
}
public static void main(String[] args) throw InterruptedException {
  FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
  //启动线程
  new Thread(futureTask).start();
  try{
    //等待任务完成。自动阻塞
    String result = futureTask.get();
  }catch(Exception e){
    //
  }
}

线程面试常见问题

Thread 类中的start() 和 run() 方法有什么区别

  • start 方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
  • run 方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的

产生死锁的条件

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁

  • 加锁顺序:确保所有的线程都是按照相同的顺序获得锁
  • 加锁时间:使用ReentrantLock。在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
  • 死锁检测:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

Java中的CAS操作

为了实现轻量同步,JDK提供了一系列原子操作类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,它们都是基于CAS去实现的

所有Atomic相关类的实现都是通过CAS(Compare And Swap)去实现的,它是一种乐观锁的实现。对于乐观锁来说,总是会把事情往乐观的方向想,他们认为所有事情总是不太容易发生问题,出错几率很小。当然与之相反的就是悲观锁,也就是synchronized锁,它总是很严谨,认为出错是一种常态,所以无论大小,都考虑的很全面,不允许一点错误发生。

CAS技术就是乐观锁的一种形式,Compare And Swap顾名思义比较交换,它会比较操作之前的值和预期的值是否一致,一致才进行操作,否则什么都不做,然后循环去CAS。它是放在Unsafe这个类中的,这个类是不允许更改的,而且也不建议开发者调用,它只是用于JDK内部调用,看名字就知道它是不安全的,因为它是直接操作内存,稍不注意就可能把内存写崩,其内部大部分是native方法。

CAS的过程是这样的:它包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会对V的值设为N,否则当前线程什么都不做。

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

与锁想比,使用CAS会使程序看起来更复杂一些,但由于其非阻塞性,它对死锁又天生免疫,并且线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

线程安全

不管何种调度方式,不需要额外的同步或者协同,都能表现正确的行为

  • 原子性:互斥访问
  • 可见性:一个线程对主存的修改能及时被其他线程观察
  • 有序性:尽管所有线程执行顺序改变,单个线程的执行顺序不会改变

原子性

Atomic包

AtomicXXX方法使用了Unsafe类的CompareAndSet方法,就是常说的CAS,此方法将内存中的值读取,和预期的值比较,如果相等则允许更新,不相等则循环再取。

  • synchronized:依赖于JVM
  • Lock:依赖特殊的CPU指令
    • ReentrantLock--java代码实现

对比

  • synchronized:不可中断,适合竞争激烈,可读性好
  • Lock:可中断锁,多样化同步,竞争激烈时维持常态
  • Atomic:竞争激烈时能维持常态,比Lock性能更好。但是一次只能同步一个值

可见性

导致不可见的原因:

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

synchronized

  • 线程解锁前必须把共享变量的最新值刷新回到主存
  • 线程加锁时,将清空工作内存中共享变量的值,重新从主存中读取最新值

volatile

通过加入内存屏障和禁止重排序优化来实现

  • 对volatile变量写操作时,会在写操作后加入一条store屏障命令,将本地内存的共享变量刷新回主存
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主存中读取共享变量

一般用于两个线程检查

有序性

如果两个线程的操作顺序无法从happen-before规则推导出来,就不能保证有序性,虚拟机可以随意进行重排序

happen-before规则

  • 程序次序规则:一个线程内,按照书写顺序执行
  • 锁定规则:一个unlock操作先行发生于后面同一个锁的lock操作
  • volatile变量规则:对于一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于B,B又先行于C,则A先于C
  • 线程启动规则:Thread对象的start()方法先行与此线程的每一个动作
  • 线程中断规则:对于线程interrupt()方法的调用 先行发生于 被中断线程的代码 检测到中断事件的发生
  • 线程终结规则:线程中所有操作都先行发生于线程的终止检测
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始