线程的创建方式

  1. 继承 Thread 类(重写 run 方法)
  2. 实现 Runnable 接口(实现 run 方法)
  3. 实现 Callable 接口(重写 call 方法)
  4. 基于 ThreasdPoolExecutor 线程池对象创建线程

    线程的七大状态

  5. new: 线程对象完成实例化

  6. runnable: 线程对象调用start()方法
  7. running: CPU调用执行
  8. terminated: 执行完成或因异常退出
  9. waiting: 等待阻塞, 执行wait()/join()/park()方法,
  10. blocked: 同步锁
  11. timeWaiting: 时间等待

    线程池的七大核心参数

  12. 核心线程数(corePoolSize)

即使核心线程没有接到任务也不会被销毁

  1. 最大线程数(maximumPoolSize)
  2. 空闲回收时间(keepAliveTime)

非核心线程超过这个时间没有接到任务就会被销毁

  1. 时间单位(unit)
  2. 阻塞队列(workQueue)

当线程数量大于等于核心线程数且线程都在执行任务时, 新添加的任务放在阻塞队列中

  1. ArrayBlockingQueue
    1. 有界阻塞队列
    2. 先进先出
    3. 不接受null元素
  2. LinkedBlockingQueue
    1. 可指定大小的有界阻塞队列(若不指定则为Integer.MAX_VALUE)
    2. 先进先出
  3. PriorityBlockingQueue
    1. 无界阻塞队列
    2. 不接受null元素
  4. SynchronousBlockingQueue
    1. 线程工厂(threadFactory)

用来创建线程

  1. 拒绝策略(handler)
    1. CallerRunsPolicy : 这个策略重试添加当前的任务, 他会自动重复调用 execute() 方法, 直到成功;
    2. AbortPolicy : 对拒绝任务抛弃处理, 并且抛出异常;
    3. DiscardPolicy : 对拒绝任务直接无声抛弃, 没有异常信息;
    4. DiscardOldestPolicy : 对拒绝任务不抛弃, 而是抛弃队列里面等待最久的一个线程, 然后把拒绝任务加到队列.

线程池的五大状态

  1. running状态

此状态下, 线程池能够接收新任务, 以及对已添加的任务进行处理;
线程池的初始化状态就是running(线程池被一旦被创建, 就处于running状态, 并且线程池中的任务数为0)

  1. shutDown状态

此状态下不接收新任务, 但能处理已添加的任务;
调用线程池的shutdown()接口时, 线程池可以由running转为shutDown状态

  1. stop状态

此状态下不接收新任务, 不处理已添加的任务, 并且会中断正在处理的任务;
调用线程池的shutdownNow()接口时, 线程池可以由running/shutDown转为stop状态

  1. tidying

当所有的任务已终止, ctl记录的”任务数量”为0, 线程池会变为tidying状态。
当线程池变为tidying状态时, 会执行钩子函数terminated(), terminated()在ThreadPoolExecutor类中是空的, 若用户想在线程池变为tidying时, 进行相应的处理, 可以通过重载terminated()函数来实现
当线程池在shutDown状态下, 阻塞队列为空并且线程池中执行的任务也为空时,就会由 shutDown转为tidying
当线程池在stop状态下, 线程池中执行的任务为空时, 就会由stop转为tidying

  1. terminated

线程池彻底终止, 就变成terminated状态
线程池处在tidying状态时,执行完terminated()之后,就会由tidying变成terminated

synchronized的实现原理

  1. 锁代码块 :依靠monitorenter和monitorexit实现。每个对象都有一个monitor监视器锁,当monitor被占用时(owner指向占用monitor的线程)就会处于锁定状态
    1. monitorenter(尝试获取锁) :
      1. 如果monitor的进入数为0,则该线程进入monitor,让后将进入数+1,该线程就是monitor的拥有者
      2. 如果该线程时monitor的拥有者,只是重新进入,则monitor进入数+1
      3. 如果其他线程已经占用monitor,则进入阻塞状态,直到monitor的进入数为0
    2. monitorexit
      1. 执行monitorexit的线程必须时monitor的拥有者
      2. 执行后monitor的进入数-1,若进入数减至0,则线程退出monitor,不再是monitor的所有者
      3. 其他被这个monitor阻塞的线程可以再次尝试去获取这个monitor
  2. 锁普通方法 :通过ACC_SYNCHRONIZED标识符实现
    1. 当方法被调用时会检查方法的ACC_SYNCHRONIZED标识符是否被设置
    2. 如果设置了,执行线程将先获取monitor(使用哪个对象调用方法,就获取谁的monitor),成功之后再执行方法体,方法体执行后释放monitor
    3. 若未获取到monitor则进入阻塞状态
  3. 锁静态方法 :通过ACC_STATIC、ACC_SYNCHRONIZED标识符实现
    1. 当方法被这两个标识符标记时,执行线程需要先获取当前类的monitor

      锁竞争过程

      锁竞争过程.png
  • (1)、多个线程请求锁,首先进入Contention List,它可以接纳所有请求线程,而且是一个后进先出(LIFO)的虚拟队列,通过结点Node和next指针构造。
  • (2)(3)、ContentionList会被线程并发访问,EntryList为了降低线程对ContentionList队尾的争用而构造出来。当Owner释放锁时,会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head结点)为Ready Thread,也就是说某个时刻最多只有一个线程正在竞争锁。
  • (4)、Owner并不是直接把锁交给OnDeck线程,而是将竞争锁的权利交给OnDeck(将锁释放了),然后让OnDeck自己去竞争。竞争成功后,OnDeck线程就变成Owner;否则继续留在EntryList的队头。
  • (5)(6)、当线程调用wait方法被阻塞时,进入WaitSet;当其他线程调用notifyAll()(notify())方法后,阻塞队列的(某个)线程就会进入EntryList中。

处于ContetionList、EntryList、WaitSet的线程均处于阻塞状态。而线程被阻塞涉及到用户态与内核态的切换(Liunx),系统切换严重影响锁的性能。解决这个问题的办法就是自旋。自旋就是线程不断进行内部循环,即for循环什么也不做,防止线程wait()阻塞,在自旋过程中不断尝试获取锁,如果自旋期间,Owner刚好释放锁,此时自旋线程就可以去竞争锁。如果自旋了一段时间还没获取到锁,那没办法,只能调用wait()阻塞了。
为什么自旋了一段时间后又调用wait()方法呢?因为自旋是要消耗CPU的,而且还有线程上下文切换,因为CPU还可以调度线程,只不过执行的是空的for循环罢了。
对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。
所以,synchronized是什么时候进行自旋的?答案是在进入ContetionList之前,因为它自旋一定时间后还没获取锁,最后它只好在ContetionList中阻塞等待了。

synchronized和Lock的区别

  1. 作用位置不同
    1. synchronized :
      1. 可以修饰静态方法,锁对象为当前类的.class对象;
      2. 修饰普通方法,锁对象为当前对象;
      3. 修饰代码块则可以指定锁对象
    2. lock :只能给代码块加锁
  2. 获取锁和释放锁的机制不同
    1. synchronized :无需手动获取和释放,执行完或是发生异常时会自动释放锁
    2. lock :需要手动获取和释放,所以一般在finally中释放锁
  3. 等待是否可中断
    1. synchronized :不可中断,除非抛出异常或正常执行完成
    2. lock :可中断:
      1. 设置超时方法 tryLock
      2. lockInterruptibly()放入代码块中,调用interrupt()方法中断
  4. 锁特点
    1. synchronized :可重入、不可中断、非公平
    2. lock :可重入、等待可中断、可判断、可公平可非公平
  5. 锁机制
    1. synchronized :悲观锁,当一个线程获得锁时其他线程只能依靠阻塞来等待线程释放锁
    2. lock :乐观锁,CAS操作
  6. 性能差别

当锁竞争不激烈时,两者性能差不多;而当资源竞争非常激烈时Lock的性能远远高于synchronized

对象锁的升级过程

java对象结构: 对象头(MarkWord + Klass), 实例数据, 对其填充
java对象被锁定的状态有四种(锁状态存储在对象头的 Markword 中):

  1. 无锁状态: markWord存储对象的hashCode, 分代年龄, 是否偏向锁, 锁标标志位
  2. 偏向锁状态: markWord存储线程Id, Epoch, 分代年龄, ,是否偏向锁, 锁标标志位
  3. 轻量级锁状态: markWord存储指向栈中锁记录的指针, 锁标标志位
  4. 重量级锁状态: markWord存储 指向重量级锁的指针, 锁标标志位

升级过程:

  1. 对象刚创建时, 还没有任何线程来竞争.对象的 MarkWord 中偏向锁标识位是0, 锁状态01, 说明该对象处于无锁状态
  2. 当只有一个线程来竞争锁时, 先用偏向锁.这时 MarkWord 会记录当前线程ID, 偏向锁标识位是1, 锁状态01
  3. 当有两个线程开始竞争锁对象时, 锁会升级为轻量级锁, JVM会在当前线程的线程栈中开辟一块单独的空间, 里面保存指向对象锁 MarkWord 的指针, 同时在对象锁 MarkWord 中保存指向这片空间的指针.(CAS操作)
  4. 轻量级锁抢锁失败, 则JVM会使用自旋锁, 自旋锁并非是一个锁, 则是一个循环操作, 不断的尝试获取锁. 从JDK1.7开始, 自旋锁默认开启, 自旋次数由JVM决定. 如果抢锁成功,则执行同步代码
  5. 自旋锁重试之后仍然未抢到锁, 同步锁会升级至重量级锁, 锁状态10, 在这个状态下, 未抢到锁的线程都会被阻塞, 由 Monitor 来管理, 并会有线程的 park 与 unpark, 因为这个存在用户态和内核态的转换, 比较消耗资源, 故名重量级锁

image.png

CAS和ABA

CAS:
某一线程执行一个CAS逻辑, 如果中途有其他线程修改了共享变量的值, 导致这个线程的CAS逻辑运算后得到的值与期望结果不一致,那么这个线程会再次执行CAS逻辑(do-whille 循环), 直到一致为止。
缺点: 虽然CAS没有加锁保证了一致性,并发性有所提高 ,但是也产生了一系列的问题,比如循环时间长开销大、只能保证一个共享变量的原子操作、会产生ABA问题

  1. public final int getAndAddInt(Object var1, long var2, int var4) {
  2. int var5;
  3. do {
  4. var5 = this.getIntVolatile(var1, var2);
  5. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  6. return var5;
  7. }

ABA: 当有两个线程 T1 和 T2 从内存中获取到值A, 线程 T2 通过某些操作把内存值修改为B, 然后又经过某些操作将值修改为回值A, T2退出. 线程 T1 进行操作的时候, 使用预期值同内存中的值比较, 此时均为A, 修改成功退出. 但是此时的A以及不是原先的A了, 这就是 ABA 问题.
如何解决: 可以使用 Java 中的提供的类 AtomicStampedReference 中的compareAndSet 方法.

  1. public class ABADemo {
  2. private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
  3. public static void main(String[] args) {
  4. new Thread(() -> {
  5. int stamp = stampedReference.getStamp();
  6. System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stamp + ",值是" + stampedReference.getReference());
  7. //暂停1秒钟t1线程
  8. try {
  9. TimeUnit.SECONDS.sleep(1);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
  14. System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
  15. stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
  16. System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
  17. System.out.println("线程t1已完成1次ABA操作~~~~~");
  18. }, "t1").start();
  19. new Thread(() -> {
  20. int stamp = stampedReference.getStamp();
  21. System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stamp + ",值是" + stampedReference.getReference());
  22. //线程2暂停3秒,保证线程1完成1次ABA
  23. try {
  24. TimeUnit.SECONDS.sleep(3);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. boolean result = stampedReference.compareAndSet(100, 6666, stamp, stamp + 1);
  29. System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",修改成功否:" + result + ",最新版本号" +
  30. stampedReference.getStamp() + ",最新的值:" + stampedReference.getReference());
  31. }, "t2").start();
  32. }
  33. }

voliate

JUC

java.util.concruuent 的缩写