线程的创建方式
- 继承 Thread 类(重写 run 方法)
- 实现 Runnable 接口(实现 run 方法)
- 实现 Callable 接口(重写 call 方法)
基于 ThreasdPoolExecutor 线程池对象创建线程
线程的七大状态
new: 线程对象完成实例化
- runnable: 线程对象调用start()方法
- running: CPU调用执行
- terminated: 执行完成或因异常退出
- waiting: 等待阻塞, 执行wait()/join()/park()方法,
- blocked: 同步锁
-
线程池的七大核心参数
核心线程数(corePoolSize)
即使核心线程没有接到任务也不会被销毁
- 最大线程数(maximumPoolSize)
- 空闲回收时间(keepAliveTime)
非核心线程超过这个时间没有接到任务就会被销毁
- 时间单位(unit)
- 阻塞队列(workQueue)
当线程数量大于等于核心线程数且线程都在执行任务时, 新添加的任务放在阻塞队列中
- ArrayBlockingQueue
- 有界阻塞队列
- 先进先出
- 不接受null元素
- LinkedBlockingQueue
- 可指定大小的有界阻塞队列(若不指定则为Integer.MAX_VALUE)
- 先进先出
- PriorityBlockingQueue
- 无界阻塞队列
- 不接受null元素
- SynchronousBlockingQueue
- 线程工厂(threadFactory)
用来创建线程
- 拒绝策略(handler)
- CallerRunsPolicy : 这个策略重试添加当前的任务, 他会自动重复调用 execute() 方法, 直到成功;
- AbortPolicy : 对拒绝任务抛弃处理, 并且抛出异常;
- DiscardPolicy : 对拒绝任务直接无声抛弃, 没有异常信息;
- DiscardOldestPolicy : 对拒绝任务不抛弃, 而是抛弃队列里面等待最久的一个线程, 然后把拒绝任务加到队列.
线程池的五大状态
- running状态
此状态下, 线程池能够接收新任务, 以及对已添加的任务进行处理;
线程池的初始化状态就是running(线程池被一旦被创建, 就处于running状态, 并且线程池中的任务数为0)
- shutDown状态
此状态下不接收新任务, 但能处理已添加的任务;
调用线程池的shutdown()接口时, 线程池可以由running转为shutDown状态
- stop状态
此状态下不接收新任务, 不处理已添加的任务, 并且会中断正在处理的任务;
调用线程池的shutdownNow()接口时, 线程池可以由running/shutDown转为stop状态
- tidying
当所有的任务已终止, ctl记录的”任务数量”为0, 线程池会变为tidying状态。
当线程池变为tidying状态时, 会执行钩子函数terminated(), terminated()在ThreadPoolExecutor类中是空的, 若用户想在线程池变为tidying时, 进行相应的处理, 可以通过重载terminated()函数来实现
当线程池在shutDown状态下, 阻塞队列为空并且线程池中执行的任务也为空时,就会由 shutDown转为tidying
当线程池在stop状态下, 线程池中执行的任务为空时, 就会由stop转为tidying
- terminated
线程池彻底终止, 就变成terminated状态
线程池处在tidying状态时,执行完terminated()之后,就会由tidying变成terminated
synchronized的实现原理
- 锁代码块 :依靠monitorenter和monitorexit实现。每个对象都有一个monitor监视器锁,当monitor被占用时(owner指向占用monitor的线程)就会处于锁定状态
- monitorenter(尝试获取锁) :
- 如果monitor的进入数为0,则该线程进入monitor,让后将进入数+1,该线程就是monitor的拥有者
- 如果该线程时monitor的拥有者,只是重新进入,则monitor进入数+1
- 如果其他线程已经占用monitor,则进入阻塞状态,直到monitor的进入数为0
- monitorexit
- 执行monitorexit的线程必须时monitor的拥有者
- 执行后monitor的进入数-1,若进入数减至0,则线程退出monitor,不再是monitor的所有者
- 其他被这个monitor阻塞的线程可以再次尝试去获取这个monitor
- monitorenter(尝试获取锁) :
- 锁普通方法 :通过ACC_SYNCHRONIZED标识符实现
- 当方法被调用时会检查方法的ACC_SYNCHRONIZED标识符是否被设置
- 如果设置了,执行线程将先获取monitor(使用哪个对象调用方法,就获取谁的monitor),成功之后再执行方法体,方法体执行后释放monitor
- 若未获取到monitor则进入阻塞状态
- 锁静态方法 :通过ACC_STATIC、ACC_SYNCHRONIZED标识符实现
- (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的区别
- 作用位置不同
- synchronized :
- 可以修饰静态方法,锁对象为当前类的.class对象;
- 修饰普通方法,锁对象为当前对象;
- 修饰代码块则可以指定锁对象
- lock :只能给代码块加锁
- synchronized :
- 获取锁和释放锁的机制不同
- synchronized :无需手动获取和释放,执行完或是发生异常时会自动释放锁
- lock :需要手动获取和释放,所以一般在finally中释放锁
- 等待是否可中断
- synchronized :不可中断,除非抛出异常或正常执行完成
- lock :可中断:
- 设置超时方法 tryLock
- lockInterruptibly()放入代码块中,调用interrupt()方法中断
- 锁特点
- synchronized :可重入、不可中断、非公平
- lock :可重入、等待可中断、可判断、可公平可非公平
- 锁机制
- synchronized :悲观锁,当一个线程获得锁时其他线程只能依靠阻塞来等待线程释放锁
- lock :乐观锁,CAS操作
- 性能差别
当锁竞争不激烈时,两者性能差不多;而当资源竞争非常激烈时Lock的性能远远高于synchronized
对象锁的升级过程
java对象结构: 对象头(MarkWord + Klass), 实例数据, 对其填充
java对象被锁定的状态有四种(锁状态存储在对象头的 Markword 中):
- 无锁状态: markWord存储对象的hashCode, 分代年龄, 是否偏向锁, 锁标标志位
- 偏向锁状态: markWord存储线程Id, Epoch, 分代年龄, ,是否偏向锁, 锁标标志位
- 轻量级锁状态: markWord存储指向栈中锁记录的指针, 锁标标志位
- 重量级锁状态: markWord存储 指向重量级锁的指针, 锁标标志位
升级过程:
- 对象刚创建时, 还没有任何线程来竞争.对象的 MarkWord 中偏向锁标识位是0, 锁状态01, 说明该对象处于无锁状态
- 当只有一个线程来竞争锁时, 先用偏向锁.这时 MarkWord 会记录当前线程ID, 偏向锁标识位是1, 锁状态01
- 当有两个线程开始竞争锁对象时, 锁会升级为轻量级锁, JVM会在当前线程的线程栈中开辟一块单独的空间, 里面保存指向对象锁 MarkWord 的指针, 同时在对象锁 MarkWord 中保存指向这片空间的指针.(CAS操作)
- 轻量级锁抢锁失败, 则JVM会使用自旋锁, 自旋锁并非是一个锁, 则是一个循环操作, 不断的尝试获取锁. 从JDK1.7开始, 自旋锁默认开启, 自旋次数由JVM决定. 如果抢锁成功,则执行同步代码
- 自旋锁重试之后仍然未抢到锁, 同步锁会升级至重量级锁, 锁状态10, 在这个状态下, 未抢到锁的线程都会被阻塞, 由 Monitor 来管理, 并会有线程的 park 与 unpark, 因为这个存在用户态和内核态的转换, 比较消耗资源, 故名重量级锁
CAS和ABA
CAS:
某一线程执行一个CAS逻辑, 如果中途有其他线程修改了共享变量的值, 导致这个线程的CAS逻辑运算后得到的值与期望结果不一致,那么这个线程会再次执行CAS逻辑(do-whille 循环), 直到一致为止。
缺点: 虽然CAS没有加锁保证了一致性,并发性有所提高 ,但是也产生了一系列的问题,比如循环时间长开销大、只能保证一个共享变量的原子操作、会产生ABA问题
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
ABA: 当有两个线程 T1 和 T2 从内存中获取到值A, 线程 T2 通过某些操作把内存值修改为B, 然后又经过某些操作将值修改为回值A, T2退出. 线程 T1 进行操作的时候, 使用预期值同内存中的值比较, 此时均为A, 修改成功退出. 但是此时的A以及不是原先的A了, 这就是 ABA 问题.
如何解决: 可以使用 Java 中的提供的类 AtomicStampedReference 中的compareAndSet 方法.
public class ABADemo {
private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stamp + ",值是" + stampedReference.getReference());
//暂停1秒钟t1线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
System.out.println("线程t1已完成1次ABA操作~~~~~");
}, "t1").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stamp + ",值是" + stampedReference.getReference());
//线程2暂停3秒,保证线程1完成1次ABA
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = stampedReference.compareAndSet(100, 6666, stamp, stamp + 1);
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",修改成功否:" + result + ",最新版本号" +
stampedReference.getStamp() + ",最新的值:" + stampedReference.getReference());
}, "t2").start();
}
}