线程的六种状态

线程池的核心参数

线程池可以看做是线程的集合。在没有任务时线程处于空闲状态,当请求到来:线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务执行(而不是销毁)。这样就实现了线程的重用。如果请求到来时,核心线程被占用,任务便会进入workQueue中等待。若workQueue也满,则会创建救急线程来执行,执行完任务后,救急线程不会保留在线程池中,有一定的生存时间,取决于keepAliveTimeunit。若还有时间,会从workQueue中依次加载任务。

  1. corePoolSize核心线程数目:最多保留的线程数。
  2. maximumPoolSize最大线程数目:核心线程+救急线程。
  3. keepAliveTime生存时间:针对救急线程。
  4. unit时间单位:针对救急线程。
  5. workQueue :阻塞队列。
  6. threadFactory线程工厂:可以为线程创建时起个好名字。
  7. handler拒绝策略:当核心线程、救急线程、阻塞队列都满是,submit一个任务,则会触发拒绝策略。
    • 默认为AbortPolicy,会抛出异常。
    • CallerRunsPolicy,由提交任务的线程完成任务(如main线程)。
    • DiscardPolicy,丢弃,不运行,也不报错。
    • DiscardOldestPolicy,抛弃队列中最先加入的任务,新任务进入队列。

代码演示

private static void testWaiting() {
Thread t2 = new Thread(() -> {
synchronized (LOCK) {
logger1.debug("before waiting"); // 1
try {
LOCK.wait(); // 3
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2");

t2.start();
main.debug("state: {}", t2.getState()); // 2
synchronized (LOCK) {
main.debug("state: {}", t2.getState()); // 4
LOCK.notify(); // 5
main.debug("state: {}", t2.getState()); // 6
}
main.debug("state: {}", t2.getState()); // 7
}

//输出
[DEBUG] 21:10:51.829 [t2] - before waiting
[DEBUG] 21:10:51.829 [main] - state: RUNNABLE
[DEBUG] 21:11:03.137 [main] - state: WAITING
[DEBUG] 21:11:04.051 [main] - state: BLOCKED
[DEBUG] 21:11:05.195 [main] - state: RUNNABLE
  1. t2.start()启动线程,t2线程变为runnable状态,线程进入代码块
  2. t2线程执行wait()方法后,锁释放,主线程进入sychronized代码块
  3. 主线程调用notify()方法,打印t2线程状态,为blocked,因为锁被主线程占有
  4. 主线程执行完sychronized代码块,t2线程继续运行,状态变为runnable

sleep vs wait

  • 共同点:wait()wait(long)sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。
  • 方法归属不同:
    1. sleep(long)Thread的静态方法
    2. wait()wait(long)都是Object的成员方法,每个对象都有
  • 醒来时机不同:
    1. 执行sleep(long)wait(long)的线程都会在等待相应毫秒后醒来
    2. wait(long)wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
    3. 它们都可以被线程对象的interrupt()打断唤醒,被打断的线程抛出异常
  • 锁特性不同:
    1. wait()方法的调用必须先获取wait对象的锁,而sleep()无此限制
    2. wait()方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃,但你们还可以用)
    3. sleep()如果在synchronized代码块中执行,并不会释放对象锁(我放弃,你们也用不了)

lock vs synchronized

  • 语法层面
    • synchronized是关键字,源码在jvm中,用c++语言实现
    • Lock是接口,源码由jdk提供,用java语言实现
    • 使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
  • 功能层面
    • 二者均属于悲观锁,都具备基本的互斥、同步、锁重入功能
      • 互斥:多个线程争抢一把锁,只有一个线程成功,其余锁进入阻塞状态
      • 同步:多线程同步运行,当某一线程需要另一线程的计算结果,那么它会等待那个线程运行完再运行下去
      • 锁重入:可以给同一对象加多道锁(加多道,解锁时也要解多道)
    • Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量(因为synchronized用c++实现,较为底层)
      • 获取等待状态:互斥、同步时,可以知道哪些线程处于阻塞状态、等待状态
      • 公平锁:多线程争抢锁失败后,后续再获得锁时,若先来先得为公平锁,可插队为非公平锁
        • Lock可以公平锁也可以非公平锁,而synchronized为非公平锁
        • 非公平锁吞吐量高,插队效率更高
      • 可打断、可超时:锁被线程占用,其他线程的等待过程可以被打断,或者设置超时时间,synchronized则一直等下去
      • 多条件变量:Lock有多个等待队列(await()方法使线程进入waiting queue,signal()方法唤醒,synchronized只有一个
    • Lock有适合不同场景的实现,如ReentrantLock(可重入锁),ReentrantReadWriteLock(针对读多写少),synchronized只有一层实现,即用c++语言在jvm底层实现
  • 性能层面
    • 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock的实现通常会提供更好的性能

volatile(修饰符)能否保证线程安全

  1. 线程安全要考虑三个方面:可见性、有序性、原子性,volatile可以保证可见性和有序性。

    • **可见性:**一个线程对共享变量修改,另一个线程能看到最新的结果。

    • **有序性:**一个线程内代码安编写顺序执行。假如你编写顺序是123,但cpu对你优化,变为321执行,这对单线程没问题,对多线程可能会有问题。

    • **原子性:**一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队。

      static volatile int balance = 10;
      public static void subtract(){
      balance -= 5;
      }
      public static void add(){
      balance += 5;
      }

      调用两个线程执行加减操作,执行javap -p - v ( + *.class字节码文件),查看反编译内容。

      t1
      0:getstatic
      3:iconst_5
      4:iadd
      5:putstatic

      t2
      0:getstatic
      3:iconst_5
      4:isub
      5:putstatic

      可以看出balance += 5一条代码,对应底层多行操作,若两个线程执行各自的多行代码时发生交错,则可能导致最后结果出错。所以要保证原子性,可用synchronizedlock锁CAS

      • CAS原理:比较并交换,如果我们读到的值v和调用weakCompareAndSetInt中快照获取的内存中的值一致的话,那么我们就将变更后的值进行写入到主内存中的操作进行下去;如果预期值与实际的值不一致(即已经有其他的线程捷足先登,提前改变了这个值,那么我们再取主内存中最新的值,进行循环的比较与交换操作)。