高并发
高并发
线程基础
概述
基本概念
程序:为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码
进程:正在运行的程序
- 这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存
- 在指令运行过程中还需要用到磁盘、网络等设备
- 进程就是用来加载程序指令、管理内存、管理 IO 的
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程,也有的程序只能启动一个实例进程
线程:进程的细化
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
"进程与线程的对比"
1. 进程之间相互独立,线程是进程的子集
2. 进程拥有共享资源,如内存空间中的堆和方法区供内部线程共享
3. 进程间相互通信较为复杂
- 同一台PC的通信进程称为IPC(Inter-Process Communication)
- 不同PC的进程通信需要通过网络,并遵从共同的协议
4. 线程通信相对简单,因为有共享内存,如多线程可以共同访问共享变量
5. 线程更轻量,线程上下文切换成本比进程低
守护线程
- 默认情况下,需要等待所线程结束,java进程才会结束
- 非守护线程全部结束,Java进程就会结束,即使守护线程还没结束
- 垃圾回收器线程就是守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,接收到shutdown命令后,不会等守护线程执行完,直接结束进程
查看进程、线程的方法
win
- 任务管理器
- 命令行
- tasklist:查看进程
- taskkill:杀死进程
linux
- ps -ef:查进程
- ps -fT -p <PID> :查看PID进程的线程
- kill:杀进程
- top:动态查进程,H键显示进程
- top -H -p <PID>:动态查PID进程的线程
java
jps:查java进程
jstack <PID>:查PID对应的java进程的线程
jconsole:图形化工具
远程连接配置
```sh java -Djava.rmi.server.hostname=ip地址 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=连接端口(自定义) -Dcom.sun.management.jmxremote.ssl=是否安全连接(false) - Dcom.sun.management.jmxremote.authenticate=是否认证(false) java类 ```
并发:单核CPU下,线程是串行执行的,操作系统中的任务调度器,将CPU的时间片分给不同的程序使用,只是达到了微观串行,宏观并行的状态,这种线程轮流抢占CPU的情况称为并发
同一时间应对(dealing with)多件事的能力
并行:多个cpu,不同线程做不同事情
同一时间做(doing)多件事情的能力
生命周期
状态
- 操作系统中的五种状态
- 初始状态:创建了线程对象,尚未与操作系统线程关联
- 可运行状态(就绪状态):与操作系统关联,可被CPU调度执行
- 运行状态:CPU时间片运行的状态,当时间片用完,进入可运行状态(上下文切换)
- 阻塞状态
- 如果调用阻塞API,如BIO读写文件,不会用到CPU,导致上下文切换,进入阻塞
- 等BIO操作完毕,操作系统唤醒阻塞,进入可运行状态
- 与可运行状态状态的区别:不被唤醒就不会被分到时间片,不会被CPU调度执行
- 终止状态:线程执行完毕,生命周期终结,不会再转换为其他状态
- API的六种状态:Thread.State枚举了六种状态
- NEW 等价于 初始状态:被创建,但未start
- Runnable 等价于 可运行、运行、阻塞
- Terminated 等价于 终结状态
- Blocked
- Waiting
- Timed_waiting
状态转换
NEW --> RUNNABLE ---------- t.start()
RUNNABLE <--> WAITING
wait-notify
> obj.wait() ----------- RUNNABLE --> WAITING > > obj.notify() , obj.notifyAll() , t.interrupt() > > * 竞争失败 ---------- WAITING --> BLOCKED > > * 竞争成功 ---------- WAITING --> RUNNABLEjoin
> t.join() ----------- RUNNABLE --> WAITING > > * ***当前线程在t 线程对象的监视器上等待*** > > * join底层是wait(保护性暂停模式),调用wait会升级为重量级锁 > > t线程结束 , mian.interrupt() --------- WAITING --> RUNNABLEpark-unpark
> LockSupport.park() ---------- RUNNABLE --> WAITING > > LockSupport.unpark(目标线程) 、 interrupt() ----------- WAITING --> RUNNABLE
RUNNABLE <--> TIMED_WAITING
wait-notify
> obj.wait(long n) ----------- RUNNABLE --> TIMED\_WAITING > > 超时、obj.notify() , obj.notifyAll() , t.interrupt() > > * 竞争失败 ---------- TIMED\_WAITING --> BLOCKED > * 竞争成功 ---------- TIMED\_WAITING --> RUNNABLEjoin
> t.join(long n) ----------- RUNNABLE --> TIMED\_WAITING > > 超时、t线程结束 , mian.interrupt() --------- TIMED\_WAITING --> RUNNABLEsleep
> Thread.sleep(long n) ------------- RUNNABLE --> TIMED\_WAITING > > 超时 ----------- TIMED\_WAITING --> RUNNABLEpark-unpark
> LockSupport.parkNanos(long nanos) 、 LockSupport.parkUntil(long millis) ----------- RUNNABLE --> TIMED\_WAITING > > 超时、LockSupport.unpark(目标线程) 、 interrupt() ----------- WAITING --> RUNNABLE
RUNNABLE <--> BLOCKED
synchronized(obj)竞争失败 ------------- RUNNABLE --> BLOCKED
被唤醒后竞争成功 ---------------- BLOCKED --> RUNNABLE
RUNNABLE --> TERMINATED --------- 所有代码运行完毕

活跃性
多把锁
将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
死锁
线程同步过程中,两线程分别占用对方的资源不放,多出现在嵌套同步中,如线程一先拿a锁,后需要b锁,线程二先拿b锁,后需要a锁
定位死锁
jconsole
命令行
```sh jps #获取进程id jstack [id] #定位死锁 +++++++++++++++++++++++++++++ ## 略去部分输出 Found one Java-level deadlock: #发现死锁 ============================= "Thread-1": #等待一个被Thread-0持有的锁 waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),which is held by "Thread-0" "Thread-0": #等待一个被Thread-1持有的锁 waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),which is held by "Thread-1" #上面列出的线程的 Java 堆栈信息 Java stack information for the threads listed above: =================================================== "Thread-1": at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) #代码位置 - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) - locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) #代码位置 - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) - locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock. ```linux下还可通过top先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
哲学家就餐问题
五位哲学家,围坐在圆桌旁
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子
如果筷子被身边的人拿着,自己就得等待
public class PhilosopherEatingTest { public static void main(String[] args) { Chopstick c1 = new Chopstick("c1"); Chopstick c2 = new Chopstick("c2"); Chopstick c3 = new Chopstick("c3"); Chopstick c4 = new Chopstick("c4"); Chopstick c5 = new Chopstick("c5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } } /** * 筷子 */ @ToString @AllArgsConstructor class Chopstick { String name; } /** * 哲学家 */ @Slf4j class Philosopher extends Thread { Chopstick leftChopstick; Chopstick rightChopstick; public Philosopher(String name, Chopstick leftChopstick, Chopstick rightChopstick) { super(name); this.leftChopstick = leftChopstick; this.rightChopstick = rightChopstick; } @SneakyThrows @Override public void run() { while (true) { synchronized (leftChopstick) { synchronized (rightChopstick) { eat(); } } } } public void eat() throws InterruptedException { log.debug("eat........"); sleep(1000); } }[DEBUG] 17:32:10.711 [苏格拉底] c.b.j.Philosopher - eat........ [DEBUG] 17:32:10.711 [亚里士多德] c.b.j.Philosopher - eat........ [DEBUG] 17:32:11.715 [柏拉图] c.b.j.Philosopher - eat........ #卡了
活锁
是一种现象,在两个线程互相改变对方的结束条件,最后谁也无法结束
@Slf4j public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(200); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(200); count++; log.debug("count: {}", count); } }, "t2").start(); } }
饥饿
线程始终得不到 CPU 调度执行,也不能够结束
哲学家就餐问题顺序加锁可以解决死锁,但是会产生饥饿问题
public class PhilosopherEatingTest { public static void main(String[] args) { Chopstick c1 = new Chopstick("c1"); Chopstick c2 = new Chopstick("c2"); Chopstick c3 = new Chopstick("c3"); Chopstick c4 = new Chopstick("c4"); Chopstick c5 = new Chopstick("c5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c1, c5).start(); //顺序加锁 } }... [DEBUG] 17:57:10.803 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:11.804 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:12.817 [亚里士多德] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:13.819 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:14.821 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:15.831 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:16.832 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:17.846 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:18.848 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:19.850 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:20.864 [赫拉克利特] c.b.j.t.Philosopher - eat........ [DEBUG] 17:57:21.865 [赫拉克利特] c.b.j.t.Philosopher - eat........ ...
锁消除与锁粗化
锁消除:编译器级别的一种锁优化方式,不用加锁的代码,虽然加了,但是到字节码文件时已经优化了
JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间
锁粗化:对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化
两个临界区,同一把锁,且中间的代码不耗时,合并成一个,可加快
public void doSomethingMethod(){ synchronized(lock){ //do some thing } //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕 synchronized(lock){ //do other thing } } //合并后 public void doSomethingMethod(){ //进行锁粗化:整合成一次锁请求、同步、释放 synchronized(lock){ //do some thing //做其它不需要同步但能很快执行完的工作 //do other thing } }for(int i=0;i<size;i++){ synchronized(lock){ } } //若循环执行很快,可包住循环 synchronized(lock){ for(int i=0;i<size;i++){ } }
线程创建
继承Thread类
- 子类继承Thread类
- 子类重写Thread类中的run方法
- 创建Thread子类对象,即创建了线程对象
- 调用线程对象start方法
class A extends Thread{ //定义Thread继承类
重写run方法 //重写的就是线程要执行的代码
}
class ATest{ //测试类
public static void main(String[] args){
A a = new A(); //创建线程对象
A b = new A();
a.start(); //启动线程,并调用run方法
b.start();
}
}
匿名子类的方式:
class ATest{
public static void main(String[] args){
new Thread(){
重写run方法;
}.start();
}
}
实现Runnable接口(推荐)
- 将线程和任务(要执行的代码)分开
- 子类实现Runnable接口
- 子类重写Runnable接口中的run方法
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
- start方法
class A impelments Runnable{ //定义Runnable的实现类
重写run方法
}
class ATest{
public static void main(String[] args){
A a = new A(); //创建实现类对象
Thread thread1 = new Thread(a); //把该对象作为形参,创建线程对象
Thread thread2 = new Thread(a,"thread2");
thread1.start(); //启动线程,并调用run方法
thread2.start();
}
}
继承Thread与实现Runnable比较:
实现接口避免了单继承的局限,多线程可以共享实现类的对象
实际上Thread也实现了Runnable接口
Runnble更容易与线程池等高级API配合
实现Callable接口
- 实现Callable接口
- 创建实现类对象
- 创建FutureTask对象,传入实体类对象
- 创建Thread对象,传入FutureTask对象
- 调用start()
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
class A implements Callable{ //1.创建Callable实现类
public Object call() throws Exception{ //2.实现call方法---可以抛异常
......
return obj; //有返回值
}
}
public class ATest{
public static void main(String[] args){
A a = new A(); //3.创建实现类对象
//4.作为形参传递到FutureTask构造器
FutureTask futureTask = new FutureTask (a);
//5.futureTask作为形参,传递到Thread构造器,并调用start方法
new Thread(futureTask).start();
//返回值有用,可执行如下代码
//获取call方法的返回值
//get()返回值即FutureTask构造器参数中,重写call方法的返回值
Object obj = futureTask.get();
System.out.println(obj);
}
}
public class ATest{
public static void main(String[] args){
new Thread(new FutureTask(() -> {
System.out.println("================");
return null;
}),"thread1").start();
}
}
**与继承Thread、实现Runnable比较:**有返回值,可抛异常,支持泛型
线程池(推荐)
public class ThreadPool{
public static void main(String[] args){
//指定线程池中线程数量
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
service1.setCorePoolSize(15); //线程数量
service1.setKeepAliveTime(); //维持时间
//2.执行指定的线程的操作。需要提供实现类的对象
service.execute(Runnable runnable);//适合适用于Runnable 这里没写实现类
service.submit(Callable callable);//适合使用于Callable 这里没写实现类
//3.关闭连接池
service.shutdown();
}
}
线程池好处: 1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
线程相关类
Thread
成员变量
优先级大只是抢占cpu执行权的概率高,并不绝对,主要看任务调度器
MAX_PRIORITY = 10
MIN_PRIORITY = 1
NORM_PRIORITY = 5 //默认优先级
构造器
Thread() //创建线程对象
Thread(String threadname) //创建线程对象并命名
Thread(Runnable target) //创建线程对象,该对象实现了Runnable接口中的run
Thread(Runnable target,String name) //创建对象,实现Runnable,并命名
常用方法
start() //启动当前线程;调用当前线程的run()
//代码不一定立刻运行,需要等待CPU时间片
//每个线程对象只能调用一次,否则报IllegalThreadStateException
run() //线程中要执行的方法(需要重写)
//直接执行不会开启新的线程
getName() //获取当前线程的名字
setName() //设置当前线程的名字
getPriority() //获取当期线程优先级
setPriority(int p)//设置线程优先级1-10
setDaemon(true) //设置为守护线程
getState() //获取线程状态
//NEW;RUNNABLE;BLOCKED;WAITING;TIMED_WAITING;TERMINATED
join() //插队,直至线程结束 a.join(),a线程加入,原线程阻塞
join(long n) //n:插队时长,到时间原线程解除阻塞
//但n大于代码执行的时间,只要执行完,原线程就解除阻塞,不用等到n
isAlive() //判断当前线程是否存活(有没有运行结束)
interrupt() //打断线程
//打断正sleep、wait、join,会报InterruptException,并清除打断标记
//打断正运行的线程、park的线程,则设置打断标记
isInterrupted() //判断线程是否被打断,不会清除打断标记
"静态方法"
interrupted() //静态方法,判断当前线程是否被打断,会清除打断标记
currentThread() //静态方法,返回当前线程,Thread.currentThread().getName()
sleep(long millitime) //静态方法,millitime毫秒。指定时间内阻塞
//Running ——> Timed Waiting
//睡眠结束后不一定立刻执行,等CPU时间片
//可以用TimeUnit的sleep代替Thread的sleep,提高可读性
yield() //静态方法,释放当前cpu的执行权,进入就绪状态
//Running ——> Runnable
//可能不能真正释放,没有其他线程时,还是会被分配到时间片
LockSupport
park-unpark静态方法
//暂停当前线程 LockSupport.park(); //恢复某个线程 LockSupport.unpark(暂停线程对象);park()的线程被打断,则设置打断标记,即取消暂停,再次调用park()也无法停止
@Slf4j public class TestPark { public static void main(String[] args) throws InterruptedException { test2(); } private static void test2() throws InterruptedException { Thread t1 = new Thread(() -> { while(true){ log.debug("park...");//将要暂停 LockSupport.park(); //暂停 log.debug("unpark..."); //将要恢复 --- 暂停可能被打断 log.debug("打断状态:{}", Thread.currentThread().isInterrupted()); //不清除标记 //改为 log.debug("打断状态:{}", Thread.interrupted()); 清除标记 } }, "t1"); t1.start(); Thread.sleep(500); t1.interrupt(); //0.5秒后打断 } } /*改前 [DEBUG] 00:09:54.258 [t1] c.b.t.TestPark - park... [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - unpark... [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - 打断状态:true [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - park... [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - unpark... [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - 打断状态:true [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - park... [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - unpark... [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - 打断状态:true [DEBUG] 00:14:31.238 [t1] c.b.t.TestPark - park... ...... ......*/ /*改后 [DEBUG] 00:09:54.258 [t1] c.b.t.TestPark - park... [DEBUG] 00:09:54.759 [t1] c.b.t.TestPark - unpark... [DEBUG] 00:09:54.759 [t1] c.b.t.TestPark - 打断状态:true [DEBUG] 00:09:54.760 [t1] c.b.t.TestPark - park... */
Atomic
AtomicInteger等
J.U.C 并发包提供了:AtomicBoolean、AtomicInteger、AtomicLong
以AtomicInteger为例
AtomicInteger i = new AtomicInteger(5) i.getAndIncrement() //i++ i.incrementAndGet() //++i i.decrementAndGet() //i-- i.getAndDecrement() //--i i.getAndAdd(5) //= 后+5 i.addAndGet(5) //+= 5 i.getAndUpdate(p -> p - 2) //可以四则运算 i.updateAndGet(p -> p * 2) //可以四则运算 i.getAndAccumulate(10, (p, x) -> p + x) // i.accumulateAndGet(10, (p, x) -> p + x) //updateAndGet()原理
public void UpdateAndGet(){ while(true){ int prev = get(); int next = prev * 10; //封装成接口 if(i.compareAndSet(prev,next)){ break; } } } //封装后 public final int UpdateAndGet(IntUnaryOperator updateFunction){ int prev,next; while(true){ prev = get(); next = updateFunction.applyAsInt(prev); if(compareAndSet(prev,next)){ break; } } return next; } public interface IntUnaryOperator{ int applyAsInt(Int operand); }
AtomicReference等
原子引用类型AtomicReference、AtomicMarkableReference、AtomicStampedReference
//AtomicReference get(); compareAndSet(V,V); getAndUpdate(UnaryOperator);解决BigDecimal等可能存在线程安全问题
// AtomicReference<BigDecimal> ref = new AtomicReference<>(new BigDecimal("1000")); //核心代码 while(true){ BigDecimal prev = get(); BigDecimal next = prev.subtract(amount); if(compareAndSet(prev,next)){ break; } }AtomicMarkableReference、AtomicStampedReference解决了ABA问题
//第一个参数为需要原子化的引用对象,第二个参数是初始版本号 static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); //核心代码 int stamp = ref.getStamp(); //获取版本号 compareAndSet(prev, next, stamp, stamp + 1); //修改成功后版本号+1//AtomicMarkableReference只关注有没有改过 AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", true); compareAndSet(prev, next, true, false); //修改成功后标识改为false
AtomicIntegerArray等
原子数组AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
保证了数组元素的原子性
public class AtomicIntegerArrayTest { public static void main(String[] args) { //demo方法在下面 demo( ()->new int[10], (array)->array.length, (array, index) -> array[index]++, array-> System.out.println(Arrays.toString(array)) ); System.out.println("========================"); demo( ()-> new AtomicIntegerArray(10), (array) -> array.length(), (array, index) -> array.getAndIncrement(index), array -> System.out.println(array) ); } }[8743, 8707, 8694, 8707, 8757, 8750, 8718, 8736, 8776, 8767] ======================== [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]/** * 参数1,提供数组、可以是线程不安全数组或线程安全数组 * 参数2,获取数组长度的方法 * 参数3,自增方法,回传 array, index * 参数4,打印数组的方法 */ private static <T> void demo( Supplier<T> arraySupplier, Function<T, Integer> lengthFun, BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) { List<Thread> ts = new ArrayList<>(); T array = arraySupplier.get(); int length = lengthFun.apply(array); for (int i = 0; i < length; i++) { // 每个线程对数组作 10000 次操作 ts.add(new Thread(() -> { for (int j = 0; j < 10000; j++) { //10000次操作平均到每个元素上 putConsumer.accept(array, j % length);//accept是BiConsumer抽象方法 } })); } ts.forEach(t -> t.start()); // 启动所有线程 ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 等所有线程结束 printConsumer.accept(array); } // supplier 提供者 无中生有 ()->结果 // function 函数 一个参数一个结果(参数)->结果 , // BiFunction(参数1, 参数2)->结果 // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
AtomicIntegerFieldUpdater等
字段更新器AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
针对属性进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常IllegalArgumentException
Student student = new Student(); /** * 参数1,哪个类的属性 * 参数2,属性类型 * 参数3,属性名 */ AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name"); /** * 参数1,哪个对象 * 参数2,改前值 * 参数3,改后值 */ updater.compareAndSet(student,"张三","李四");class Student{ volatile String name = "张三"; }
LongAdder等
原子累加器LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
效率比AtomicInteger、AtomicLong高
在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能
public class LongAdderTest { public static void main(String[] args) { for (int i = 0; i < 5; i++) { demo(() -> new AtomicLong(0),(adder) -> adder.getAndIncrement()); } System.out.println("=============="); for (int i = 0; i < 5; i++) { //空参构造,默认从0开始,调用increment自增 demo(() -> new LongAdder(),(adder) -> adder.increment()); } } private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) { T adder = adderSupplier.get(); ArrayList<Thread> threads = new ArrayList<>(); for (int i = 0; i < 4; i++) { threads.add(new Thread(() ->{ for (int j = 0; j < 500000; j++) { action.accept(adder); } })); } long start = System.nanoTime(); threads.forEach(thread -> thread.start()); threads.forEach(thread -> { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder + " 花费了 " + (end - start) / 1000_000); } }2000000 花费了 75 2000000 花费了 77 2000000 花费了 69 2000000 花费了 63 2000000 花费了 62 ============== 2000000 花费了 28 2000000 花费了 12 2000000 花费了 11 2000000 花费了 11 2000000 花费了 9
Unsafe
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
并不是不安全,而是直接操作底层,不建议直接调用
只能通过反射获取
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe)theUnsafe.get(null);CAS
/** * 被测类 */ class Student{ volatile int id; volatile String name; } /** * 测试类 */ public class UnsafeTest { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe)theUnsafe.get(null); Student student = new Student(); //获取成员变量的偏移量 long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id")); long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name")); //执行cas unsafe.compareAndSwapInt(student,id,0,10); unsafe.compareAndSwapObject(student,name,null,"张三"); System.out.println(student.id + student.name); } }自定义原子整数类
class MyAtomicInteger { private volatile int value; private static final long VALUE_OFFSET; private static final Unsafe UNSAFE; static { try { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); UNSAFE = (Unsafe) theUnsafe.get(null); VALUE_OFFSET = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value")); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } } public MyAtomicInteger(int value) { this.value = value; } public void decrease(int i) { int prev, next; do { prev = value; next = value - i; } while (!UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, next)); } public int get() { return value; } } /** * 测试 */ class MyAtomicIntegerTest { public static void main(String[] args) { MyAtomicInteger myAtomicInteger = new MyAtomicInteger(10000); List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 1000; j++) { //通过自定义原子类修改 --- 安全 myAtomicInteger.decrease(1); /*通过反射直接修改 --- 不安全 try { Field value = MyAtomicInteger.class.getDeclaredField("value"); value.setAccessible(true); value.set(myAtomicInteger,(int)value.get(myAtomicInteger)-1); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }*/ } }, "Thread" + i); threads.add(thread); thread.start(); } for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(myAtomicInteger.get()); } }
ThreadPoolExecutor
构造方法
public ThreadPoolExecutor(int corePoolSize, //核心线程数 int maximumPoolSize, //最大线程数(=核心线程数 + 救急线程) long keepAliveTime, //救急线程存活时间 TimeUnit unit, //时间单位 BlockingQueue<Runnable> workQueue, //阻塞队列 ThreadFactory threadFactory, //线程工厂 --- 可以给线程起名字 RejectedExecutionHandler handler) //拒绝策略工作特点:
先直接执行,达到corePoolSize后存入阻塞队列
队列满后,不会立刻执行拒绝策略,先用救急线程执行新来的任务(不是在阻塞队列中的)
达到存活时间,救急线程销毁
此期间达到maximumPoolSize,则启动拒绝策略
> 拒绝策略的四个实现\:ThreadPoolExecutor的内部类,实现了RejectedExecutionHandler接口 > > * AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略 > > ```java > public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { > throw new RejectedExecutionException("Task " + r.toString() + > " rejected from " + > e.toString()); > } > ``` > > * CallerRunsPolicy 让调用者运行任务 > > ```java > public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { > if (!e.isShutdown()) { > r.run(); > } > ``` > > * DiscardPolicy 放弃本次任务 > > ```java > public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {} > ``` > > * DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之 > > ```java > public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { > if (!e.isShutdown()) { > e.getQueue().poll(); > e.execute(r); > } > } > ``` > Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题 > > Netty 的实现,是创建一个新线程来执行任务 > > ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略 > > PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
工厂类 ---- 主要作用就是命名
new ThreadFactory(){ private AtomicInteger poolNum = new AtomicInteger(); @Override public Thread newThread(Runnable r) { return new Thread(r,"myPool" + poolNum.getAndIncrement()); } }Executors类中定义了很多工厂方法
newFixedThreadPool --- 固定大小线程池
最大线程数 = 核心线程数,即没有救急线程,没有超时时间
阻塞队列是无界的(0 - ∞),可以放任意数量的任务
适用于任务量已知,相对耗时的任务
```java public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } ```
newCachedThreadPool --- 缓冲线程池
最大线程数 = 救急线程数,即全是救急线程,且无限制创建(nteger.MAX_VALUE)
队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(有取的才能放)
- 放的时候陷入阻塞,取走了,放才能结束
适用于任务量大,执行时间短的任务
```java public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } ``` ```java public static void testSynchronousQueue() throws InterruptedException { SynchronousQueue<Integer> integers = new SynchronousQueue<>(); new Thread(() -> { try { log.info("put 1 start..."); integers.put(1); log.info("1 is put"); log.info("put 2 start..."); integers.put(2); log.info("2 is put"); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); sleep(1000); new Thread(() -> { try { integers.take(); log.info("1 is took"); integers.take(); log.info("2 is took"); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2").start(); } ``` ```java [INFO ] 20:58:58.417 [t1] c.b.t.TestThreadFactory - put 1 start... [INFO ] 20:58:59.419 [t1] c.b.t.TestThreadFactory - 1 is put [INFO ] 20:58:59.419 [t2] c.b.t.TestThreadFactory - 1 is took [INFO ] 20:58:59.419 [t1] c.b.t.TestThreadFactory - put 2 start... [INFO ] 20:58:59.419 [t1] c.b.t.TestThreadFactory - 2 is put [INFO ] 20:58:59.419 [t2] c.b.t.TestThreadFactory - 2 is took ```newSingleThreadExecutor --- 单线程池
线程数固定为 1,任务数多于 1 时,会放入无界队列排队
任务执行完毕,这唯一的线程也不会被释放
适用于多任务排队执行
与单线程区别
- 单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施
- 而线程池还会新建一个线程,保证池的正常工作
与newFixedThreadPool(1)区别
Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
> FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法(没有返回ThreadPoolExecutor 对象)Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
> 返回的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }newScheduledThreadPool --- 任务调度线程池
线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务
对比Timer:Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务
方法
```java /** command Runnable任务 callable Callable任务 delay 延迟 unit 时间单位 initialDelay 初始延迟时间 */ //预约执行一次 public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); //预约周期性执行 周期 = max(执行时间,间隔时间period) public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); //预约周期性执行 周期 = 执行时间 + 间隔时间 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); ```异常
- 不显示
- 主动捕捉 try-catch
- 使用 Future
任务提交
// 执行任务 void execute(Runnable command); // 提交任务 task,用返回值 Future 获得任务执行结果 <T> Future<T> submit(Callable<T> task); // 提交 tasks 中所有任务 <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) // 提交 tasks 中所有任务,带超时时间 <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 <T> T invokeAny(Collection<? extends Callable<T>> tasks) // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间 <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)关闭线程池
//状态变为 SHUTDOWN,不会接收新任务,但已提交任务会执行完,不会阻塞调用线程的执行 public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(SHUTDOWN); // 修改线程池状态 interruptIdleWorkers(); onShutdown(); // hook for ScheduledThreadPoolExecutor // 扩展点 ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也会等,执行完后结束) tryTerminate(); } //状态变为 STOP,将队列中的任务返回,用 interrupt 的方式中断正在执行的任务 public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); // 修改线程池状态 interruptWorkers(); // 打断所有线程 tasks = drainQueue(); // 获取队列中剩余任务 } finally { mainLock.unlock(); } tryTerminate(); return tasks; } //不在 RUNNING 状态的线程池,此方法就返回 true public boolean isShutdown() { return ! isRunning(ctl.get()); } // 线程池状态是否是 TERMINATED boolean isTerminated(); // 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
ForkJoinPool
体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值)
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率:
每个线程都维护了一个双端队列,用来存储需要执行的任务
工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行
窃取的必须是最晚的任务,避免和队列所属线程发生竞争,但是队列中只有一个任务时还是会发生竞争
public static void main(String[] args) { ForkJoinPool pool = new ForkJoinPool(4); System.out.println(pool.invoke(new MyTask(5))); //拆分 5 + MyTask(4) --> 4 + MyTask(3) --> } //对 1~n 之间的整数求和的任务 class AddTask1 extends RecursiveTask<Integer> { int n; public AddTask1(int n) { this.n = n; } @Override public String toString() { return "{" + n + '}'; } //计算 @Override protected Integer compute() { // 如果 n 已经为 1,可以求得结果了 if (n == 1) { log.debug("join() {}", n); return n; } // 将任务进行拆分(fork) AddTask1 t1 = new AddTask1(n - 1); t1.fork(); //按t1(n-1)的方式拆分 log.debug("fork() {} + {}", n, t1); // 合并(join)结果 int result = n + t1.join(); log.debug("join() {} + {} = {}", n, t1, result); return result; } }拆分优化
class AddTask extends RecursiveTask<Integer> { int begin; int end; public AddTask(int begin, int end) { this.begin = begin; this.end = end; } @Override public String toString() { return "{" + begin + "," + end + '}'; } @Override protected Integer compute() { // 5, 5 if (begin == end) { return begin; } // 4, 5 防止多余的拆分 提高效率 if (end - begin == 1) { return end + begin; } // 1 5 int mid = (end + begin) / 2; // 3 AddTask t1 = new AddTask(begin, mid); // 1,3 t1.fork(); AddTask t2 = new AddTask(mid + 1, end); // 4,5 t2.fork(); int result = t1.join() + t2.join(); return result; } }
Semaphore
信号量,用来限制能同时访问共享资源的线程上限
synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行
Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁
构造方法:
public Semaphore(int permits):permits 表示许可线程的数量(state)public Semaphore(int permits, boolean fair):fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程常用API:
public void acquire():表示获取许可public void release():表示释放许可,acquire() 和 release() 方法之间的代码为同步代码
public static void main(String[] args) {
// 1.创建Semaphore对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 3. 获取许可
semaphore.acquire();
sout(Thread.currentThread().getName() + " running...");
Thread.sleep(1000);
sout(Thread.currentThread().getName() + " end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
CountdownLatch
CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成
构造器:
public CountDownLatch(int count):初始化唤醒需要的 down 几步
常用API:
public void await():让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待public void countDown():计数器进行减 1(down 1)
应用:同步等待多个 Rest 远程调用结束
// LOL 10人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
ExecutorService service = Executors.newFixedThreadPool(10);
String[] all = new String[10];
Random random = new Random();
for (int j = 0; j < 10; j++) {
int finalJ = j;//常量
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
Thread.sleep(random.nextInt(100)); //随机休眠
all[finalJ] = i + "%";
System.out.print("\r" + Arrays.toString(all)); // \r代表覆盖
}
latch.countDown();
});
}
latch.await();
System.out.println("\n游戏开始");
service.shutdown();
}
/*
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
游戏开始
CyclicBarrier
CyclicBarrier:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行
常用方法:
public CyclicBarrier(int parties, Runnable barrierAction):用于在线程到达屏障 parties 时,执行 barrierAction- parties:代表多少个线程到达屏障开始触发线程任务
- barrierAction:线程任务
public int await():线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障
与 CountDownLatch 的区别:CyclicBarrier 是可以重用的
应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
System.out.println("task1 task2 finish...");
});
for (int i = 0; i < 3; i++) { // 循环重用
service.submit(() -> {
System.out.println("task1 begin...");
try {
Thread.sleep(1000);
barrier.await(); // 2 - 1 = 1
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.submit(() -> {
System.out.println("task2 begin...");
try {
Thread.sleep(2000);
barrier.await(); // 1 - 1 = 0
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
service.shutdown();
}
集合类
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历
线程安全集合类可以分为三大类:
遗留的线程安全集合如 Hashtable , Vector
- 使用 Collections 装饰的线程安全集合,如:
- Collections.synchronizedCollection
- Collections.synchronizedList
- Collections.synchronizedMap
- Collections.synchronizedSet
- Collections.synchronizedNavigableMap
- Collections.synchronizedNavigableSet
- Collections.synchronizedSortedMap
- Collections.synchronizedSortedSet
- 使用 Collections 装饰的线程安全集合,如:
java.util.concurrent.*
> 重点介绍 java.util.concurrent.\* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent > > * Blocking 大部分实现基于锁,并提供用来阻塞的方法 > * CopyOnWrite 之类容器修改开销相对较重 > * Concurrent 类型的容器 > * 内部很多操作使用 cas 优化,一般可以提供较高吞吐量 > * 弱一致性 > * 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的 > * 求大小弱一致性,size 操作未必是 100% 准确 > * 读取弱一致性
ConcurrentHashMap
单词计数
生成测试数据
static final String ALPHA = "abcedfghijklmnopqrstuvwxyz"; public static void main(String[] args) { int length = ALPHA.length(); int count = 200; List<String> list = new ArrayList<>(length * count); for (int i = 0; i < length; i++) { char ch = ALPHA.charAt(i); for (int j = 0; j < count; j++) { list.add(String.valueOf(ch)); } } Collections.shuffle(list); for (int i = 0; i < 26; i++) { try (PrintWriter out = new PrintWriter( new OutputStreamWriter( new FileOutputStream("tmp/" + (i + 1) + ".txt")))) { String collect = list.subList(i * count, (i + 1) * count).stream() .collect(Collectors.joining("\n")); out.print(collect); } catch (IOException e) { } } }模版代码,模版代码中封装了多线程读取文件的代码
private static <V> void demo(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>, List<String>> consumer) { Map<String, V> counterMap = supplier.get(); List<Thread> ts = new ArrayList<>(); for (int i = 1; i <= 26; i++) { int idx = i; Thread thread = new Thread(() -> { List<String> words = readFromFile(idx); consumer.accept(counterMap, words); }); ts.add(thread); } ts.forEach(t -> t.start()); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(counterMap); } public static List<String> readFromFile(int i) { ArrayList<String> words = new ArrayList<>(); try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/" + i + ".txt")))) { while (true) { String word = in.readLine(); if (word == null) { break; } words.add(word); } return words; } catch (IOException e) { throw new RuntimeException(e); } }你要做的是实现两个参数
- 一是提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
- 二是提供一组操作,保证计数的安全性,会传递 map 集合以及 单词 List
漏洞实现
demo( // 创建 map 集合 // 创建 ConcurrentHashMap 对不对? () -> new HashMap<String, Integer>(), // 进行计数 (map, words) -> { for (String word : words) { Integer counter = map.get(word); int newValue = counter == null ? 1 : counter + 1; map.put(word, newValue); } } );参考解答1
demo( () -> new ConcurrentHashMap<String, LongAdder>(), (map, words) -> { for (String word : words) { // 注意不能使用 putIfAbsent,此方法返回的是上一次的 value,首次调用返回 null map.computeIfAbsent(word, (key) -> new LongAdder()).increment(); } } );参考解答2
demo( () -> new ConcurrentHashMap<String, Integer>(), (map, words) -> { for (String word : words) { // 函数式编程,无需原子变量 map.merge(word, 1, Integer::sum); } } );
ConcurrentLinkedQueue
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了 cas 来实现
事实上,ConcurrentLinkedQueue 应用还是非常广泛的
- 例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
线程同步
synchronized
同步代码块
synchronized (同步监视器){
// 临界区;
}
实现需要被同步的代码要恰到好处,多了可能会变成单线程,少了可能解决不了安全问题
同步监视器,俗称锁,
锁只能有一个,共用一把锁,可以用任何对象充当
可以慎用this,尤其是继承创建的多线程,每个线程new一个新对象
继承方式创建多线程时,可以考虑用当前类充当对象,确保每个线程拿到的锁是同一个对象
本质上,把锁内变成了单线程
同步方法
public synchronized void 方法名 (){
....
}
仍有同步监视器,不显式声明,非静态的方法省略了this,静态的省略了当前类
由于非静态省略this,在继承创建的多线程中,this指代不明,我们必须声明为静态方法
同步代码块与同步方法对比:
同步代码块
继承类时,共享资源需要static,锁用当前类
实现接口时,不需static,锁用this即可
同步方法
- 继承类时,同步方法需要static,隐式的锁是当前类
- 实现接口时,不需static,隐式的锁是this即可
需要被同步的代码分布在多个方法中时,我们选择同步方法,
每个方法都synchronized一般我们选择同步代码块,更有针对性,效率稍高
线程八锁
变量的线程安全分析
对于属性(成员变量、类变量)
单线程 --- 安全
没有共享资源 --- 安全
只有读操作 --- 安全
多线程,共享数据,读写操作 --- 临界区 --- 不安全
```java class ThreadUnsafe { ArrayList<String> list = new ArrayList<>(); //测试 public static void main(String[] args) { ThreadUnsafe test = new ThreadUnsafe(); for (int i = 0; i < 2; i++) { new Thread(() -> { test.method1(200); }, "Thread" + i).start(); } } public void method1(int loopNumber) { for (int i = 0; i < loopNumber; i++) { // 临界区 -- 会产生竟态条件 method2(); method3(); } } private void method2() { list.add("1"); } private void method3() { list.remove(0); } } ``` ```java //两种情况 //正常执行 //报角标越界,add方法底层有size++操作 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } ```
对于局部变量
一般是安全的
```java public static void test1() { int i = 10; i++; } ``` ```java public static void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 0, 1 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 3 4 0 i I ```但变量发生逃逸(被引用)可能存在安全问题
未逃离出方法 --- 安全
```java class ThreadSafe { public final void method1(int loopNumber) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList<String> list) { list.add("1"); } private void method3(ArrayList<String> list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadUnsafe{ public void method3(ArrayList<String> list) { new Thread(() -> { list.remove(0); }).start(); } } ``` [DEBUG] 15:00:50.859 [Thread0] c.b.t.ThreadUnsafe - 0 [DEBUG] 15:00:50.859 [Thread0] c.b.t.ThreadUnsafe - 0 [DEBUG] 15:00:50.859 [Thread0] c.b.t.ThreadUnsafe - 0 [DEBUG] 15:00:50.859 [Thread0] c.b.t.ThreadUnsafe - 0 [DEBUG] 15:00:50.859 [Thread0] c.b.t.ThreadUnsafe - 0 [DEBUG] 15:00:50.859 [Thread0] c.b.t.ThreadUnsafe - 0逃离出方法 --- 不安全
```java class ThreadSafe { //测试 public static void main(String[] args) { ThreadSafe test = new ThreadSafeSubClass(); for (int i = 0; i < 2; i++) { new Thread(() -> { test.method1(200000); }, "Thread" + i).start(); } } public final void method1(int loopNumber) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } public void method2(ArrayList<String> list) { list.add("1"); } public void method3(ArrayList<String> list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe{ public void method3(ArrayList<String> list) { new Thread(() -> { //新线程开始了共享资源 list.remove(0); }).start(); } } ``` [DEBUG] 15:03:59.656 [Thread1] c.b.t.ThreadUnsafe - 8 [DEBUG] 15:03:59.656 [Thread0] c.b.t.ThreadUnsafe - 38 [DEBUG] 15:03:59.657 [Thread0] c.b.t.ThreadUnsafe - 37 [DEBUG] 15:03:59.657 [Thread0] c.b.t.ThreadUnsafe - 38 [DEBUG] 15:03:59.657 [Thread1] c.b.t.ThreadUnsafe - 8 [DEBUG] 15:03:59.657 [Thread1] c.b.t.ThreadUnsafe - 8
安全类
String、Integer、StringBuffer、Random、Vector、Hashtable、JUC(java.util.concurrent )
它们的每个方法是原子的,但注意它们多个方法的组合不是原子的
Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); } //两个线程get到的都是null,都试图添加 //如:t1-get、t2-get、t1-put、t2-put -----> 导致后put的覆盖了先put的
例题
不可变性 ---- 安全
//Servlet单例,被线程共享 public class MyServlet extends HttpServlet { Map<String,Object> map = new HashMap<>(); //不安全 String S1 = "..."; //安全 final String S2 = "..."; //安全 Date D1 = new Date(); //不安全 final Date D2 = new Date(); //不安全,内部年月日等属性可变 public void doGet(HttpServletRequest request, HttpServletResponse response) { //使用上述变量 } }public class MyServlet extends HttpServlet { // 不安全,count是共享资源,update()有操作共享资源等临界区,发生竟态条件 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 记录调用次数 private int count = 0; public void update() { // ... count++; } }//Spring AOP //单例,可能存在线程安全,用环绕通知解决 @Aspect @Component public class MyAspect { //不安全 start是共享资源,before()、after()有临界区 private long start = 0L; @Before("execution(* *(..))") public void before() { start = System.nanoTime(); } @After("execution(* *(..))") public void after() { long end = System.nanoTime(); System.out.println("cost time:" + (end-start)); } }无状态性 ---- 安全 (没有成员变量)
public class MyServlet extends HttpServlet { // 对象属性不可变 -- 安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 无状态对象 -- 安全 private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { public void update() { String sql = "update user set password = ? where username = ?"; // 局部变量 -- 未逃逸 -- 安全 try (Connection conn = DriverManager.getConnection("","","")){ // ... } catch (Exception e) { // ... } } } /* public class UserDaoImpl implements UserDao { // 不安全 private Connection conn = null; public void update() throws SQLException { String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","",""); // ... conn.close(); } }*/public class MyServlet extends HttpServlet { // 无状态 -- 安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { public void update() { //每个线程都new一个,没有共享 -- 安全 UserDao userDao = new UserDaoImpl(); userDao.update(); } } public class UserDaoImpl implements UserDao { // 不安全,但如上调用是安全的,不推荐,有隐患 private Connection = null; public void update() throws SQLException { String sql = "update user set password = ? where username = ?"; conn = DriverManager.getConnection("","",""); // ... conn.close(); } }外星方法 ---- 不安全(不确定的,可能导致不安全的发生方法,称为外星方法)
public abstract class Test { public void bar() { // 被外星方法引用 -- 不安全 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); foo(sdf); } //外星方法 public abstract void foo(SimpleDateFormat sdf); public static void main(String[] args) { new TestImpl().bar(); } } class TestImpl extends Test{ public void foo(SimpleDateFormat sdf) { String dateStr = "1999-10-11 00:00:00"; for (int i = 0; i < 20; i++) { new Thread(() -> { try { sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } }).start(); } } }卖票
- 对卖票方法加上synchronized即可
转账
需要锁住两个账户对象
```java public void transfer(Account target, int amount) { synchronized(类.class){ if (this.money > amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } } /*错误解法 public synchronized void transfer(Account target, int amount) { if (this.money > amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } }*/ ```
Lock
ReentrantLock
与synchronized对比:
实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的
性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同,锁JVM将花费较少时间来调度线程,性能更好,具有更好扩展性(提供更多的子类)
synchronized执行完自动释放锁,lock手动解锁
- lock只有代码块锁,无方法锁
- lock可中断锁(等待锁的进程被打断),synchronized不可中断
- lock可以设置超时时间
- lock可以设置为公平锁,synchronized随机唤醒
- lock支持多个条件变量,synchronized只支持Monitor的waitSet
- 相同点:都支持可重入
......
lock.lock(); //上锁
try{
...... //需要同步的代码
}finally{
lock.unlock(); //解锁
}
锁打断
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(); //实例化ReentrantLock
log.debug("开始....");
Thread t1 = new Thread(() -> {
try {
lock.lockInterruptibly();
log.debug("获得锁..");
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断..........");
return;
} finally {
lock.unlock();
}
}, "t1");
try {
lock.lock();
log.debug("获得锁..");
t1.start();
sleep(1000);
t1.interrupt();
log.debug("打断{}",t1.getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/*
[DEBUG] 18:32:29.229 [main] c.b.j.t.ReentrantLockTest - 开始....
[DEBUG] 18:32:29.306 [main] c.b.j.t.ReentrantLockTest - 获得锁..
java.lang.InterruptedException
Exception in thread "t1" java.lang.IllegalMonitorStateException
[DEBUG] 18:32:30.323 [t1] c.b.j.t.ReentrantLockTest - 被打断..........
[DEBUG] 18:32:30.321 [main] c.b.j.t.ReentrantLockTest - 打断t1
*/
锁超时
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(); //实例化ReentrantLock
Thread t1 = new Thread(() -> {
try {
if(! lock.tryLock(1, TimeUnit.SECONDS)){//1s内没抢到锁就立刻返回,不再抢
//if(! lock.tryLock()){ //没有抢到锁就立刻返回,不再抢
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断..........");
return;
} finally {
lock.unlock();
}
}, "t1");
try {
lock.lock();
log.debug("获得锁..");
t1.start();
sleep(1000);
t1.interrupt();
log.debug("打断{}",t1.getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
tryLock解决哲学家就餐问题
//tryLock解决哲学家就餐问题 /** * 筷子 */ @ToString @AllArgsConstructor class Chopstick extends ReentrantLock{ //继承ReentrantLock String name; } /** * 哲学家 */ @Slf4j class Philosopher extends Thread { Chopstick leftChopstick; Chopstick rightChopstick; public Philosopher(String name, Chopstick leftChopstick, Chopstick rightChopstick) { super(name); this.leftChopstick = leftChopstick; this.rightChopstick = rightChopstick; } @SneakyThrows @Override public void run() { while (true) { if(leftChopstick.tryLock()){ try{ if(rightChopstick.tryLock()){ try{ eat(); }finally{ rightChopstick.unlock(); } } }finally{ leftChopstick.unlock(); } } } } public void eat() throws InterruptedException { log.debug("eat........"); sleep(1000); } }
公平锁
默认不是公平锁,可强行插入
设为公平锁后,不能强行插入,在最后输出,先到先得
构造器设置公平锁
公平锁一般没有必要,会降低并发度
ReentrantLock lock = new ReentrantLock(true);
条件变量
synchronized只有WaitSet
ReentrantLock是支持多个条件变量
Condition newCondition() //
public class ReentrantLockTest {
ReentrantLock lock = new ReentrantLock(); //实例化ReentrantLock
static Condition waitAAAQueue = lock.newCondition();
static Condition waitBBBQueue = lock.newCondition();
static volatile boolean hasAAA = false;
static volatile boolean hasBBB = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
while(! hasAAA){
waitAAAQueue.awiat();
}
} finally {
lock.unlock();
}
}, "等AAA").start();
new Thread(() -> {
try {
lock.lock();
while(! hasBBB){
waitBBBQueue.awiat();
}
} finally {
lock.unlock();
}
}, "等BBB").start();
sleep(1000);
sendAAA();
sleep(1000);
sendBBB();
}
//送AAA
private static void sendAAA() {
lock.lock();
try{
log.debug("送AAA");
hasAAA = true;
waitAAAQueue.signal();
}finally{
lock.unlock();
}
}
//送BBB
private static void sendBBB() {
lock.lock();
try{
log.debug("送AAA");
hasBBB = true;
waitBBBQueue.signal();
}finally{
lock.unlock();
}
}
}
ReentrantReadWriteLock
读写锁
独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁
共享锁:指该锁可以被多个线程锁持有
ReentrantReadWriteLock 其读锁是共享锁,写锁是独占锁 -- 读读并行,读写互斥,写写互斥
作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
规则
读锁不支持条件变量
重入时,不支持升级:读上锁 -- 写上锁 --解锁 -- 解锁 ×
> 读上锁 -- 读解锁 -- 写上锁 --写解锁重入时,支持降级:写上锁 -- 读上锁 --解锁 -- 解锁
> 写锁会互斥其他的锁,造成**只有当前线程会持有读锁**r.lock(); try { // 临界区 } finally { r.unlock(); }
方法
ReentrantReadWriteLock() //默认构造方法,非公平锁 ReentrantReadWriteLock(boolean fair) //true 为公平锁 ReentrantReadWriteLock.ReadLock readLock() //返回读锁 ReentrantReadWriteLock.WriteLock writeLock()//返回写锁 void lock() //加锁 void unlock()//解锁 boolean tryLock() //尝试获取锁
StampedLock
StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能
特点:
在使用读锁、写锁时都必须配合戳使用
StampedLock 不支持条件变量
StampedLock 不支持重入
基本用法
加解读锁:
long stamp = lock.readLock(); lock.unlockRead(stamp); // 类似于 unpark,解指定的锁加解写锁:
long stamp = lock.writeLock(); lock.unlockWrite(stamp);乐观读,StampedLock 支持
tryOptimisticRead()方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性long stamp = lock.tryOptimisticRead(); // 验戳 if(!lock.validate(stamp)){ // 锁升级 }
提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法:
- 读-读可以优化
- 读-写优化读,补加读锁
public static void main(String[] args) throws InterruptedException {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1000);
},"t1").start();
Thread.sleep(500);
new Thread(() -> {
dataContainer.write(1000);
},"t2").start();
}
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public int read(int readTime) throws InterruptedException {
long stamp = lock.tryOptimisticRead();
System.out.println(new Date() + " optimistic read locking" + stamp);
Thread.sleep(readTime);
// 戳有效,直接返回数据
if (lock.validate(stamp)) {
Sout(new Date() + " optimistic read finish..." + stamp);
return data;
}
// 说明其他线程更改了戳,需要锁升级了,从乐观读升级到读锁
System.out.println(new Date() + " updating to read lock" + stamp);
try {
stamp = lock.readLock();
System.out.println(new Date() + " read lock" + stamp);
Thread.sleep(readTime);
System.out.println(new Date() + " read finish..." + stamp);
return data;
} finally {
System.out.println(new Date() + " read unlock " + stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
System.out.println(new Date() + " write lock " + stamp);
try {
Thread.sleep(2000);
this.data = newData;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(new Date() + " write unlock " + stamp);
lock.unlockWrite(stamp);
}
}
}
乐观锁(无锁)
- synchronized和Lock实现类都是悲观锁
CAS
转账为例
悲观锁
class AccountUnsafe implements Account { private Integer balance; public AccountUnsafe(Integer balance) { this.balance = balance; } @Override public synchronized Integer getBalance() { return balance; } @Override public synchronized void withdraw(Integer amount) { balance -= amount; } }无锁
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
不一致了,next 作废,返回 false 表示失败,开始下一轮,直至成功
一致,以 next 设置为新值,返回 true 表示成功
class AccountUnsafe implements Account { private AtomicInteger balance; //原子整形 public AccountUnsafe(Integer balance) { this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); //调用AtomicInteger中的git() } @Override public void withdraw(Integer amount) { while(true){ int prev = balance.get(); int next = prev - amount; if(balance.compareAndSet(prev,next)){ break; } } } }
compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性,在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰,CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
效率高
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞
- 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,否则依然因分不到时间片,发生上下文切换
特点
- 结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了就再重试
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量
- CAS 体现的是无锁并发、无阻塞并发
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
线程模式
终止模式
错误方式
- 调用stop方法、suspend方法,没时间料理后事,尤其是该线程锁住了共享资源,杀死后不能再释放锁
- stop停止线程
- suspend挂起线程 --- resume恢复线程
- System.exit(int),杀死整个java程序
- 调用stop方法、suspend方法,没时间料理后事,尤其是该线程锁住了共享资源,杀死后不能再释放锁
两阶段终止模式
利用isInterrupted --- interrupt打断
```java public class TPTInterrupt { private Thread thread; /** * 测试线程 */ public static void main(String[] args) { TPTInterrupt t = new TPTInterrupt(); t.start(); Thread.sleep(3500); //省略了try-catch System.out.println("stop");; t.stop(); } /** * 执行监控线程 */ public void start(){ thread = new Thread(() -> { while(true) { Thread current = Thread.currentThread(); if(current.isInterrupted()) { System.out.println("料理后事"); break; } try { Thread.sleep(1000); //sleep被打断,报异常,且标识被清除 System.out.println("将结果保存"); //在这里被打断,不报异常,标识不被清除 } catch (InterruptedException e) { current.interrupt(); //重新标识,避免无法进入if } } },"监控线程"); thread.start(); } /** * 停止监控 --- 打断 */ public void stop() { thread.interrupt(); } } ``` 将结果保存 将结果保存 将结果保存 stop 料理后事
两阶段终止模式--volatile改进
用current.isInterrupted()和current.interrupt()略有麻烦
public class TPTInterrupt { private Thread thread; private volatile boolean stop; //打断标记 /** * 测试线程 */ public static void main(String[] args) { TPTInterrupt t = new TPTInterrupt(); t.start(); Thread.sleep(3500); //省略了try-catch System.out.println("stop");; t.stop(); } /** * 执行监控线程 */ public void start(){ thread = new Thread(() -> { while(true) { Thread current = Thread.currentThread(); if(stop) { //读 System.out.println("料理后事");; break; } try { Thread.sleep(1000); System.out.println("将结果保存"); } catch (InterruptedException e) { } } },"监控线程"); thread.start(); } /** * 停止监控 --- 打断 */ public void stop() { stop = true; //写 只能保证thread线程下次if时停止 thread.interrupt(); //可以保证立刻停止 } }
同步模式
保护性暂停
- 保护性暂停(Guarded Suspension),用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject“保护对象”
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
实现
class GuardedObject{
private Object response;
private final Object lock = new Object();
public Object get() {
synchronized (lock) {
// 条件不满足则等待
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
lock.notifyAll();
}
}
}
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
try {
// 子线程执行下载
List<String> response = download();
log.debug("download complete...");
guardedObject.complete(response);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
log.debug("waiting...");
// 主线程阻塞等待
Object response = guardedObject.get();
log.debug("get response: [{}] lines", ((List<String>) response).size());
}
超时版实现
class GuardedObject{
private Object response;
private final Object lock = new Object();
public Object get(long millis) { //带时间形参
synchronized (lock) {
long begin = System.currentTimeMillis();// 1) 记录最初时间
long timePassed = 0; // 2) 已经经历的时间
// 条件不满足则等待
while (response == null) {
long waitTime = millis - timePassed; // 4) 还要等的时间
if (waitTime <= 0) {
break;
}
lock.wait(waitTime); //带形参的wait
timePassed = System.currentTimeMillis() - begin;//3) 重置已经经历的时间
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
lock.notifyAll();
}
}
}
多任务版实现
不再一对一
会有并发修改异常问题 --- ConcurrentModificationException

image-20220916224212275 - 图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员
- 如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理
@Slf4j
public class GuardedSuspensionTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new PeoPle().start();
}
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (Integer id : Boxes.getIds()) {
new Postman(id, "内容是" + id).start();
}
}
}
/**
* 保护对象类
*/
class GuardedObject {
private int id;
private Object response;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
public synchronized Object getResponse(int timeout) {
//开始时间
long begin = System.currentTimeMillis();
//经历时间
long passTime = 0;
while (response == null) {
//还需等待时间
long waitTime = timeout - passTime;
if (waitTime <= 0) {
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
//重置经历时间
passTime = System.currentTimeMillis() - begin;
}
return response;
}
public synchronized void complete(Object response) {
this.response = response;
this.notifyAll();
}
}
/**
* 中间类
*/
class Boxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
//产生唯一id
private static int id;
//setGurdedID
private static synchronized int setGuardedID() {
return id++;
}
//获取GuardedObject
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
//创建GuardedObject
public static GuardedObject createGuardedObject() {
GuardedObject guardedObject = new GuardedObject(setGuardedID());
boxes.put(guardedObject.getId(), guardedObject);
return guardedObject;
}
//获取ID列表
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
/**
* 业务类:等待接收方
*/
@Slf4j
class PeoPle extends Thread {
@Override
public void run() {
//准备接收:等待接收方创建一个GuardedObject,等着接收
GuardedObject guardedObject = Boxes.createGuardedObject();
log.debug("开始接收{}",guardedObject.getId());
//开始接收
Object response = guardedObject.getResponse(500000);
//接收完毕
log.debug("接收到了......{},,内容是:{}",guardedObject.getId(),response.toString());
}
}
/**
* 业务类:提供服务方
*/
@Slf4j
class Postman extends Thread {
private int id;
private String response;
public Postman(int id, String response) {
this.id = id;
this.response = response;
}
@Override
public void run() {
//准备发送:提供服务方把对应id的GuardedObject发送给消费者
GuardedObject guardedObject = Boxes.getGuardedObject(id);
log.debug("开始发送{},,内容是:{}",guardedObject.getId(),response.toString());
//开始发送
guardedObject.complete(response);
//发送完成
log.debug("发送完成{}",guardedObject.getId());
}
}
Balking(犹豫)模式
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
class MonitorService { // 用来表示是否已经有线程已经在执行启动了 private volatile boolean starting; public void start() { //尝试启动监控线程 synchronized (this) { if (starting) { return; } starting = true; } // 真正启动监控线程... } }用来实现线程安全的单例
public final class Singleton { private Singleton() {} private static Singleton INSTANCE = null; public static synchronized Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } }
顺序控制
固定顺序
wait-notify
- 需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
- 如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
- 唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个
park-unpark(推荐)
park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』
Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) {} // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行 LockSupport.park(); System.out.println("1"); }); Thread t2 = new Thread(() -> { System.out.println("2"); // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) LockSupport.unpark(t1); }); t1.start(); t2.start();
交替输出
wait-notify
class SyncWaitNotify {
private int flag; //线程标记
private int loopNumber; //重复次数
public SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
syncWaitNotify.print(3, 1, "c");
}).start();
Lock条件变量
class AwaitSignal extends ReentrantLock {
public void start(Condition first) {
this.lock();
try {
first.signal();
} finally {
this.unlock();
}
}
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
current.await();
log.debug(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
// 循环次数
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
}
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> {
as.print("a", aWaitSet, bWaitSet);
}).start();
new Thread(() -> {
as.print("b", bWaitSet, cWaitSet);
}).start();
new Thread(() -> {
as.print("c", cWaitSet, aWaitSet);
}).start();
as.start(aWaitSet);
该实现没有考虑 a,b,c 线程都就绪再开始
park-unpark
class SyncPark {
private int loopNumber;
private Thread[] threads;
public SyncPark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(nextThread());
}
}
private Thread nextThread() {
Thread current = Thread.currentThread();
int index = 0;
for (int i = 0; i < threads.length; i++) {
if(threads[i] == current) {
index = i;
break;
}
}
if(index < threads.length - 1) {
return threads[index+1];
} else {
return threads[0];
}
}
public void start() {
for (Thread thread : threads) {
thread.start();
}
LockSupport.unpark(threads[0]);
}
}
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
syncPark.print("a");
});
Thread t2 = new Thread(() -> {
syncPark.print("b");
});
Thread t3 = new Thread(() -> {
syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();
异步模式
生产者、消费者
- 保护性暂停中的 GuardObject 不同,需要产生结果和消费结果的线程一一对应,异步则不需要,只要有,就可以执行
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
/**
* 消息
*/
class Message {
private int id; //消息标识
private Object message; //消息
public Message(int id, Object message) {
this.id = id;
this.message = message;
}
public int getId() {
return id;
}
public Object getMessage() {
return message;
}
}
/**
* 消息队列
*/
class MessageQueue {
private LinkedList<Message> queue; //消息队列
private int capacity; //队列容量
public MessageQueue(int capacity) {
this.capacity = capacity;
queue = new LinkedList<>();
}
//获取消息
public Message take() {
synchronized (queue) {
while (queue.isEmpty()) { //等
log.debug("没货了, wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = queue.removeFirst();//取出
queue.notifyAll();
return message;
}
}
//添加消息
public void put(Message message) {
synchronized (queue) {
while (queue.size() == capacity) { //超限
log.debug("库存已达上限, wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(message); //添加
queue.notifyAll(); //叫醒
}
}
}
MessageQueue messageQueue = new MessageQueue(2);
// 4 个生产者线程, 下载任务
for (int i = 0; i < 4; i++) {
int id = i;
new Thread(() -> {
try {
log.debug("download...");
List<String> response = Downloader.download();
log.debug("try put message({})", id);
messageQueue.put(new Message(id, response));
} catch (IOException e) {
e.printStackTrace();
}
}, "生产者" + i).start();
}
// 1 个消费者线程, 处理结果
new Thread(() -> {
while (true) {
Message message = messageQueue.take();
List<String> response = (List<String>) message.getMessage();
log.debug("take message({}): [{}] lines", message.getId(), response.size());
}
}, "消费者").start();
package com.botuer.thread;
class Clerk { // 售货员
private int product = 0;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产了第" + product + "个产品");
notifyAll();
}
}
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" +
product + "个产品");
product--;
notifyAll();
}
}
}
class Consumer implements Runnable { // 消费者
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}
class Productor implements Runnable { // 生产者
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread = new Thread(new Productor(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));
productorThread.start();
consumerThread.start();
}
}
分工模式
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式
对比另一种多线程设计模式:Thread-Per-Message,成本太高
固定大小线程池(或单线程池)会有饥饿现象
这里的饥饿类似死锁,但是jconsole检测不到
每个线程都可以完成多个任务,但每个线程都在执行一个多任务的任务时被一个任务占住了,没有多余的线程处理下面的任务,陷入了类似死锁的状态
@Slf4j public class WorkerThreadTest { static final List<String> MENU = Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅"); static Random RANDOM = new Random(); static String cooking(){ return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.execute(()->{ log.info("点餐"); Future<String> future = executorService.submit(() -> { log.info("做饭"); return cooking(); }); try { log.info("上菜:{}",future.get()); } catch (InterruptedException |ExecutionException e) { e.printStackTrace(); } }); executorService.execute(()->{ log.info("点餐"); Future<String> future = executorService.submit(() -> { log.info("做饭"); return cooking(); }); try { log.info("上菜:{}",future.get()); } catch (InterruptedException |ExecutionException e) { e.printStackTrace(); } }); } }[INFO ] 15:58:28.971 [pool-1-thread-2] c.b.t.WorkerThreadTest - 点餐 [INFO ] 15:58:28.971 [pool-1-thread-1] c.b.t.WorkerThreadTest - 点餐增加线程池大小解决不了根本问题
分工
ExecutorService orderingPool = Executors.newFixedThreadPool(2); ExecutorService cookingPool = Executors.newFixedThreadPool(2); orderingPool.execute(()->{ log.info("点餐"); Future<String> future = cookingPool.submit(() -> { log.info("做饭"); return cooking(); }); try { log.info("上菜:{}",future.get()); } catch (InterruptedException |ExecutionException e) { e.printStackTrace(); } });
创建多少线程池合适
一般来说池中总线程数是核心池线程数量两倍,确保当核心池有线程停止时,核心池外有线程进入核心池
过小会导致程序不能充分地利用系统资源、容易导致饥饿
过大会导致更多的线程上下文切换,占用更多内存
核心线程数
CPU 密集型运算 通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费
I/O 密集型运算 CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率
经验公式如下
**线程数 = 核数 \* 期望 CPU 利用率 \* 总时间(CPU计算时间+等待时间) / CPU 计算时间** 例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式 4 \* 100% \* 100% / 50% = 8
不可变模式
日期转换问题
SimpleDateFormat 不是线程安全的 -- 报异常NumberFormatException 或 错误的日期
@Slf4j public class FinalTest { public static void main(String[] args) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); for (int i = 0; i < 10; i++) { new Thread(() -> { try { log.debug("{}", sdf.parse("1951-04-21")); } catch (Exception e) { log.error("{}", e); } }).start(); } } }可以加同步锁 --- 但是效率低
不可变 --- DateTimeFormatter
- 所有属性都是final
不可变设计模式
- final 的使用
- 所有属性都是 final 的
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
- 保护性拷贝
- 通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
- private的使用
- 只提供get方法,不提供set方法
- 但是反射可以暴力更改,所以配合final使用
- 无状态 --- 没有属性
享元模式
Flyweight pattern. 当需要重用数量有限的同一类对象时用到的一种模式
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects
一个轻量级对象是通过与其他类似对象共享尽可能多的数据来最大限度地减少内存使用的对象
包装类中的应用
Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象
public static Long valueOf(long l) { final int offset = 128; if (l >= -128 && l <= 127) { // will cache return LongCache.cache[(int)l + offset]; } return new Long(l); }注意:
- Byte, Short, Long 缓存的范围都是 -128~127
- Character 缓存的范围是 0~127
- Integer的默认范围是 -128~127
- 最小值不能变
- 但最大值可以通过调整虚拟机参数
-Djava.lang.Integer.IntegerCache.high来改变
- Boolean 缓存了 TRUE 和 FALSE
String
- String(final + 保护性拷贝)
BigDecimal、BigInteger
- 线程安全(final + 保护性拷贝),但是多个方法组合不能保障线程安全(原子性),故需要AtomicReference
自定义连接池
@Slf4j
class Pool {
//连接池大小
private final int poolSize;
//连接数组
private Connection[] connections;
//连接状态,0空闲,1繁忙
private AtomicIntegerArray states;
//构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MyConnection();
}
}
//借连接
public Connection borrow() throws InterruptedException {
while (true) {
for (int i = 0; i < poolSize; i++) {
if (states.compareAndSet(i, 0, 1)) {
{
log.debug("借到了,连接{}", i);
return connections[i];
}
}
}
//cas适合段时间运行的片段,长时间会占用CPU资源
synchronized (this) {
log.debug("没借到,等");
wait();
}
}
}
//归还连接
public void free(Connection connection){
for (int i = 0; i < poolSize; i++) {
if(connections[i] == connection){
states.set(i,0); //不存在竞争,只有自己可以归还
synchronized (this) {
log.debug("用好了,连接{}已归还",i);
notifyAll();
}
break;
}
}
}
}
//Connection是java.sql包下的接口,实现方法省略
class MyConnection implements Connection {}
public class PoolTest {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(()->{
Connection conn = null;
try {
conn = pool.borrow();
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
}
}
线程应用
异步调用
大文件格式转换,避免阻塞
tomcat的异步servlet,让用户线程处理耗时操作,避免阻塞tomcat的工作线程
ui程序中,开发进程进行其他操作,避免阻塞ui进程
提高效率
- 单核下,时间片轮询,不同任务之间切换,不至于一个线程长时间占用CPU
- 多核下,能后拆分的任务,并行执行,提高效率(阿姆达尔定律)
- IO操作不占用CPU,只是文件拷贝使用的是阻塞IO,CPU需要等待IO结束,所以后面有了非阻塞IO和异步IO(NIO)
- 单核下,时间片轮询,不同任务之间切换,不至于一个线程长时间占用CPU
同步等待
- join
统筹
join:小王泡茶
@Slf4j public class TestTea { public static void main(String[] args) { Thread t1 = new Thread(()->{ log.debug("洗水壶"); try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("烧开水"); try { sleep(15); } catch (InterruptedException e) { e.printStackTrace(); } },"老王"); Thread t2 = new Thread(() -> { log.debug("洗茶壶"); try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("洗茶杯"); try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("拿茶叶"); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("泡茶"); },"小王"); t1.start(); t2.start(); } }[DEBUG] 11:34:08.305 [小王] c.b.t.TestTea - 洗茶壶 [DEBUG] 11:34:08.305 [老王] c.b.t.TestTea - 洗水壶 [DEBUG] 11:34:08.308 [小王] c.b.t.TestTea - 洗茶杯 [DEBUG] 11:34:08.308 [老王] c.b.t.TestTea - 烧开水 [DEBUG] 11:34:08.310 [小王] c.b.t.TestTea - 拿茶叶 [DEBUG] 11:34:08.324 [小王] c.b.t.TestTea - 泡茶wait-notify:通信 -- 准备好后告知对方,两人都可泡
@Slf4j public class TestTea { static boolean water = false; //true开水 static String tea = null; //拿到的什么茶 static boolean isMake = false; //泡了吗 static final Object lock = new Object(); //锁 public static void main(String[] args) { Thread t1 = new Thread(()->{ log.debug("洗水壶"); sleep(1000); log.debug("烧开水"); sleep(5000); water = true; synchronized (lock) { lock.notifyAll(); while (tea == null){ lock.wait(); } if(! isMake){ log.debug("{}泡{}茶",Thread.currentThread().getName(),tea); isMake = true; } } },"老王"); Thread t2 = new Thread(() -> { log.debug("洗茶壶"); sleep(1000); log.debug("洗茶杯"); sleep(1000); log.debug("拿茶叶"); sleep(1000); tea = "龙井"; synchronized (lock){ lock.notifyAll(); while (! water){ lock.wait(); } if (! isMake){ log.debug("{}泡{}茶",Thread.currentThread().getName(),tea); isMake = true; } } },"小王"); t1.start(); t2.start(); } }[DEBUG] 12:22:13.900 [老王] c.b.t.TestTea - 洗水壶 [DEBUG] 12:22:13.900 [小王] c.b.t.TestTea - 洗茶壶 [DEBUG] 12:22:14.903 [小王] c.b.t.TestTea - 洗茶杯 [DEBUG] 12:22:14.903 [老王] c.b.t.TestTea - 烧开水 [DEBUG] 12:22:15.904 [小王] c.b.t.TestTea - 拿茶叶 [DEBUG] 12:22:19.903 [老王] c.b.t.TestTea - 老王泡龙井茶第三者协调 -- 打破两个线程都可能等待
@Slf4j public class TestTea { static boolean water = false; //true开水 static String tea = null; //拿到的什么茶 static final Object lock = new Object(); //锁 public static void main(String[] args) { new Thread(()->{ log.debug("洗水壶"); sleep(1000); log.debug("烧开水"); sleep(5000); water = true; synchronized (lock) { lock.notifyAll(); } },"老王").start(); new Thread(() -> { log.debug("洗茶壶"); sleep(1000); log.debug("洗茶杯"); sleep(1000); log.debug("拿茶叶"); sleep(1000); tea = "龙井"; synchronized (lock){ lock.notifyAll(); } },"小王").start(); new Thread(() ->{ synchronized (lock){ while (water == false || tea == null){ try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("{}泡{}茶",Thread.currentThread().getName(),tea); } },"王夫人").start(); } }
定时任务
让每周四 18:00:00 定时执行任务
// 获得当前时间 LocalDateTime now = LocalDateTime.now(); // 获取本周四 18:00:00.000 LocalDateTime thursday = now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0); // 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000 if(now.compareTo(thursday) >= 0) { thursday = thursday.plusWeeks(1); } // 计算时间差,即延时执行时间 long initialDelay = Duration.between(now, thursday).toMillis(); // 计算间隔时间,即 1 周的毫秒值 long oneWeek = 7 * 24 * 3600 * 1000; ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); System.out.println("开始时间:" + new Date()); executor.scheduleAtFixedRate(() -> { System.out.println("执行时间:" + new Date()); }, initialDelay, oneWeek, TimeUnit.MILLISECONDS);
缓存
缓存更新时,是先清缓存还是先更新数据库
先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存

image-20220923164251912 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据

image-20220923164308092 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

image-20220923164057272
class GenericCachedDao<T> {
// HashMap 作为缓存非线程安全, 需要保护
HashMap<SqlPair, T> map = new HashMap<>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
GenericDao genericDao = new GenericDao();
//装饰器模式,对原方法进行装饰
public int update(String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
try {
int rows = genericDao.update(sql, params);
map.clear();
return rows;
} finally {
lock.writeLock().unlock();
}
}
//装饰器模式,对原方法进行装饰
public T queryOne(Class<T> beanClass, String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加读锁, 防止其它线程对缓存更改
lock.readLock().lock();
try {
T value = map.get(key);
if (value != null) {
return value;
}
} finally {
lock.readLock().unlock();
}
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
try {
// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
// 为防止重复查询数据库, 再次验证
T value = map.get(key);
if (value == null) {
// 如果没有, 查询数据库
value = genericDao.queryOne(beanClass, sql, params);
map.put(key, value);
}
return value;
} finally {
lock.writeLock().unlock();
}
}
// 作为 key 保证其是不可变的
class SqlPair {
private String sql;
private Object[] params;
public SqlPair(String sql, Object[] params) {
this.sql = sql;
this.params = params;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SqlPair sqlPair = (SqlPair) o;
return sql.equals(sqlPair.sql) &&
Arrays.equals(params, sqlPair.params);
}
@Override
public int hashCode() {
int result = Objects.hash(sql);
result = 31 * result + Arrays.hashCode(params);
return result;
}
}
}
注意
- 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
- 适合读多写少,如果写操作比较频繁,以上实现性能低
- 没有考虑缓存容量oom
- 没有考虑缓存过期oom
- 只适合单机
- 并发性还是低,目前只会用一把锁
- 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
- 乐观锁实现:用 CAS 去更新
线程原理
JMM & volatile
- Java Memory Model ---------- Java 内存模型,定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
- JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
原子性
非原子性操作,时间片轮训时,出现指令交错
如对静态变量的自增自减
```java //i++ getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //加 isub 减 putstatic i //修改后的存到静态变量i ```
临界区
Critical Section
对共享资源的操作可能出现指令交错的代码块
竟态条件
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
解决方案
- 阻塞式的解决方案:synchronized、Lock
- 非阻塞式的解决方案:原子变量
互斥与同步
- 互斥和同步都可以采用 synchronized 关键字来完成
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
可见性
某线程对变量的修改,对另一线程不可见(读取的仍是修改前的值)
static boolean run = true; public static void mian(String[] args){ new Thread(()-> { while(run){ //... } },"t").start(); sleep(1000); run = false; //虽然修改了,但是t仍不会停下来 }- 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存
- 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
- 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方案
volatile关键字
> volatile 可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存synchronized
> synchronized 是属于重量级操作,性能相对更低System.out.println()
一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况
有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,这种特性称之为『指令重排』
指令重排,是 JIT 编译器在运行时的一些优化
int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; }- 执行结果可能是1 1. 线程1执行完,线程2才开始 2. 线程1执行到if时,线程2执行到num=2
- 执行结果可能是4 3. 线程2执行完,线程1才开始
- 执行结果可能是0 4. 对线程2指令重排,先ready = true时,线程1执行到if
解决方案
volatile 修饰的变量,可以禁用指令重排
```java int num = 0; volatile boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; } ```此例中,只需对ready加volatile,因为会在ready = true后加一个写屏障,保证了写屏障之前的代码不会重排序
指令级并行原理
主频:CPU的时钟频率,计算机的操作在时钟信号的控制下分步执行,每个时钟信号周期完成一步操作,时钟频率的高低在很大程度上反映了CPU速度的快慢
Clock Cycle Time:(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位
例如,运行一条加法指令一般需要一个时钟周期时间
CPI:有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数
IPC:(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数
CPU 执行时间: user + system 时间,可以用下面的公式来表示
程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
现代处理器会设计为 一个时钟周期 完成一条 执行时间最长的 CPU 指令,指令再划分成更小的阶段
例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段

image-20220918190528899 - instruction fetch (IF)
- instruction decode (ID)
- execute (EX)
- memory access (MEM)
- register write back (WB)
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
指令重排的前提是,重排指令不能影响结果
现代 CPU 支持多级指令流水线
例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线
这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1
本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令的吞吐率

image-20220918191052058 SuperScalar 处理器
大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1

image-20220918194048521
CPU缓存结构

速度比较
从CPU到 大约需要的时钟周期 寄存器 1 cycle(4GHz的CPU约为0.25ns) 一级缓存 3~4 cycle 二级缓存 10~20 cycle 三级缓存 40~45 cycle 内存 120~240 cycle 查看cpu缓存行
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64 ========= [高位组标记][低位索引][偏移量]
image-20220918143300262 CPU缓存读
- 根据低位,计算在缓存中的索引
- 判断是否有效
- 0 去内存读取新数据更新缓存行
- 1 再对比高位组标记是否一致
- 一致,根据偏移量返回缓存数据
- 不一致,去内存读取新数据更新缓存行
CPU 缓存一致性 ------- MESI 协议
- E、S、M 状态的缓存行都可以满足 CPU 的读请求
- E 状态的缓存行,有写请求,会将状态改为 M,这时并不触发向主存的写
- E 状态的缓存行,必须监听该缓存行的读操作,如果有,要变为 S 状态
- M 状态的缓存行,必须监听该缓存行的读操作,如果有,先将其它缓存(S 状态)中该缓存行变成 I 状态(即的流程),写入主存,自己变为 S 状态
- S 状态的缓存行,有写请求,走 4. 的流程
- S 状态的缓存行,必须监听该缓存行的失效操作,如果有,自己变为 I 状态
- I 状态的缓存行,有读请求,必须从主存读取
内存屏障
- Memory Barrier(Memory Fence)
- 可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
volatile
volatile 的底层实现原理是内存屏障
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
可见性与有序性
public void actor2(I_Result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障,保证在该屏障之前的,对共享变量的改动,都同步到主存当中,确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 } public void actor1(I_Result r) { // 读屏障,保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据,确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 // ready 是 volatile ,读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } }更底层是 读写变量时,使用 lock 指令来多核 CPU 之间的可见性与有序性
不能解决原子性,只适用于一写多读,多写出现指令交错
volatile的应用
在单例模式中的应用
双检锁double-checked locking
```java public final class Singleton{ private Singleton(){} private static Singleton INSTANCE = null; //有 public static Singleton getInstance(){ if(INSTANCE == null){ synchronized(Singleton.class){ if(INSTANCE == null){ INSTANCE = new Singleton(); } } } return INSTANCE; } } ```getInstance () 对应的字节码
```java 0: getstatic #2 // Field INSTANCE:Singleton;获取INSTANCE值 3: ifnonnull 37 // if null 6: ldc #3 // class Singleton 常量INSTANCE入栈 8: dup // 复制一份引用地址 9: astore_0 // 存入 局部变量表 索引0 10: monitorenter // 上锁 11: getstatic #2 // Field INSTANCE:Singleton;获取INSTANCE值 14: ifnonnull 27 // if null 17: new #3 // class Singleton 创建对象,将对象引用入栈 20: dup // 复制一份对象引用 21: invokespecial #4 // Method "<init>":()V 调用构造方法 24: putstatic #2 // Field INSTANCE:Singleton; 赋值 27: aload_0 // 取出 局部变量表 索引0 的放到 操作数栈 28: monitorexit // 解锁 29: goto 37 // 下面是出现异常解锁 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Singleton; 40: areturn ```21和24,调用构造方法和赋值,非原子性,由synchronized解决,但是有序性得不到解决
> * 根源在于if(INSTANCE = null) > > * 0: getstatic #2 // Field INSTANCE\:Singleton;获取INSTANCE值 > > * 3: ifnonnull 37 // if null > > * t1线程调用getlnstance()并顺利进入,进行了 > > *t1 ---------->17(new)* > > *t1 ---------->20(复制引用)* > > *t1 ---------->21(调用构造器)* > > 此时t2突然调用getInstance() > > **t2 ---------->0**(getstatic 获取Instance的值) > > t1才执行 > > *t1 ---------->24(putstatic,赋值)* > > * t2获得到的引用仍
volatile的读写屏障可以保证读取之前完成所有的写操作
```java public final class Singleton{ private Singleton(){} private static volatile Singleton INSTANCE = null; //加volatile关键字 private static Singleton getInstance(){ if(INSTANCE == null){ synchronized(Singleton.class){ if(INSTANCE == null){ INSTANCE = new Singleton(); } } } return INSTANCE; } } ``` ```java //字节码上看不出来 volatile 指令的效果 // -------------------------------------> 加入对 INSTANCE 变量的读屏障 0: getstatic #2 // Field INSTANCE:Singleton;获取INSTANCE值 3: ifnonnull 37 // if null 6: ldc #3 // class Singleton 常量INSTANCE入栈 8: dup // 复制一份引用地址 9: astore_0 // 存入 局部变量表 索引0 10: monitorenter // 上锁 -----------------------> 保证原子性、可见性 11: getstatic #2 // Field INSTANCE:Singleton;获取INSTANCE值 14: ifnonnull 27 // if null 17: new #3 // class Singleton 创建对象,将对象引用入栈 20: dup // 复制一份对象引用 21: invokespecial #4 // Method "<init>":()V 调用构造方法 24: putstatic #2 // Field INSTANCE:Singleton; 赋值 // -------------------------------------> 加入对 INSTANCE 变量的写屏障 27: aload_0 // 取出 局部变量表 索引0 的放到 操作数栈 28: monitorexit // 解锁 ------------------------> 保证原子性、可见性 ```
happens-before
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
情况一:synchronized读可见
```java static int x; static Object m = new Object(); new Thread(()->{ synchronized(m) { x = 10; //写 } },"t1").start(); new Thread(()->{ synchronized(m) { System.out.println(x); //读 } },"t2").start(); ```情况二:volatile读可见
```java volatile static int x; new Thread(()->{ x = 10; //写 },"t1").start(); new Thread(()->{ System.out.println(x); //读 },"t2").start(); ```情况三:线程开始前写,读可见
```java static int x; x = 10; new Thread(()->{ System.out.println(x); },"t2").start(); ```情况四:有线程结束通知,读可见
```java static int x; Thread t1 = new Thread(()->{ x = 10; },"t1"); t1.start(); t1.join(); //会通知主线程t1线程已结束 //t1.isAlive(); //会通知主线程t1线程已结束 System.out.println(x); ```情况五:打断前的写,被打断的读可见
```java static int x; public static void main(String[] args) { Thread t2 = new Thread(()->{ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println(x); break; } } },"t2"); t2.start(); new Thread(()->{ sleep(1); x = 10; t2.interrupt(); },"t1").start(); while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); } ```情况六:对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
情况七:具有传递性,写屏障之前的操作都读可见
```java volatile static int x; static int y; new Thread(()->{ y = 10; x = 20; },"t1").start(); new Thread(()->{ System.out.println(x); System.out.println(y); //y也可见 },"t2").start(); ```
balking模式的错误应用
volatile只能保证可见性、有序性
init()包含了对共享资源initialized的读写操作,多线程调用,多读多写,必然需要加锁
public class TestVolatile { volatile boolean initialized = false; void init() { if (initialized) { return; } doInit(); //能否保证doInit()只被执行一次? initialized = true; } private void doInit() { } }
final
设置
public class TestFinal { final int a = 20; }字节码
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 20 7: putfield #2 // Field a:I <-- 写屏障 10: return读取
```javapublic class FinalTest { final static int A = 10; final static int B = Short.MAX_VALUE + 1; final int a = 20; final int b = Integer.MAX_VALUE; int x = 30; int y = Integer.MAX_VALUE; } class UseFinal{ public void test(){ System.out.println(FinalTest.A); // 1 System.out.println(FinalTest.B); // 2 System.out.println(new FinalTest().a); // 3 System.out.println(new FinalTest().b); // 4 System.out.println(new FinalTest().b); // 5 System.out.println(new FinalTest().b); // 6 } } ``` 栈中读取(bipish)还是常量池获取(ldc)都比getstatic效率高 ```java // 1 final static int A = 10; BIPUSH 10 //整数入栈 //去掉final变成 GETSTATIC com/botuer/thread/FinalTest.A : I // 2 final static int B = Short.MAX_VALUE + 1; LDC 32768 //常量池中的项入栈 ///去掉final变成 GETSTATIC com/botuer/thread/FinalTest.B : I // 3 final int a = 20; NEW com/botuer/thread/FinalTest //new DUP //复制栈顶 INVOKESPECIAL com/botuer/thread/FinalTest.<init> ()V //加载FinalTest类 INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; POP //弹出栈顶 BIPUSH 20 //整数入栈 // 4 final int b = Integer.MAX_VALUE; NEW com/botuer/thread/FinalTest DUP INVOKESPECIAL com/botuer/thread/FinalTest.<init> ()V INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; POP LDC 2147483647 //5 int x = 30; NEW com/botuer/thread/FinalTest DUP INVOKESPECIAL com/botuer/thread/FinalTest.<init> ()V GETFIELD com/botuer/thread/FinalTest.x : I //从对象中获取字段 //6 int y = Integer.MAX_VALUE; NEW com/botuer/thread/FinalTest DUP INVOKESPECIAL com/botuer/thread/FinalTest.<init> ()V GETFIELD com/botuer/thread/FinalTest.y : I //从对象中获取字段 ```
synchronized
Monitor
称为监视器、管程
刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的 Owner 置为 Thread-2,Monitor中只能有一个 Owner
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList进入BLOCKED状态
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足(wait 等),进入 WAITING 状态

image-20220914153645525
cxq(竞争列表):是一个单向链表(栈)。被挂起线程等待重新竞争锁的链表, monitor 通过CAS将包装成ObjectWaiter写入到列表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素
- 在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程
EntryList(锁候选者列表):是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程
WatiList是Owner线程地调用wait()方法后进入的线程。进入WaitList中的线程在notify()/notifyAll()调用后会被加入到EntryList
Owner当前锁持有者
OnDeckThread:可进行锁竞争的线程。若一个线程被设置为OnDeck,则表明其可以进行tryLock操作,若获取锁成功,则变为Owner,否则仍将其回插到EntryList头部
- OnDeckThread竞争锁失败的原因:cxq中的线程可以进行自旋竞争锁,OnDeckThread若碰上自旋线程就需要和他们竞争
recursions(重入计数器):用来表示某个线程进入该锁的次数
image-20220914225256506 .notify()方法其实就是移动waitset中的线程要么到cxq要么到entrylist中,'当同步方法结束的时候会触发唤醒机制,根据Qmode不同类型进行不同的规则唤醒' notif() 根据Policy 做不同的操作 Policy==0 :放入到entrylist队列的排头位置 Policy==1 :放入到entrylist队列的末尾位置 Policy==2 :判断entrylist是否为空,为空就放入entrylist中,否则放入cxq队列排头位置(默认策略) Policy==3 :判断cxq是否为空,如果为空,直接放入头部,否则放入cxq队列末尾位置 当同步代码块执行完毕,唤醒是什么样子的呢?根据Qmode进行选择 根据QMode策略唤醒: QMode=2,取cxq头部节点直接唤醒 QMode=3,如果cxq非空,把cxq队列放置到entrylist的尾部(顺序跟cxq一致) QMode=4,如果cxq非空,把cxq队列放置到entrylist的头部(顺序跟cxq相反) QMode=0,啥都不做,继续往下走(QMode默认是0)默认是0 Qmode=0的判断逻辑就是先判断entrylist是否为空,如果不为空,则取出第一个唤醒,如果为空再从cxq里面获取第一个唤醒
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储 Monitor 对象
_owner = NULL; // 持有当前线程的 owner
_WaitSet = NULL; // 处于wait状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表:进入EntryList之前,先在cxq,还未阻塞
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
字节码
- 同步方法的synchronized不会在字节码中体现
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // Field counter:I
9: iconst_1 // 准备常数 1
10: iadd // 加法
11: putstatic #3 // Field counter:I
14: aload_1 // slot 1 -> lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24 // 异常处理
19: astore_2
20: aload_1
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
对象头
对象头在堆中,32位JVM中占64字节(数组是96 bit),下面以64位为例
64位的jvm中,占128 bit,mark word占64 bit
|--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------|array length(32/64bits) (数组对象特有)
Klass Word (32/64 bits) --- 类型指针
Mark Word (32/64 bit) --- 运行时元数据,有5种形式
> 32位/64位主要区别在hashcode、threadId、cms\_free <img src="http://minio.botuer.cn/study-node/old/image-20220914191123410.png" alt="image-20220914191123410" />normal --- 正常状态无锁
> * hashcode:调用的是底层native方法,不再随成员变量变化,采用延时加载,用到对象的hashcode再加载,biased无hashcode的存放位置,故先跳到轻量级锁,凡是做过类似hashmap.put(k,v)操作且没覆写hashcode的k对象, 以后加锁时,都会直接略过偏向锁 > > * age分代年龄:4个bit,最大值为15,设置参数不能超过15 > > * cms\_free:CMS收集器用到,32位JVM没有 > > * 持有的锁的标识(normal和biased对应01):无锁就是未进入synchronized,**无锁但是标记为01,指的是可偏向状态** > > * 偏向锁状态(0无1有)biased --- 偏向锁
> * 线程ID:进入synchronized的线程ID > > * epoch:偏向时间戳,**jvm可以知道这个对象的偏向锁是否过期了,过期的情况下允许直接试图抢占,而不进行撤销偏向锁的操作**lightweight locked --- 轻量级锁
heavyweight locked --- 重量级锁
marked for gc --- 被GC标记,即将回收
轻量级锁
使用场景:加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

image-20220914192900028 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录,如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁

image-20220914192931276 如果 cas 失败,有两种情况
锁膨胀:如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
锁重入:如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀(升级为重量级锁)或已经升级为重量级锁,进入重量级锁解锁流程
重量级锁
锁膨胀:在尝试加轻量级锁的过程中,CAS 操作无法成功,将轻量级锁变为重量级锁的过程
由于对象头的mark word 变成......00,说明有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

image-20220914193148465 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败,进入重量级解锁流程
即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋:重量级锁竞争的时候,在放到EntryList之前,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
- 自旋达到10次,升级为重量级锁,进入EntryList
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能(故,自旋次数的上限不再是10次)
- Java 7 之后不能控制是否开启自旋功能
升级为重量级锁的条件
- 轻量级锁 ----> 重量级锁:自旋超过10次 或者 达到自适应自旋上限次数
- 无锁 / 偏向锁 ----> 重量级锁:调用了object.wait() / t.join(),则会直接升级为重量级锁
markword去哪了
- 轻量级锁中,被换到了lockRecord中
- 重量级锁中,被存入了objectMonitor对象的header字段中了
偏向锁
解决锁重入时多次CAS:轻量级锁在没有竞争时(就自己这个线程),每次锁重入仍然需要执行 CAS 操作
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向锁如何避免冲突和竞争
- j会通过CAS来设置偏向线程id,一旦设置成功那么这个偏向锁就算挂上了
- 后面每次访问时,检查线程id一致,就直接进入同步代码块执行了
如果开启了偏向锁(默认开启),那么对象创建后,markword 值最后 3 位为 101,这时它的thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
如果没有开启偏向锁,那么对象创建后,markword 值最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
禁用偏向锁:添加 VM 参数 -XX:-UseBiasedLocking
撤销偏向锁
调用对象 hashCode --- 变成轻量级锁
> 偏向锁的对象 MarkWord 中存储的是线程 id,没有hashcode,调用了hashcode就撤销了偏向锁其它线程使用对象 --- 其他线程变成轻量级锁
调用 wait/notify --- 变成重量级锁
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的ThreadID
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
> 1-19次时,线程2每次都把锁改成轻量级,20次以后,直接偏向线程2
批量撤销
- 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向
- 于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
wait-notify
wait() //让进入 object 监视器的线程到 waitSet 等待
notify() //在 object 上正在 waitSet 等待的线程中挑一个唤醒
notifyAll() //让 object 上正在 waitSet 等待的线程全部唤醒
必须获得锁后才能调用
sleep(n)与wait(n)
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
- 状态都是 TIMED_WAITING
问题分析
问题1:线程阻塞
- sleep导致其他线程阻塞
- 用wait-notify
问题2:虚假唤醒
- 其他线程可能也在等待自己的资源,notify是随机唤醒,出现虚假唤醒
- 用notifyAll
问题3:一次机会
- 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会
- 用 while + wait
//线程一 new Thread(() -> { synchronized (room) { while (!hasResource) { log.debug("没资源,阻塞!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } if (hasResource) { log.debug("可以开始干活了"); } } },"线程一").strat(); //线程二:略 //线程三 new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("资源到了噢!"); room.notifyAll(); } }, "资源线程").start();
原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
join
运用了【保护性暂停】模式:GuardedObject超时实现
源码:
public final synchronized void join(long millis)throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
//调用者轮询检查线程 alive 状态
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
park-unpark
- 每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex 打个比喻
- 线程就像旅人
- Parker 就像他随身携带的背包, _counter是包中资源(0为耗尽,1为充足),默认是0
- _cond 就像帐篷,阻塞队列,
- _mutex互斥锁,进入阻塞队列,用互斥锁锁住
- park时, _counter为0,进入阻塞
- unpark时,将_counter设为1,不再阻塞
- unpark无论调多少次,相当于一次
- unpark无论在park前或后,都可唤醒
- 与 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
源码:
class Parker : public os::PlatformParker {
private:
volatile int _counter ; //是否阻塞的标识
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ; //互斥锁,_counter == 0,线程获得互斥锁,进入阻塞
pthread_cond_t _cond [1] ; //条件变量,阻塞
...
}
void Parker::park(bool isAbsolute, jlong time) {
//判断信号量counter是否大于0,如果大于设为0返回
if (Atomic::xchg(0, &_counter) > 0) return;
//获取当前线程
Thread* thread = Thread::current();
assert(thread->is_Java_thread(), "Must be JavaThread");
JavaThread *jt = (JavaThread *)thread;
//如果中途已经是interrupt了,那么立刻返回,不阻塞
// Check interrupt before trying to wait
if (Thread::is_interrupted(thread, false)) {
return;
}
//记录当前绝对时间戳
// Next, demultiplex/decode time arguments
timespec absTime;
//如果park的超时时间已到,则返回
if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
return;
}
//更换时间戳
if (time > 0) {
unpackTime(&absTime, isAbsolute, time);
}
// Enter safepoint region
// Beware of deadlocks such as 6317397.
// The per-thread Parker:: mutex is a classic leaf-lock.
// In particular a thread must never block on the Threads_lock while
// holding the Parker:: mutex. If safepoints are pending both the
// the ThreadBlockInVM() CTOR and DTOR may grab Threads_lock.
// 进入安全点,利用该thread构造一个ThreadBlockInVM
ThreadBlockInVM tbivm(jt);
// Don't wait if cannot get lock since interference arises from
// unblocking. Also. check interrupt before trying wait
if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
return;
}
//记录等待状态
int status ;
//中途再次检查许可,有则直接返回不等带。
if (_counter > 0) { // no wait needed
_counter = 0;
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
// Paranoia to ensure our locked and lock-free paths interact
// correctly with each other and Java-level accesses.
OrderAccess::fence();
return;
}
OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition() or java_suspend_self()
assert(_cur_index == -1, "invariant");
if (time == 0) {
_cur_index = REL_INDEX; // arbitrary choice when not timed
//线程条件等待 线程等待信号触发,如果没有信号触发,无限期等待下去。
status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
} else {
_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
//线程等待一定的时间,如果超时或有信号触发,线程唤醒
status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
if (status != 0 && WorkAroundNPTLTimedWaitHang) {
pthread_cond_destroy (&_cond[_cur_index]) ;
pthread_cond_init (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
}
}
_cur_index = -1;
assert_status(status == 0 || status == EINTR ||
status == ETIME || status == ETIMEDOUT,
status, "cond_timedwait");
_counter = 0 ;
status = pthread_mutex_unlock(_mutex) ;
assert_status(status == 0, status, "invariant") ;
// Paranoia to ensure our locked and lock-free paths interact
// correctly with each other and Java-level accesses.
OrderAccess::fence();
// If externally suspended while waiting, re-suspend
if (jt->handle_special_suspend_equivalent_condition()) {
jt->java_suspend_self();
}
}
void Parker::unpark() {
//定义两个变量,staus用于判断是否获取锁
int s, status ;
//获取锁
status = pthread_mutex_lock(_mutex);
//判断是否成功
assert (status == 0, "invariant") ;
//存储原先变量_counter
s = _counter;
//把_counter设为1
_counter = 1;
if (s < 1) {
// thread might be parked
if (_cur_index != -1) {
// thread is definitely parked
if (WorkAroundNPTLTimedWaitHang) {
status = pthread_cond_signal (&_cond[_cur_index]);
assert (status == 0, "invariant");
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant");
} else {
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant");
status = pthread_cond_signal (&_cond[_cur_index]);
assert (status == 0, "invariant");
}
} else {
//释放锁
pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
}
} else {
//释放锁
pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
}
}
LongAdder
在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能
缓存行的伪共享
缓存行:缓存以缓存行为单位,每个缓存行对应一块内存,一般是64字节
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

image-20220919171359226 因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了,Core-0 要修改 Cell[0],Core-1 要修改 Cell[1],无论谁修改成功,都会导致对方 Core 的缓存行失效,@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding"填充",从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
父类(抽象类)中 -- Striped64
longAccumulate
// 累加单元数组, 懒惰初始化 transient volatile Cell[] cells; // 基础值, 如果没有竞争, 则用 cas 累加这个域 transient volatile long base; // 在 cells 创建或扩容时, 置为 1, 表示加锁 transient volatile int cellsBusy; //内部类Cell --- 累加单元 @sun.misc.Contended //防止缓存行伪共享 static final class Cell { volatile long value; Cell(long x) { value = x; } //用CAS累加 final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); } } final boolean casBase(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, BASE, cmp, val); } final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; //当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell if ((h = getProbe()) == 0) { ThreadLocalRandom.current(); // force initialization"强制初始化指针" h = getProbe(); // h 对应新的 probe 值, 用来对应 cell wasUncontended = true; } // collide 为 true 表示需要扩容, boolean collide = false; // True if last slot nonempty for (;;) { Cell[] as; Cell a; int n; long v; // 已经有了 cells if ((as = cells) != null && (n = as.length) > 0) { // 但还没有 cell if ((a = as[(n - 1) & h]) == null) { // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x // 成功则 break, 否则继续 continue 循环 if (cellsBusy == 0) { // Try to attach new Cell Cell r = new Cell(x); // Optimistically create if (cellsBusy == 0 && casCellsBusy()) { boolean created = false; try { // Recheck under lock Cell[] rs; int m, j; if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { rs[j] = r; created = true; } } finally { cellsBusy = 0; } if (created) break; continue; // Slot is now non-empty } } collide = false; } // 有竞争, 改变线程对应的 cell 来重试 cas else if (!wasUncontended) // CAS already known to fail wasUncontended = true; // Continue after rehash // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas else if (n >= NCPU || cells != as) collide = false; // At max size or stale // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了 else if (!collide) collide = true; // 加锁 else if (cellsBusy == 0 && casCellsBusy()) { // 加锁成功, 扩容 try { if (cells == as) { // Expand table unless stale Cell[] rs = new Cell[n << 1]; for (int i = 0; i < n; ++i) rs[i] = as[i]; cells = rs; } } finally { cellsBusy = 0; } collide = false; continue; // Retry with expanded table } // 改变线程对应的 cell h = advanceProbe(h); } // 还没有 cells, 尝试给 cellsBusy 加锁 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell // 成功则 break; boolean init = false; try { // Initialize table if (cells == as) { Cell[] rs = new Cell[2]; rs[h & 1] = new Cell(x); cells = rs; init = true; } } finally { cellsBusy = 0; } if (init) break; } // 上两种情况失败, 尝试给 base 累加 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // Fall back on using base } }
image-20220919204617290 
image-20220919204956571 
image-20220919205027568 LongAdder的increment()
add
cells为空 ---- 没有竞争 ---- casBase累加成功 ---- return ①
---- 失败 ---- longAccumulate ③
cells不为空 ---- 当前线程的Cell已经创建 ---- casBase累加成功 ---- return ②
---- 失败 ---- longAccumulate ④
---- 当前线程的Cell还未创建 ---- longAccumulate ⑤
public void increment() { add(1L); } public void add(long x) { Cell[] as; long b, v; int m; Cell a; //1. cells == null ---> casBase == true ---> return //2. cells != null ---> if ---> cell == null ---> longAccumulate //3. cell != null ---> cas == true ---> return //4. cas == false ---> longAccumulate //5. cells == null ---> casBase == false ---> if ---> longAccumulate if ((as = cells) != null || !casBase(b = base, b + x)) { boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || //当前线程指针 和 cells长度-1 进行与运算,判断当前线程的cell是否已经创建 (a = as[getProbe() & m]) == null || //在当前线程累加单元累加 !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); } } public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
image-20220919204643225
线程池
线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作
线程池作用:
- 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死
- 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务
池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销
阻塞队列
阻塞队列:有界队列和无界队列:
有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0
无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出(超过 Integer.MAX_VALUE),所以相当于无界
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:FIFO 队列
- ArrayBlockQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)阻塞队列
- PriorityBlockQueue:支持优先级排序的无界阻塞队列
- DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全:
- 阻塞添加 put():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行
- 阻塞删除 take():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素)
核心方法
方法类型 抛出异常 特殊值 阻塞 超时 插入(尾) add(e) offer(e) put(e) offer(e,time,unit) 移除(头) remove() poll() take() poll(time,unit) 检查(队首元素) element() peek() 不可用 不可用 - 抛出异常组:
- 当阻塞队列满时:在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full
- 当阻塞队列空时:再往队列中 remove 移除元素,会抛出 NoSuchException
- 特殊值组:
- 插入方法:成功 true,失败 false
- 移除方法:成功返回出队列元素,队列没有就返回 null
- 阻塞组:
- 当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到队列有空间 put 数据或响应中断退出
- 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列中有可用元素
- 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出
- 抛出异常组:
LinkedBlockingQueue
- 链表队列
入队出队
LinkedBlockingQueue 源码:
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是尾节点了
*/
Node<E> next;
Node(E x) { item = x; }
}
}
入队:尾插法
初始化链表
last = head = new Node<E>(null),Dummy 节点用来占位,item 为 nullpublic LinkedBlockingQueue(int capacity) { // 默认是 Integer.MAX_VALUE if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }当一个节点入队:
private void enqueue(Node<E> node) { // 从右向左计算 last = last.next = node; }
image-20220923204348194 再来一个节点入队
last = last.next = node
出队:出队头节点,FIFO
出队源码:
private E dequeue() { Node<E> h = head; // 获取临头节点 Node<E> first = h.next; // 自己指向自己,help GC h.next = h; head = first; // 出队的元素 E x = first.item; // 【当前节点置为 Dummy 节点】 first.item = null; return x; }h = head→first = h.next
image-20220923204423486 h.next = h→head = first
image-20220923204444677 first.item = null:当前节点置为 Dummy 节点
加锁分析
用了两把锁和 dummy 节点:
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析:
当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全,两把锁保证了入队和出队没有竞争
当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
// 用于 put(阻塞) offer(非阻塞) private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition(); // 阻塞等待不满,说明已经满了 // 用于 take(阻塞) poll(非阻塞) private final ReentrantLock takeLock = new ReentrantLock(); private final Condition notEmpty = takeLock.newCondition(); // 阻塞等待不空,说明已经是空的
入队出队:
put 操作:
public void put(E e) throws InterruptedException { // 空指针异常 if (e == null) throw new NullPointerException(); int c = -1; // 把待添加的元素封装为 node 节点 Node<E> node = new Node<E>(e); // 获取全局生产锁 final ReentrantLock putLock = this.putLock; // count 用来维护元素计数 final AtomicInteger count = this.count; // 获取可打断锁,会抛出异常 putLock.lockInterruptibly(); try { // 队列满了等待 while (count.get() == capacity) { // 【等待队列不满时,就可以生产数据】,线程处于 Waiting notFull.await(); } // 有空位, 入队且计数加一,尾插法 enqueue(node); // 返回自增前的数字 c = count.getAndIncrement(); // put 完队列还有空位, 唤醒其他生产 put 线程,唤醒一个减少竞争 if (c + 1 < capacity) notFull.signal(); } finally { // 解锁 putLock.unlock(); } // c自增前是0,说明生产了一个元素,唤醒一个 take 线程 if (c == 0) signalNotEmpty(); }private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { // 调用 notEmpty.signal(),而不是 notEmpty.signalAll() 是为了减少竞争,因为只剩下一个元素 notEmpty.signal(); } finally { takeLock.unlock(); } }take 操作:
public E take() throws InterruptedException { E x; int c = -1; // 元素个数 final AtomicInteger count = this.count; // 获取全局消费锁 final ReentrantLock takeLock = this.takeLock; // 可打断锁 takeLock.lockInterruptibly(); try { // 没有元素可以出队 while (count.get() == 0) { // 【阻塞等待队列不空,就可以消费数据】,线程处于 Waiting notEmpty.await(); } // 出队,计数减一,FIFO,出队头节点 x = dequeue(); // 返回自减前的数字 c = count.getAndDecrement(); // 队列还有元素 if (c > 1) // 唤醒一个消费take线程 notEmpty.signal(); } finally { takeLock.unlock(); } // c 是消费前的数据,消费前满了,消费一个后还剩一个空位,唤醒生产线程 if (c == capacity) // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争 signalNotFull(); return x; }
性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较:
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
SynchronousQueue
- 同步队列
成员属性
SynchronousQueue 是一个不存储元素的 BlockingQueue,每一个生产者必须阻塞匹配到一个消费者
成员变量:
运行当前程序的平台拥有 CPU 的数量:
static final int NCPUS = Runtime.getRuntime().availableProcessors()指定超时时间后,当前线程最大自旋次数:
// 只有一个 CPU 时自旋次数为 0,所有程序都是串行执行,多核 CPU 时自旋 32 次是一个经验值 static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;自旋的原因:线程挂起唤醒需要进行上下文切换,涉及到用户态和内核态的转变,是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到,如果自旋期间被匹配到,那么直接就返回了,如果自旋次数达到某个指标后,还是会将当前线程挂起
未指定超时时间,当前线程最大自旋次数:
static final int maxUntimedSpins = maxTimedSpins * 16; // maxTimedSpins 的 16 倍指定超时限制的阈值,小于该值的线程不会被挂起:
static final long spinForTimeoutThreshold = 1000L; // 纳秒超时时间设置的小于该值,就会被禁止挂起,阻塞再唤醒的成本太高,不如选择自旋空转
转换器:
private transient volatile Transferer<E> transferer; abstract static class Transferer<E> { /** * 参数一:可以为 null,null 时表示这个请求是一个 REQUEST 类型的请求,反之是一个 DATA 类型的请求 * 参数二:如果为 true 表示指定了超时时间,如果为 false 表示不支持超时,会一直阻塞到匹配或者被打断 * 参数三:超时时间限制,单位是纳秒 * 返回值:返回值如果不为 null 表示匹配成功,DATA 类型的请求返回当前线程 put 的数据 * 如果返回 null,表示请求超时或被中断 */ abstract E transfer(E e, boolean timed, long nanos); }构造方法:
public SynchronousQueue(boolean fair) { // fair 默认 false // 非公平模式实现的数据结构是栈,公平模式的数据结构是队列 transferer = fair ? new TransferQueue<E>() : new TransferStack<E>(); }成员方法:
public boolean offer(E e) { if (e == null) throw new NullPointerException(); return transferer.transfer(e, true, 0) != null; } public E poll() { return transferer.transfer(null, true, 0); }
非公实现
TransferStack 是非公平的同步队列,因为所有的请求都被压入栈中,栈顶的元素会最先得到匹配,造成栈底的等待线程饥饿
TransferStack 类成员变量:
请求类型:
// 表示 Node 类型为请求类型 static final int REQUEST = 0; // 表示 Node类 型为数据类型 static final int DATA = 1; // 表示 Node 类型为匹配中类型 // 假设栈顶元素为 REQUEST-NODE,当前请求类型为 DATA,入栈会修改类型为 FULFILLING 【栈顶 & 栈顶之下的一个node】 // 假设栈顶元素为 DATA-NODE,当前请求类型为 REQUEST,入栈会修改类型为 FULFILLING 【栈顶 & 栈顶之下的一个node】 static final int FULFILLING = 2;栈顶元素:
volatile SNode head;
内部类 SNode:
成员变量:
static final class SNode { // 指向下一个栈帧 volatile SNode next; // 与当前 node 匹配的节点 volatile SNode match; // 假设当前node对应的线程自旋期间未被匹配成功,那么node对应的线程需要挂起, // 挂起前 waiter 保存对应的线程引用,方便匹配成功后,被唤醒。 volatile Thread waiter; // 数据域,不为空表示当前 Node 对应的请求类型为 DATA 类型,反之则表示 Node 为 REQUEST 类型 Object item; // 表示当前Node的模式 【DATA/REQUEST/FULFILLING】 int mode; }构造方法:
SNode(Object item) { this.item = item; }设置方法:设置 Node 对象的 next 字段,此处对 CAS 进行了优化,提升了 CAS 的效率
boolean casNext(SNode cmp, SNode val) { //【优化:cmp == next】,可以提升一部分性能。 cmp == next 不相等,就没必要走 cas指令。 return cmp == next && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); }匹配方法:
boolean tryMatch(SNode s) { // 当前 node 尚未与任何节点发生过匹配,CAS 设置 match 字段为 s 节点,表示当前 node 已经被匹配 if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) { // 当前 node 如果自旋结束,会 park 阻塞,阻塞前将 node 对应的 Thread 保留到 waiter 字段 // 获取当前 node 对应的阻塞线程 Thread w = waiter; // 条件成立说明 node 对应的 Thread 正在阻塞 if (w != null) { waiter = null; // 使用 unpark 方式唤醒线程 LockSupport.unpark(w); } return true; } // 匹配成功返回 true return match == s; }取消方法:
// 取消节点的方法 void tryCancel() { // match 字段指向自己,表示这个 node 是取消状态,取消状态的 node,最终会被强制移除出栈 UNSAFE.compareAndSwapObject(this, matchOffset, null, this); } boolean isCancelled() { return match == this; }
TransferStack 类成员方法:
snode():填充节点方法
static SNode snode(SNode s, Object e, SNode next, int mode) { // 引用指向空时,snode 方法会创建一个 SNode 对象 if (s == null) s = new SNode(e); // 填充数据 s.mode = mode; s.next = next; return s; }transfer():核心方法,请求匹配出栈,不匹配阻塞
E transfer(E e, boolean timed, long nanos) { // 包装当前线程的 node SNode s = null; // 根据元素判断当前的请求类型 int mode = (e == null) ? REQUEST : DATA; // 自旋 for (;;) { // 获取栈顶指针 SNode h = head; // 【CASE1】:当前栈为空或者栈顶 node 模式与当前请求模式一致无法匹配,做入栈操作 if (h == null || h.mode == mode) { // 当前请求是支持超时的,但是 nanos <= 0 说明这个请求不支持 “阻塞等待” if (timed && nanos <= 0) { // 栈顶元素是取消状态 if (h != null && h.isCancelled()) // 栈顶出栈,设置新的栈顶 casHead(h, h.next); else // 表示【匹配失败】 return null; // 入栈 } else if (casHead(h, s = snode(s, e, h, mode))) { // 等待被匹配的逻辑,正常情况返回匹配的节点;取消情况返回当前节点,就是 s SNode m = awaitFulfill(s, timed, nanos); // 说明当前 node 是【取消状态】 if (m == s) { // 将取消节点出栈 clean(s); return null; } // 执行到这说明【匹配成功】了 // 栈顶有节点并且 匹配节点还未出栈,需要协助出栈 if ((h = head) != null && h.next == s) casHead(h, s.next); // 当前 node 模式为 REQUEST 类型,返回匹配节点的 m.item 数据域 // 当前 node 模式为 DATA 类型:返回 node.item 数据域,当前请求提交的数据 e return (E) ((mode == REQUEST) ? m.item : s.item); } // 【CASE2】:逻辑到这说明请求模式不一致,如果栈顶不是 FULFILLING 说明没被其他节点匹配,【当前可以匹配】 } else if (!isFulfilling(h.mode)) { // 头节点是取消节点,match 指向自己,协助出栈 if (h.isCancelled()) casHead(h, h.next); // 入栈当前请求的节点 else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // m 是 s 的匹配的节点 SNode m = s.next; // m 节点在 awaitFulfill 方法中被中断,clean 了自己 if (m == null) { // 清空栈 casHead(s, null); s = null; // 返回到外层自旋中 break; } // 获取匹配节点的下一个节点 SNode mn = m.next; // 尝试匹配,【匹配成功】,则将 fulfilling 和 m 一起出栈,并且唤醒被匹配的节点的线程 if (m.tryMatch(s)) { casHead(s, mn); return (E) ((mode == REQUEST) ? m.item : s.item); } else // 匹配失败,出栈 m s.casNext(m, mn); } } // 【CASE3】:栈顶模式为 FULFILLING 模式,表示【栈顶和栈顶下面的节点正在发生匹配】,当前请求需要做协助工作 } else { // h 表示的是 fulfilling 节点,m 表示 fulfilling 匹配的节点 SNode m = h.next; if (m == null) // 清空栈 casHead(h, null); else { SNode mn = m.next; // m 和 h 匹配,唤醒 m 中的线程 if (m.tryMatch(h)) casHead(h, mn); else h.casNext(m, mn); } } } }awaitFulfill():阻塞当前线程等待被匹配,返回匹配的节点,或者被取消的节点
SNode awaitFulfill(SNode s, boolean timed, long nanos) { // 等待的截止时间 final long deadline = timed ? System.nanoTime() + nanos : 0L; // 当前线程 Thread w = Thread.currentThread(); // 表示当前请求线程在下面的 for(;;) 自旋检查的次数 int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); // 自旋检查逻辑:是否匹配、是否超时、是否被中断 for (;;) { // 当前线程收到中断信号,需要设置 node 状态为取消状态 if (w.isInterrupted()) s.tryCancel(); // 获取与当前 s 匹配的节点 SNode m = s.match; if (m != null) // 可能是正常的匹配的,也可能是取消的 return m; // 执行了超时限制就判断是否超时 if (timed) { nanos = deadline - System.nanoTime(); // 【超时了,取消节点】 if (nanos <= 0L) { s.tryCancel(); continue; } } // 说明当前线程还可以进行自旋检查 if (spins > 0) // 自旋一次 递减 1 spins = shouldSpin(s) ? (spins - 1) : 0; // 说明没有自旋次数了 else if (s.waiter == null) //【把当前 node 对应的 Thread 保存到 node.waiter 字段中,要阻塞了】 s.waiter = w; // 没有超时限制直接阻塞 else if (!timed) LockSupport.park(this); // nanos > 1000 纳秒的情况下,才允许挂起当前线程 else if (nanos > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanos); } }boolean shouldSpin(SNode s) { // 获取栈顶 SNode h = head; // 条件一成立说明当前 s 就是栈顶,允许自旋检查 // 条件二成立说明当前 s 节点自旋检查期间,又来了一个与当前 s 节点匹配的请求,双双出栈后条件会成立 // 条件三成立前提当前 s 不是栈顶元素,并且当前栈顶正在匹配中,这种状态栈顶下面的元素,都允许自旋检查 return (h == s || h == null || isFulfilling(h.mode)); }clear():指定节点出栈
void clean(SNode s) { // 清空数据域和关联线程 s.item = null; s.waiter = null; // 获取取消节点的下一个节点 SNode past = s.next; // 判断后继节点是不是取消节点,是就更新 past if (past != null && past.isCancelled()) past = past.next; SNode p; // 从栈顶开始向下检查,【将栈顶开始向下的 取消状态 的节点全部清理出去】,直到碰到 past 或者不是取消状态为止 while ((p = head) != null && p != past && p.isCancelled()) // 修改的是内存地址对应的值,p 指向该内存地址所以数据一直在变化 casHead(p, p.next); // 说明中间遇到了不是取消状态的节点,继续迭代下去 while (p != null && p != past) { SNode n = p.next; if (n != null && n.isCancelled()) p.casNext(n, n.next); else p = n; } }
公平实现
TransferQueue 是公平的同步队列,采用 FIFO 的队列实现,请求节点与队尾模式不同,需要与队头发生匹配
TransferQueue 类成员变量:
指向队列的 dummy 节点:
transient volatile QNode head;指向队列的尾节点:
transient volatile QNode tail;被清理节点的前驱节点:
transient volatile QNode cleanMe;入队操作是两步完成的,第一步是 t.next = newNode,第二步是 tail = newNode,所以队尾节点出队,是一种非常特殊的情况
TransferQueue 内部类:
QNode:
static final class QNode { // 指向当前节点的下一个节点 volatile QNode next; // 数据域,Node 代表的是 DATA 类型 item 表示数据,否则 Node 代表的 REQUEST 类型,item == null volatile Object item; // 假设当前 node 对应的线程自旋期间未被匹配成功,那么 node 对应的线程需要挂起, // 挂起前 waiter 保存对应的线程引用,方便匹配成功后被唤醒。 volatile Thread waiter; // true 当前 Node 是一个 DATA 类型,false 表示当前 Node 是一个 REQUEST 类型 final boolean isData; // 构建方法 QNode(Object item, boolean isData) { this.item = item; this.isData = isData; } // 尝试取消当前 node,取消状态的 node 的 item 域指向自己 void tryCancel(Object cmp) { UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this); } // 判断当前 node 是否为取消状态 boolean isCancelled() { return item == this; } // 判断当前节点是否 “不在” 队列内,当 next 指向自己时,说明节点已经出队。 boolean isOffList() { return next == this; } }
TransferQueue 类成员方法:
设置头尾节点:
void advanceHead(QNode h, QNode nh) { // 设置头指针指向新的节点, if (h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh)) // 老的头节点出队 h.next = h; } void advanceTail(QNode t, QNode nt) { if (tail == t) // 更新队尾节点为新的队尾 UNSAFE.compareAndSwapObject(this, tailOffset, t, nt); }transfer():核心方法
E transfer(E e, boolean timed, long nanos) { // s 指向当前请求对应的 node QNode s = null; // 是否是 DATA 类型的请求 boolean isData = (e != null); // 自旋 for (;;) { QNode t = tail; QNode h = head; if (t == null || h == null) continue; // head 和 tail 同时指向 dummy 节点,说明是空队列 // 队尾节点与当前请求类型是一致的情况,说明阻塞队列中都无法匹配, if (h == t || t.isData == isData) { // 获取队尾 t 的 next 节点 QNode tn = t.next; // 多线程环境中其他线程可能修改尾节点 if (t != tail) continue; // 已经有线程入队了,更新 tail if (tn != null) { advanceTail(t, tn); continue; } // 允许超时,超时时间小于 0,这种方法不支持阻塞等待 if (timed && nanos <= 0) return null; // 创建 node 的逻辑 if (s == null) s = new QNode(e, isData); // 将 node 添加到队尾 if (!t.casNext(null, s)) continue; // 更新队尾指针 advanceTail(t, s); // 当前节点 等待匹配.... Object x = awaitFulfill(s, e, timed, nanos); // 说明【当前 node 状态为 取消状态】,需要做出队逻辑 if (x == s) { clean(t, s); return null; } // 说明当前 node 仍然在队列内,匹配成功,需要做出队逻辑 if (!s.isOffList()) { // t 是当前 s 节点的前驱节点,判断 t 是不是头节点,是就更新 dummy 节点为 s 节点 advanceHead(t, s); // s 节点已经出队,所以需要把它的 item 域设置为它自己,表示它是个取消状态 if (x != null) s.item = s; s.waiter = null; } return (x != null) ? (E)x : e; // 队尾节点与当前请求节点【互补匹配】 } else { // h.next 节点,【请求节点与队尾模式不同,需要与队头发生匹配】,TransferQueue 是一个【公平模式】 QNode m = h.next; // 并发导致其他线程修改了队尾节点,或者已经把 head.next 匹配走了 if (t != tail || m == null || h != head) continue; // 获取匹配节点的数据域保存到 x Object x = m.item; // 判断是否匹配成功 if (isData == (x != null) || x == m || !m.casItem(x, e)) { advanceHead(h, m); continue; } // 【匹配完成】,将头节点出队,让这个新的头结点成为 dummy 节点 advanceHead(h, m); // 唤醒该匹配节点的线程 LockSupport.unpark(m.waiter); return (x != null) ? (E)x : e; } } }awaitFulfill():阻塞当前线程等待被匹配
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) { // 表示等待截止时间 final long deadline = timed ? System.nanoTime() + nanos : 0L; Thread w = Thread.currentThread(); // 自选检查的次数 int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); for (;;) { // 被打断就取消节点 if (w.isInterrupted()) s.tryCancel(e); // 获取当前 Node 数据域 Object x = s.item; // 当前请求为 DATA 模式时:e 请求带来的数据 // s.item 修改为 this,说明当前 QNode 对应的线程 取消状态 // s.item 修改为 null 表示已经有匹配节点了,并且匹配节点拿走了 item 数据 // 当前请求为 REQUEST 模式时:e == null // s.item 修改为 this,说明当前 QNode 对应的线程 取消状态 // s.item != null 且 item != this 表示当前 REQUEST 类型的 Node 已经匹配到 DATA 了 if (x != e) return x; // 超时检查 if (timed) { nanos = deadline - System.nanoTime(); if (nanos <= 0L) { s.tryCancel(e); continue; } } // 自旋次数减一 if (spins > 0) --spins; // 没有自旋次数了,把当前线程封装进去 waiter else if (s.waiter == null) s.waiter = w; // 阻塞 else if (!timed) LockSupport.park(this); else if (nanos > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanos); } }
ThreadPoolExecutor
使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
状态名 高3位 接收新任务 处理阻塞队列任务 说明 Running 101即-1 y y Shutdown 000即 0 n y 不会接收新任务,但会处理阻塞队列剩余任务 Stop 001即 1 n n 会中断正在执行的任务,并抛弃阻塞队列任务 Tidying 010即 2 任务全执行完毕,活动线程为 0 即将进入终结 Terminated 011即 3 终结状态 从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
> 线程池状态、线程数量用两个变量表示,则需要两次cas操作,效率低 ```java // 高3位:表示当前线程池运行状态,除去高3位之后的低位:表示当前线程池中所拥有的线程数量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 表示在 ctl 中,低 COUNT_BITS 位,是用于存放当前线程数量的位 private static final int COUNT_BITS = Integer.SIZE - 3; // 低 COUNT_BITS 位所能表达的最大数值,000 11111111111111111111 => 5亿多 private static final int CAPACITY = (1 << COUNT_BITS) - 1; ```  ```java // 111 000000000000000000,转换成整数后其实就是一个【负数】 private static final int RUNNING = -1 << COUNT_BITS; // 000 000000000000000000 private static final int SHUTDOWN = 0 << COUNT_BITS; // 001 000000000000000000 private static final int STOP = 1 << COUNT_BITS; // 010 000000000000000000 private static final int TIDYING = 2 << COUNT_BITS; // 011 000000000000000000 private static final int TERMINATED = 3 << COUNT_BITS; ```获取当前线程池运行状态:
```java // ~CAPACITY = ~000 11111111111111111111 = 111 000000000000000000000(取反) // c == ctl = 111 000000000000000000111 // 111 000000000000000000111 // 111 000000000000000000000 // 111 000000000000000000000 获取到了运行状态 private static int runStateOf(int c) { return c & ~CAPACITY; } ```获取当前线程池线程数量:
```java // c = 111 000000000000000000111 // CAPACITY = 000 111111111111111111111 // 000 000000000000000000111 => 7 private static int workerCountOf(int c) { return c & CAPACITY; } ```重置当前线程池状态 ctl:
```java // c 为旧值, ctlOf 返回结果为新值 ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))); // rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们 private static int ctlOf(int rs, int wc) { return rs | wc; } ```比较当前线程池 ctl 所表示的状态:
```java // 比较当前线程池 ctl 所表示的状态,是否小于某个状态 s // 状态对比:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED private static boolean runStateLessThan(int c, int s) { return c < s; } // 比较当前线程池 ctl 所表示的状态,是否大于等于某个状态s private static boolean runStateAtLeast(int c, int s) { return c >= s; } // 小于 SHUTDOWN 的一定是 RUNNING,SHUTDOWN == 0 private static boolean isRunning(int c) { return c < SHUTDOWN; } ```设置线程池 ctl:
```java // 使用 CAS 方式 让 ctl 值 +1 ,成功返回 true, 失败返回 false private boolean compareAndIncrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect + 1); } // 使用 CAS 方式 让 ctl 值 -1 ,成功返回 true, 失败返回 false private boolean compareAndDecrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect - 1); } // 将 ctl 值减一,do while 循环会一直重试,直到成功为止 private void decrementWorkerCount() { do {} while (!compareAndDecrementWorkerCount(ctl.get())); } ```
成员属性
成员变量
线程池中存放 Worker 的容器:线程池没有初始化,直接往池中加线程即可
private final HashSet<Worker> workers = new HashSet<Worker>();线程全局锁:
// 增加减少 worker 或者时修改线程池运行状态需要持有 mainLock private final ReentrantLock mainLock = new ReentrantLock();可重入锁的条件变量:
// 当外部线程调用 awaitTermination() 方法时,会等待当前线程池状态为 Termination 为止 private final Condition termination = mainLock.newCondition()线程池相关参数:
private volatile int corePoolSize; // 核心线程数量 private volatile int maximumPoolSize; // 线程池最大线程数量 private volatile long keepAliveTime; // 空闲线程存活时间 private volatile ThreadFactory threadFactory; // 创建线程时使用的线程工厂,默认是 DefaultThreadFactory private final BlockingQueue<Runnable> workQueue;// 【超过核心线程提交任务就放入 阻塞队列】private volatile RejectedExecutionHandler handler; // 拒绝策略,juc包提供了4中方式 private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();// 默认策略记录线程池相关属性的数值:
private int largestPoolSize; // 记录线程池生命周期内线程数最大值 private long completedTaskCount; // 记录线程池所完成任务总数,当某个 worker 退出时将完成的任务累加到该属性控制核心线程数量内的线程是否可以被回收:
// false(默认)代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 // allowCoreThreadTimeOut(boolean value) 方法可以设置该值 private volatile boolean allowCoreThreadTimeOut;
内部类:
Worker 类:每个 Worker 对象会绑定一个初始任务,启动 Worker 时优先执行,这也是造成线程池不公平的原因。Worker 继承自 AQS,本身具有锁的特性,采用独占锁模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态不能被抢锁
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; // worker 内部封装的工作线程 Runnable firstTask; // worker 第一个执行的任务,普通的 Runnable 实现类或者是 FutureTask volatile long completedTasks; // 记录当前 worker 所完成任务数量 // 构造方法 Worker(Runnable firstTask) { // 设置AQS独占模式为初始化中状态,这个状态不能被抢占锁 setState(-1); // firstTask不为空时,当worker启动后,内部线程会优先执行firstTask,执行完后会到queue中去获取下个任务 this.firstTask = firstTask; // 使用线程工厂创建一个线程,并且【将当前worker指定为Runnable】,所以thread启动时会调用 worker.run() this.thread = getThreadFactory().newThread(this); } // 【不可重入锁】 protected boolean tryAcquire(int unused) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } }public Thread newThread(Runnable r) { // 将当前 worker 指定为 thread 的执行方法,线程调用 start 会调用 r.run() Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; }拒绝策略相关的内部类
成员方法
提交方法
AbstractExecutorService#submit():提交任务,把 Runnable 或 Callable 任务封装成 FutureTask 执行,可以通过方法返回的任务对象,调用 get 阻塞获取任务执行的结果或者异常,源码分析在笔记的 Future 部分
public Future<?> submit(Runnable task) { // 空指针异常 if (task == null) throw new NullPointerException(); // 把 Runnable 封装成未来任务对象,执行结果就是 null,也可以通过参数指定 FutureTask#get 返回数据 RunnableFuture<Void> ftask = newTaskFor(task, null); // 执行方法 execute(ftask); return ftask; } public <T> Future<T> submit(Callable<T> task) { if (task == null) throw new NullPointerException(); // 把 Callable 封装成未来任务对象 RunnableFuture<T> ftask = newTaskFor(task); // 执行方法 execute(ftask); // 返回未来任务对象,用来获取返回值 return ftask; }protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { // Runnable 封装成 FutureTask,【指定返回值】 return new FutureTask<T>(runnable, value); } protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { // Callable 直接封装成 FutureTask return new FutureTask<T>(callable); }execute():执行任务,但是没有返回值,没办法获取任务执行结果,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式
// command 可以是普通的 Runnable 实现类,也可以是 FutureTask,不能是 Callable public void execute(Runnable command) { // 非空判断 if (command == null) throw new NullPointerException(); // 获取 ctl 最新值赋值给 c,ctl 高 3 位表示线程池状态,低位表示当前线程池线程数量。 int c = ctl.get(); // 【1】当前线程数量小于核心线程数,此次提交任务直接创建一个新的 worker,线程池中多了一个新的线程 if (workerCountOf(c) < corePoolSize) { // addWorker 为创建线程的过程,会创建 worker 对象并且将 command 作为 firstTask,优先执行 if (addWorker(command, true)) return; // 执行到这条语句,说明 addWorker 一定是失败的,存在并发现象或者线程池状态被改变,重新获取状态 // SHUTDOWN 状态下也有可能创建成功,前提 firstTask == null 而且当前 queue 不为空(特殊情况) c = ctl.get(); } // 【2】执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败 // 判断当前线程池是否处于running状态,成立就尝试将 task 放入到 workQueue 中 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); // 条件一成立说明线程池状态被外部线程给修改了,可能是执行了 shutdown() 方法,该状态不能接收新提交的任务 // 所以要把刚提交的任务删除,删除成功说明提交之后线程池中的线程还未消费(处理)该任务 if (!isRunning(recheck) && remove(command)) // 任务出队成功,走拒绝策略 reject(command); // 执行到这说明线程池是 running 状态,获取线程池中的线程数量,判断是否是 0 // 【担保机制】,保证线程池在 running 状态下,最起码得有一个线程在工作 else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 【3】offer失败说明queue满了 // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程直接执行 command,【这也是不公平的原因】 // 如果当前线程数量达到 maximumPoolSiz,这里 addWorker 也会失败,走拒绝策略 else if (!addWorker(command, false)) reject(command); }
添加线程
prestartAllCoreThreads():提前预热,创建所有的核心线程
public int prestartAllCoreThreads() { int n = 0; while (addWorker(null, true)) ++n; return n; }addWorker():添加线程到线程池,返回 true 表示创建 Worker 成功,且线程启动。首先判断线程池是否允许添加线程,允许就让线程数量 + 1,然后去创建 Worker 加入线程池
注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助线程池执行队列中的任务
// core == true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize private boolean addWorker(Runnable firstTask, boolean core) { // 自旋【判断当前线程池状态是否允许创建线程】,允许就设置线程数量 + 1 retry: for (;;) { // 获取 ctl 的值 int c = ctl.get(); // 获取当前线程池运行状态 int rs = runStateOf(c); // 判断当前线程池状态【是否允许添加线程】 // 当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完,需要处理完 queue 中的任务 // 【不允许再提交新的 task,所以 firstTask 为空,但是可以继续添加 worker】 if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) return false; for (;;) { // 获取线程池中线程数量 int wc = workerCountOf(c); // 条件一一般不成立,CAPACITY是5亿多,根据 core 判断使用哪个大小限制线程数量,超过了返回 false if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; // 记录线程数量已经加 1,类比于申请到了一块令牌,条件失败说明其他线程修改了数量 if (compareAndIncrementWorkerCount(c)) // 申请成功,跳出了 retry 这个 for 自旋 break retry; // CAS 失败,没有成功的申请到令牌 c = ctl.get(); // 判断当前线程池状态是否发生过变化,被其他线程修改了,可能其他线程调用了 shutdown() 方法 if (runStateOf(c) != rs) // 返回外层循环检查是否能创建线程,在 if 语句中返回 false continue retry; } } //【令牌申请成功,开始创建线程】 // 运行标记,表示创建的 worker 是否已经启动,false未启动 true启动 boolean workerStarted = false; // 添加标记,表示创建的 worker 是否添加到池子中了,默认false未添加,true是添加。 boolean workerAdded = false; Worker w = null; try { // 【创建 Worker,底层通过线程工厂 newThread 方法创建执行线程,指定了首先执行的任务】 w = new Worker(firstTask); // 将新创建的 worker 节点中的线程赋值给 t final Thread t = w.thread; // 这里的判断为了防止 程序员自定义的 ThreadFactory 实现类有 bug,创造不出线程 if (t != null) { final ReentrantLock mainLock = this.mainLock; // 加互斥锁,要添加 worker 了 mainLock.lock(); try { // 获取最新线程池运行状态保存到 rs int rs = runStateOf(ctl.get()); // 判断线程池是否为RUNNING状态,不是再【判断当前是否为SHUTDOWN状态且firstTask为空,特殊情况】 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { // 当线程start后,线程isAlive会返回true,这里还没开始启动线程,如果被启动了就需要报错 if (t.isAlive()) throw new IllegalThreadStateException(); //【将新建的 Worker 添加到线程池中】 workers.add(w); int s = workers.size(); // 当前池中的线程数量是一个新高,更新 largestPoolSize if (s > largestPoolSize) largestPoolSize = s; // 添加标记置为 true workerAdded = true; } } finally { // 解锁啊 mainLock.unlock(); } // 添加成功就【启动线程执行任务】 if (workerAdded) { // Thread 类中持有 Runnable 任务对象,调用的是 Runnable 的 run ,也就是 FutureTask t.start(); // 运行标记置为 true workerStarted = true; } } } finally { // 如果启动线程失败,做清理工作 if (! workerStarted) addWorkerFailed(w); } // 返回新创建的线程是否启动 return workerStarted; }addWorkerFailed():清理任务
private void addWorkerFailed(Worker w) { final ReentrantLock mainLock = this.mainLock; // 持有线程池全局锁,因为操作的是线程池相关的东西 mainLock.lock(); try { //条件成立需要将 worker 在 workers 中清理出去。 if (w != null) workers.remove(w); // 将线程池计数 -1,相当于归还令牌。 decrementWorkerCount(); // 尝试停止线程池 tryTerminate(); } finally { //释放线程池全局锁。 mainLock.unlock(); } }
运行方法
Worker#run:Worker 实现了 Runnable 接口,当线程启动时,会调用 Worker 的 run() 方法
public void run() { // ThreadPoolExecutor#runWorker() runWorker(this); }runWorker():线程启动就要执行任务,会一直 while 循环获取任务并执行
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); // 获取 worker 的 firstTask Runnable task = w.firstTask; // 引用置空,【防止复用该线程时重复执行该任务】 w.firstTask = null; // 初始化 worker 时设置 state = -1,表示不允许抢占锁 // 这里需要设置 state = 0 和 exclusiveOwnerThread = null,开始独占模式抢锁 w.unlock(); // true 表示发生异常退出,false 表示正常退出。 boolean completedAbruptly = true; try { // firstTask 不是 null 就直接运行,否则去 queue 中获取任务 // 【getTask 如果是阻塞获取任务,会一直阻塞在take方法,直到获取任务,不会走返回null的逻辑】 while (task != null || (task = getTask()) != null) { // worker 加锁,shutdown 时会判断当前 worker 状态,【根据独占锁状态判断是否空闲】 w.lock(); // 说明线程池状态大于 STOP,目前处于 STOP/TIDYING/TERMINATION,此时给线程一个中断信号 if ((runStateAtLeast(ctl.get(), STOP) || // 说明线程处于 RUNNING 或者 SHUTDOWN 状态,清除打断标记 (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) // 中断线程,设置线程的中断标志位为 true wt.interrupt(); try { // 钩子方法,【任务执行的前置处理】 beforeExecute(wt, task); Throwable thrown = null; try { // 【执行任务】 task.run(); } catch (Exception x) { //..... } finally { // 钩子方法,【任务执行的后置处理】 afterExecute(task, thrown); } } finally { task = null; // 将局部变量task置为null,代表任务执行完成 w.completedTasks++; // 更新worker完成任务数量 w.unlock(); // 解锁 } } // getTask()方法返回null时会走到这里,表示queue为空并且线程空闲超过保活时间,【当前线程执行退出逻辑】 completedAbruptly = false; } finally { // 正常退出 completedAbruptly = false // 异常退出 completedAbruptly = true,【从 task.run() 内部抛出异常】时,跳到这一行 processWorkerExit(w, completedAbruptly); } }unlock():重置锁
public void unlock() { release(1); } // 外部不会直接调用这个方法 这个方法是 AQS 内调用的,外部调用 unlock 时触发此方法 protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); // 设置持有者为 null setState(0); // 设置 state = 0 return true; }getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是当前线程阻塞获取任务超过保活时间,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑。线程池具有担保机制,对于 RUNNING 状态下的超时回收,要保证线程池中最少有一个线程运行,或者任务阻塞队列已经是空
private Runnable getTask() { // 超时标记,表示当前线程获取任务是否超时,true 表示已超时 boolean timedOut = false; for (;;) { int c = ctl.get(); // 获取线程池当前运行状态 int rs = runStateOf(c); // 【tryTerminate】打断线程后执行到这,此时线程池状态为STOP或者线程池状态为SHUTDOWN并且队列已经是空 // 所以下面的 if 条件一定是成立的,可以直接返回 null,线程就应该退出了 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { // 使用 CAS 自旋的方式让 ctl 值 -1 decrementWorkerCount(); return null; } // 获取线程池中的线程数量 int wc = workerCountOf(c); // 线程没有明确的区分谁是核心或者非核心线程,是根据当前池中的线程数量判断 // timed = false 表示当前这个线程 获取task时不支持超时机制的,当前线程会使用 queue.take() 阻塞获取 // timed = true 表示当前这个线程 获取task时支持超时机制,使用 queue.poll(xxx,xxx) 超时获取 // 条件一代表允许回收核心线程,那就无所谓了,全部线程都执行超时回收 // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程,有保活时间,去超时获取任务 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 如果线程数量是否超过最大线程数,直接回收 // 如果当前线程【允许超时回收并且已经超时了】,就应该被回收了,由于【担保机制】还要做判断: // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 // workQueue.isEmpty() 前置条件是 wc = 1,【如果当前任务队列也是空了,最后一个线程就可以退出】 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { // 使用 CAS 机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,代表可以退出 if (compareAndDecrementWorkerCount(c)) return null; continue; } try { // 根据当前线程是否需要超时回收,【选择从队列获取任务的方法】是超时获取或者阻塞获取 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); // 获取到任务返回任务,【阻塞获取会阻塞到获取任务为止】,不会返回 null if (r != null) return r; // 获取任务为 null 说明超时了,将超时标记设置为 true,下次自旋时返 null timedOut = true; } catch (InterruptedException retry) { // 阻塞线程被打断后超时标记置为 false,【说明被打断不算超时】,要继续获取,直到超时或者获取到任务 // 如果线程池 SHUTDOWN 状态下的打断,会在循环获取任务前判断,返回 null timedOut = false; } } }processWorkerExit():线程退出线程池,也有担保机制,保证队列中的任务被执行
// 正常退出 completedAbruptly = false,异常退出为 true private void processWorkerExit(Worker w, boolean completedAbruptly) { // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了 if (completedAbruptly) // 从异常时到这里 ctl 一直没有 -1,需要在这里 -1 decrementWorkerCount(); final ReentrantLock mainLock = this.mainLock; // 加锁 mainLock.lock(); try { // 将当前 worker 完成的 task 数量,汇总到线程池的 completedTaskCount completedTaskCount += w.completedTasks; // 将 worker 从线程池中移除 workers.remove(w); } finally { mainLock.unlock(); // 解锁 } // 尝试停止线程池,唤醒下一个线程 tryTerminate(); int c = ctl.get(); // 线程池不是停止状态就应该有线程运行【担保机制】 if (runStateLessThan(c, STOP)) { // 正常退出的逻辑,是对空闲线程回收,不是执行出错 if (!completedAbruptly) { // 根据是否回收核心线程确定【线程池中的线程数量最小值】 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务担保机制 if (min == 0 && !workQueue.isEmpty()) min = 1; // 线程池中的线程数量大于最小值可以直接返回 if (workerCountOf(c) >= min) return; } // 执行 task 时发生异常,有个线程因为异常终止了,需要添加 // 或者线程池中的数量小于最小值,这里要创建一个新 worker 加进线程池 addWorker(null, false); } }
停止方法
shutdown():停止线程池
public void shutdown() { final ReentrantLock mainLock = this.mainLock; // 获取线程池全局锁 mainLock.lock(); try { checkShutdownAccess(); // 设置线程池状态为 SHUTDOWN,如果线程池状态大于 SHUTDOWN,就不会设置直接返回 advanceRunState(SHUTDOWN); // 中断空闲线程 interruptIdleWorkers(); // 空方法,子类可以扩展 onShutdown(); } finally { // 释放线程池全局锁 mainLock.unlock(); } tryTerminate(); }interruptIdleWorkers():shutdown 方法会中断所有空闲线程,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕
// onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 private void interruptIdleWorkers(boolean onlyOne) { final ReentrantLock mainLock = this.mainLock; / /持有全局锁 mainLock.lock(); try { // 遍历所有 worker for (Worker w : workers) { // 获取当前 worker 的线程 Thread t = w.thread; // 条件一成立:说明当前迭代的这个线程尚未中断 // 条件二成立:说明【当前worker处于空闲状态】,阻塞在poll或者take,因为worker执行task时是要加锁的 // 每个worker有一个独占锁,w.tryLock()尝试加锁,加锁成功返回 true if (!t.isInterrupted() && w.tryLock()) { try { // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回 null,执行退出相逻辑 t.interrupt(); } catch (SecurityException ignore) { } finally { // 释放worker的独占锁 w.unlock(); } } // false,代表中断所有的线程 if (onlyOne) break; } } finally { // 释放全局锁 mainLock.unlock(); } }shutdownNow():直接关闭线程池,不会等待任务执行完成
public List<Runnable> shutdownNow() { // 返回值引用 List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; // 获取线程池全局锁 mainLock.lock(); try { checkShutdownAccess(); // 设置线程池状态为STOP advanceRunState(STOP); // 中断线程池中【所有线程】 interruptWorkers(); // 从阻塞队列中导出未处理的task tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); // 返回当前任务队列中 未处理的任务。 return tasks; }tryTerminate():设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty)
final void tryTerminate() { for (;;) { // 获取 ctl 的值 int c = ctl.get(); // 线程池正常,或者有其他线程执行了状态转换的方法,当前线程直接返回 if (isRunning(c) || runStateAtLeast(c, TIDYING) || // 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务 (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空 // 判断线程池中线程的数量 if (workerCountOf(c) != 0) { // 【中断一个空闲线程】,在 queue.take() | queue.poll() 阻塞空闲 // 唤醒后的线程会在getTask()方法返回null, // 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程 interruptIdleWorkers(ONLY_ONE); return; } // 池中的线程数量为 0 来到这里 final ReentrantLock mainLock = this.mainLock; // 加全局锁 mainLock.lock(); try { // 设置线程池状态为 TIDYING 状态,线程数量为 0 if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { // 结束线程池 terminated(); } finally { // 设置线程池状态为TERMINATED状态。 ctl.set(ctlOf(TERMINATED, 0)); // 【唤醒所有调用 awaitTermination() 方法的线程】 termination.signalAll(); } return; } } finally { // 释放线程池全局锁 mainLock.unlock(); } } }
Future
线程使用
FutureTask 未来任务对象,继承 Runnable、Future 接口,用于包装 Callable 对象,实现任务的提交
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "Hello World";
}
});
new Thread(task).start(); //启动线程
String msg = task.get(); //获取返回任务数据
System.out.println(msg);
}
构造方法:
public FutureTask(Callable<V> callable){
this.callable = callable; // 属性注入
this.state = NEW; // 任务状态设置为 new
}
public FutureTask(Runnable runnable, V result) {
// 适配器模式
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// 使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取
// 当前任务执行结果时,结果可能为 null 也可能为传进来的值,【传进来什么返回什么】
return new RunnableAdapter<T>(task, result);
}
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
// 构造方法
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
// 实则调用 Runnable#run 方法
task.run();
// 返回值为构造 FutureTask 对象时传入的返回值或者是 null
return result;
}
}
成员属性
FutureTask 类的成员属性:
任务状态:
// 表示当前task状态 private volatile int state; // 当前任务尚未执行 private static final int NEW = 0; // 当前任务正在结束,尚未完全结束,一种临界状态 private static final int COMPLETING = 1; // 当前任务正常结束 private static final int NORMAL = 2; // 当前任务执行过程中发生了异常,内部封装的 callable.run() 向上抛出异常了 private static final int EXCEPTIONAL = 3; // 当前任务被取消 private static final int CANCELLED = 4; // 当前任务中断中 private static final int INTERRUPTING = 5; // 当前任务已中断 private static final int INTERRUPTED = 6;任务对象:
private Callable<V> callable; // Runnable 使用装饰者模式伪装成 Callable存储任务执行的结果,这是 run 方法返回值是 void 也可以获取到执行结果的原因:
// 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值 // 非正常情况:callable 向上抛出异常,outcome 保存异常 private Object outcome;执行当前任务的线程对象:
private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用线程阻塞队列的头节点:
// 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 private volatile WaitNode waiters;内部类:
static final class WaitNode { // 单向链表 volatile Thread thread; volatile WaitNode next; WaitNode() { thread = Thread.currentThread(); } }
成员方法
FutureTask 类的成员方法:
FutureTask#run:任务执行入口
public void run() { //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不需要处理了 //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务,直接返回 if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { // 执行到这里,当前 task 一定是 NEW 状态,而且【当前线程也抢占 task 成功】 Callable<V> c = callable; // 判断任务是否为空,防止空指针异常;判断 state 状态,防止外部线程在此期间 cancel 掉当前任务 // 【因为 task 的执行者已经设置为当前线程,所以这里是线程安全的】 if (c != null && state == NEW) { V result; // true 表示 callable.run 代码块执行成功 未抛出异常 // false 表示 callable.run 代码块执行失败 抛出异常 boolean ran; try { // 【调用自定义的方法,执行结果赋值给 result】 result = c.call(); // 没有出现异常 ran = true; } catch (Throwable ex) { // 出现异常,返回值置空,ran 置为 false result = null; ran = false; // 设置返回的异常 setException(ex); } // 代码块执行正常 if (ran) // 设置返回的结果 set(result); } } finally { // 任务执行完成,取消线程的引用,help GC runner = null; int s = state; // 判断任务是不是被中断 if (s >= INTERRUPTING) // 执行中断处理方法 handlePossibleCancellationInterrupt(s); } }FutureTask#set:设置正常返回值,首先将任务状态设置为 COMPLETING 状态代表完成中,逻辑执行完设置为 NORMAL 状态代表任务正常执行完成,最后唤醒 get() 阻塞线程
protected void set(V v) { // CAS 方式设置当前任务状态为完成中,设置失败说明其他线程取消了该任务 if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { // 【将结果赋值给 outcome】 outcome = v; // 将当前任务状态修改为 NORMAL 正常结束状态。 UNSAFE.putOrderedInt(this, stateOffset, NORMAL); finishCompletion(); } }FutureTask#setException:设置异常返回值
protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { // 赋值给返回结果,用来向上层抛出来的异常 outcome = t; // 将当前任务的状态 修改为 EXCEPTIONAL UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); finishCompletion(); } }FutureTask#finishCompletion:唤醒 get() 阻塞线程
private void finishCompletion() { // 遍历所有的等待的节点,q 指向头节点 for (WaitNode q; (q = waiters) != null;) { // 使用cas设置 waiters 为 null,防止外部线程使用cancel取消当前任务,触发finishCompletion方法重复执行 if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { // 自旋 for (;;) { // 获取当前 WaitNode 节点封装的 thread Thread t = q.thread; // 当前线程不为 null,唤醒当前 get() 等待获取数据的线程 if (t != null) { q.thread = null; LockSupport.unpark(t); } // 获取当前节点的下一个节点 WaitNode next = q.next; // 当前节点是最后一个节点了 if (next == null) break; // 断开链表 q.next = null; // help gc q = next; } break; } } done(); callable = null; // help GC }FutureTask#handlePossibleCancellationInterrupt:任务中断处理
private void handlePossibleCancellationInterrupt(int s) { if (s == INTERRUPTING) // 中断状态中 while (state == INTERRUPTING) // 等待中断完成 Thread.yield(); }FutureTask#get:获取任务执行的返回值,执行 run 和 get 的不是同一个线程,一般有多个线程 get,只有一个线程 run
public V get() throws InterruptedException, ExecutionException { // 获取当前任务状态 int s = state; // 条件成立说明任务还没执行完成 if (s <= COMPLETING) // 返回 task 当前状态,可能当前线程在里面已经睡了一会 s = awaitDone(false, 0L); return report(s); }FutureTask#awaitDone:get 线程封装成 WaitNode 对象进入阻塞队列阻塞等待
private int awaitDone(boolean timed, long nanos) throws InterruptedException { // 0 不带超时 final long deadline = timed ? System.nanoTime() + nanos : 0L; // 引用当前线程,封装成 WaitNode 对象 WaitNode q = null; // 表示当前线程 waitNode 对象,是否进入阻塞队列 boolean queued = false; // 【三次自旋开始休眠】 for (;;) { // 判断当前 get() 线程是否被打断,打断返回 true,清除打断标记 if (Thread.interrupted()) { // 当前线程对应的等待 node 出队, removeWaiter(q); throw new InterruptedException(); } // 获取任务状态 int s = state; // 条件成立说明当前任务执行完成已经有结果了 if (s > COMPLETING) { // 条件成立说明已经为当前线程创建了 WaitNode,置空 help GC if (q != null) q.thread = null; // 返回当前的状态 return s; } // 条件成立说明当前任务接近完成状态,这里让当前线程释放一下 cpu ,等待进行下一次抢占 cpu else if (s == COMPLETING) Thread.yield(); // 【第一次自旋】,当前线程还未创建 WaitNode 对象,此时为当前线程创建 WaitNode对象 else if (q == null) q = new WaitNode(); // 【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 else if (!queued) // waiters 指向队首,让当前 WaitNode 成为新的队首,【头插法】,失败说明其他线程修改了新的队首 queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); // 【第三次自旋】,会到这里,或者 else 内 else if (timed) { nanos = deadline - System.nanoTime(); if (nanos <= 0L) { removeWaiter(q); return state; } // 阻塞指定的时间 LockSupport.parkNanos(this, nanos); } // 条件成立:说明需要阻塞 else // 【当前 get 操作的线程被 park 阻塞】,除非有其它线程将唤醒或者将当前线程中断 LockSupport.park(this); } }FutureTask#report:封装运行结果,可以获取 run() 方法中设置的成员变量 outcome,这是 run 方法的返回值是 void 也可以获取到任务执行的结果的原因
private V report(int s) throws ExecutionException { // 获取执行结果,是在一个 futuretask 对象中的属性,可以直接获取 Object x = outcome; // 当前任务状态正常结束 if (s == NORMAL) return (V)x; // 直接返回 callable 的逻辑结果 // 当前任务被取消或者中断 if (s >= CANCELLED) throw new CancellationException(); // 抛出异常 // 执行到这里说明自定义的 callable 中的方法有异常,使用 outcome 上层抛出异常 throw new ExecutionException((Throwable)x); }FutureTask#cancel:任务取消,打断正在执行该任务的线程
public boolean cancel(boolean mayInterruptIfRunning) { // 条件一:表示当前任务处于运行中或者处于线程池任务队列中 // 条件二:表示修改状态,成功可以去执行下面逻辑,否则返回 false 表示 cancel 失败 if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) return false; try { // 如果任务已经被执行,是否允许打断 if (mayInterruptIfRunning) { try { // 获取执行当前 FutureTask 的线程 Thread t = runner; if (t != null) // 打断执行的线程 t.interrupt(); } finally { // 设置任务状态为【中断完成】 UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); } } } finally { // 唤醒所有 get() 阻塞的线程 finishCompletion(); } return true; }
自定义线程池
阻塞队列
阻塞队列双向链表queue、锁ReentrantLock、生产者条件变量Condition、消费者条件变量Condition、容量capacity
带拒绝策略的尝试添加tryPut(rejectPolicy)、阻塞获取带超时、不带超时、阻塞添加带超时、不带超时、队列长度获取
/** * 阻塞队列 * @param <T> Runnable 阻塞的任务 */ @Slf4j class BlockingQueue<T> { // 任务队列:双向链表 private Deque<T> queue = new ArrayDeque<>(); // 锁ReentrantLock private ReentrantLock lock = new ReentrantLock(); // 生产者条件变量 private Condition fullWaitSet = lock.newCondition(); // 消费者条件变量 private Condition emptyWaitSet = lock.newCondition(); // 容量 private int capacity; //构造器 public BlockingQueue(int capacity) { this.capacity = capacity; } //尝试添加 public void tryPut(RejectPolicy<T> rejectPolicy, T task) throws InterruptedException { lock.lock(); try { if (queue.size() == capacity) { log.info("拒绝策略"); rejectPolicy.reject(this, task); }else { log.debug("加入任务队列 {}", task); queue.addLast(task); emptyWaitSet.signal(); } } finally { lock.unlock(); } } // 阻塞获取:不带超时 public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { emptyWaitSet.await(); } T t = queue.removeFirst(); fullWaitSet.signal(); log.info("成功在阻塞队列拿到+++++++++++++++"); return t; } finally { lock.unlock(); } } // 阻塞获取:带超时 public T take(long timeout, TimeUnit unit) throws InterruptedException { lock.lock(); try { //超时时间转化为纳秒 long nanos = unit.toNanos(timeout); while (queue.isEmpty()) { if (nanos <= 0) { log.error("超时,没在阻塞队列拿到"); return null; //超时返回null } //返回值为剩余时间,避免虚假唤醒仍以开始超时时间等待 nanos = emptyWaitSet.awaitNanos(nanos); } T t = queue.removeFirst(); fullWaitSet.signal(); log.info("成功在阻塞队列拿到+++++++++++++++"); return t; } finally { lock.unlock(); } } // 阻塞添加:不带超时 public void put(T task) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { fullWaitSet.await(); log.debug("等待加入任务队列 {} ...", task); } log.debug("加入任务队列 {}", task); queue.addLast(task); emptyWaitSet.signal(); } finally { lock.unlock(); } } // 阻塞添加:带超时 public boolean put(T task, long timeout, TimeUnit unit) throws InterruptedException { lock.lock(); try { long nanos = unit.toNanos(timeout); while (queue.size() == capacity) { if (nanos <= 0) { log.error("加入阻塞队列失败-----------"); return false; } log.debug("等待加入任务队列 {} ...", task); nanos = fullWaitSet.awaitNanos(nanos); } log.debug("加入任务队列 {}", task); queue.addLast(task); emptyWaitSet.signal(); return true; } finally { lock.unlock(); } } // 获取队列的size public int getSize() { lock.lock(); try { return queue.size(); } finally { lock.unlock(); } } }线程池
阻塞队列blockingQueue、线程集合workers、核心线程数corePoolSize、超时时间timeout、拒绝策略rejectPolicy
执行任务execute(Runnable task):调用worker.start() / tryPut(rejectPolicy,task)
内部类Worker:继承Thread,完成线程循环利用的逻辑
/** * 线程池 */ @Slf4j class ThreadPool { // 阻塞队列 private BlockingQueue<Runnable> blockingQueue; // 线程集合 private HashSet<Worker> blockingQueue = new HashSet<>(); // 核心线程数 private int corePoolSize; // 超时时间 private long timeout; private TimeUnit unit; // 拒绝策略 private RejectPolicy<Runnable> rejectPolicy; /** * * @param blockingQueueCapacity 阻塞队列容量 * @param corePoolSize 线程池大小 * @param timeout 超时时间 * @param unit * @param rejectPolicy 拒绝策略 */ public ThreadPool(int blockingQueueCapacity,int corePoolSize, long timeout, TimeUnit unit, RejectPolicy<Runnable> rejectPolicy) { this.corePoolSize = corePoolSize; this.timeout = timeout; this.unit = unit; this.rejectPolicy = rejectPolicy; this.blockingQueue = new BlockingQueue<>(blockingQueueCapacity); } // 执行任务 public void execute(Runnable task) throws InterruptedException { // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行 // 如果任务数超过 coreSize 时,加入任务队列暂存 if (workers.size() < corePoolSize) { Worker worker = new Worker(task); log.debug("新增 worker{}, {}", worker, task); workers.add(worker); worker.start(); }else { log.debug("满了,存入阻塞队列"); blockingQueue.tryPut(rejectPolicy,task); } } // 内部类 class Worker extends Thread { private Runnable task; public Worker(Runnable task) { this.task = task; } @SneakyThrows @Override public void run() { // 达到corePoolSize,存入阻塞队列(tryPut),阻塞队列满了,不让加入(拒绝策略,死等), // 小于线程池大小,直接进入run,每个worker进入循环,各一次task != null,直接执行; // 后面开始从阻塞队列take任务,(task = blockingQueue.take(timeout,unit)) != null,执行 // 直到两个worker拿不到task,全部任务结束,线程集合把worker移除 while (task != null || (task = blockingQueue.take(timeout,unit)) != null){ try { log.debug("正在执行...{}", task); task.run(); } finally { task = null; } } synchronized (workers){ workers.remove(this); log.debug("worker 被移除{}", this); } } } }拒绝策略
实现:死等、超时等、放弃、自己执行、抛异常
/** * 拒绝策略 * @param <T> Runnable 拒绝任务 */ @FunctionalInterface interface RejectPolicy<T> { void reject(BlockingQueue<T> queue, T task) throws InterruptedException; }测试
@Slf4j public class TestThreadPool { public static void main(String[] args) throws InterruptedException { ThreadPool threadPool = new ThreadPool(5, 2, 1000000, TimeUnit.MILLISECONDS, (blockingQueue, task) -> { // blockingQueue.put(task); //死等 // blockingQueue.put(task,1000,TimeUnit.MILLISECONDS); //超时等 // return; //放弃 // throw new RuntimeException("满了满了!!"); //抛异常 // task.run(); //自己执行 }); for (int i = 0; i < 10; i++) { int j = i; threadPool.execute(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("{}",j); }); } } }
tomcat中的线程池
- LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
- Acceptor 只负责【接收新的 socket 连接】
- Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

如果总线程数达到 maximumPoolSize
- 这时不会立刻抛 RejectedExecutionException 异常
- 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
源码
public void execute(Runnable command, long timeout, TimeUnit unit) { submittedCount.incrementAndGet(); try { super.execute(command); } catch (RejectedExecutionException rx) { if (super.getQueue() instanceof TaskQueue) { final TaskQueue queue = (TaskQueue) super.getQueue(); try { if (!queue.force(command, timeout, unit)) { submittedCount.decrementAndGet(); throw new RejectedExecutionException("Queue capacity is full."); } } catch (InterruptedException x) { submittedCount.decrementAndGet(); Thread.interrupted(); throw new RejectedExecutionException(x); } } else { submittedCount.decrementAndGet(); throw rx; } } }TaskQueue.java
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException { if ( parent.isShutdown() ) throw new RejectedExecutionException( "Executor not running, can't force a command into the queue" ); return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected }Connector 配置
配置项 默认值 说明 acceptorThreadCount 1 acceptor 线程数量 pollerThreadCount 1 poller 线程数量 minSpareThreads 10 核心线程数,即 corePoolSize maxThreads 200 最大线程数,即 maximumPoolSize executor Executor 名称,用来引用下面的 Executor Executor 线程配置
配置项 默认值 说明 threadPriority 5 线程优先级 daemon true 是否守护线程 minSpareThreads 25 核心线程数,即 corePoolSize maxThreads 200 最大线程数,即 maximumPoolSize maxIdleTime 60000 线程生存时间,单位是毫秒,默认值即 1 分钟 maxQueueSize Integer.MAX_VALUE 队列长度 prestartminSpareThreads false 核心线程是否在服务器启动时启动
AQS
AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器
特点
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively
// 获取锁的姿势 如果获取锁失败 if (!tryAcquire(arg)) { // 入队, 可以选择阻塞当前线程 park unpark } // 释放锁的姿势 // 如果释放锁成功 if (tryRelease(arg)) { // 让阻塞线程恢复运行 }
实现不可重入锁
class MyLock implements Lock {
//自定义同步器 独占锁 不可重入
final class MySync extends AbstractQueuedSynchronizer {
@Override //arg是可重入锁的计数变量
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
// 加上锁 设置 owner 为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override //解锁
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);//volatile 修饰的变量放在后面,防止指令重排
return true;
}
@Override //是否持有独占锁
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition() {
return new ConditionObject();
}
}
private MySync sync = new MySync();
@Override //加锁(不成功进入等待队列等待)
public void lock() {
sync.acquire(1);
}
@Override //加锁 可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override //尝试加锁,尝试一次
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override //尝试加锁,带超时
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override //解锁
public void unlock() {
sync.release(1);
}
@Override //条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
// lock.lock(); //不可重入
try {
log.debug("locking...");
sleep(1); //阻塞,执行完第二个线程才能得到锁
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t2").start();
ReentrantLock

非公平锁
先从构造器开始看,默认为非公平锁实现 ---- NonfairSync 继承自 AQS
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
加锁源码
public void lock() {
sync.lock(); //调用同步器(AQS的子类)的lock方法
}
没有竞争时,将state改为1,exclusiveOwnerThread改成当前线程

出现竞争时,cas改state为1失败,进入acquire
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//第一次尝试获得锁,失败后进行acquire
final void lock() {
if (compareAndSetState(0, 1)) //尝试把state改为1,成了Owner改成自己独占
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //没改成,开始进行acquire
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
acquire 逻辑
//通过至少调用一次 tryAcquire 实现,成功后返回。否则线程将排队,可能会反复阻塞和解除阻塞,调用 tryAcquire 直到成功
public final void acquire(int arg) {
//tryAcquire 尝试获取锁失败时, 会调用 addWaiter 将当前线程封装成node入队,acquireQueued 阻塞当前线程,
// acquireQueued 返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果线程被中断了逻辑来到这,完成一次真正的打断效果
selfInterrupt();
}
acquire --- tryAcquire 逻辑,失败,进行addWaiter 逻辑,构造 Node 队列
// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
// 抢占成功返回 true,抢占失败返回 false
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前state值
int c = getState();
//state = 0,无锁,赶紧抢
if (c == 0) {
//cas改state抢到了,设置owner,返回
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//有锁,owner还是自己,说明进行了重入,改state累加,返回
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//int溢出,发生到负数的突变,抛异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; //第二次尝试获得锁,失败后开始addWaiter,进入阻塞队列
}
}
addWaiter 逻辑,链接到Node阻塞队列,Node队列为null,进行enq来初始化队列
无需初始化就直接进入acquireQueued
//tryAcquire(arg)失败后,开始addWaiter(Node.EXCLUSIVE)
// AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点
private Node addWaiter(Node mode) {
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; //尾节点
// 如果 tail 不为 null,说明存在阻塞队列,在尾部插入
if (pred != null) {
// 将当前线程的前驱节点指向 尾节点
node.prev = pred;
// 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点,【尾插法】
if (compareAndSetTail(pred, node)) {
pred.next = node; //双向链表,尾节点指向当前线程
return node;
}
}
// 初始时队列为空,或者 CAS 失败进入enq,插入队列,并初始化
enq(node);
return node;
}
static final Node EXCLUSIVE = null; //Node初始值为null
enq初始化队列,初始化完成,进入acquireQueued
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

// AbstractQueuedSynchronizer#enq
//将当前线程(封装在了Node中)插入队列,必要时初始化
private Node enq(final Node node) {
// 自旋入队,必须入队成功才结束循环
for (;;) {
Node t = tail;
// 当前线程可能是【第一个获取锁失败】的线程,【还没有建立队列】
if (t == null) { // Must initialize
// 设置一个【哑元节点】,头尾指针都指向该节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 自旋到这,普通入队方式,首先赋值尾节点到前驱节点【尾插法】
node.prev = t; //当前线程前驱节点指向尾节点
// -------------------->【①】<--------------------
//更新尾节点
if (compareAndSetTail(t, node)) {
//-------------------->【②】<--------------------
t.next = node; //双向链表,原尾节点的后继节点指向node
return t; // 返回尾节点(即当前线程的前驱节点),开始acquireQueued
}
}
}
}
//【①】在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点
//【②】此时还没有后继节点 t.next = null,并且这里已经 CAS 结束,线程并不是安全的
acquireQueued中陷入死循环,只能获得锁结束循环
失败后进入 park 阻塞(检查唤醒直接,判断是否可以park)
不可打断模式,被打断也不会结束循环
可打断模式下,被打断进行cancelAcquire

//node为前置节点
final boolean acquireQueued(final Node node, int arg) {
// true 表示当前线程抢占锁失败,false 表示成功
boolean failed = true;
try {
// 中断标记,表示当前线程是否被中断
boolean interrupted = false;
//死循环,正常情况下线程只有获得锁才能跳出循环
for (;;) {
// 获得【所有前驱节点】
final Node p = node.predecessor();
// 前驱的前驱是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁
if (p == head && tryAcquire(arg)) { //第三次尝试获得锁
setHead(node); // 获取成功, 设置当前线程自己的 node 为 head
p.next = null; // help GC
failed = false; // 表示抢占锁成功
return interrupted; // 返回当前线程是否被中断,【得到锁,结束循环】
}
//1. 判断是否应当阻塞(waitStatus是否SIGNAL即-1,有唤醒职责,确保唤醒才能阻塞)
//2. 判断是否被打断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//【就算被打断了,也会继续循环,并不会返回】
interrupted = true;
}
} finally {
// 【可打断模式下才会进入该逻辑】,撤销获取
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire检查是否有唤醒职责,有唤醒职责可park,执行parkAndCheckInterrupt
- 将前驱 node,即 head 的 waitStatus 改为 -1,返回 false
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取waitStatus,waitStatus = -1表示前置节点是个可以唤醒当前节点的节点
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //SIGNAL = -1
return true; //SIGNAL,则返回true,表示要阻塞当前线程
if (ws > 0) { //CANCELLED = 1,已取消
// 前置节点的状态处于取消状态,需要【删除前面所有取消的节点】, 返回到外层循环重试
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); //只要前驱节点waitStatus > 0,就循环
pred.next = node;
} else {
//初始化状态,此时通过compareAndSetWaitStatus方法将前驱的状态改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt进行park,判断当前线程是否被打断,清除打断标记
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
return Thread.interrupted();// 判断当前线程是否被打断,清除打断标记
}
持续acquireQueued中的死循环,直到获得锁
再次有多个线程经历上述过程竞争失败

解锁源码
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁,true表示完全释放,包括重入
Node h = head; // 队列头节点
if (h != null && h.waitStatus != 0) //有阻塞,且不是哑元,需要唤醒
unparkSuccessor(h); //唤醒后继
return true;
}
return false;
}
// ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //修改state,含重入
if (Thread.currentThread() != getExclusiveOwnerThread())//非持锁者,没资格,抛异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //释放成功,含重入
free = true;
setExclusiveOwnerThread(null);
}
setState(c); //持锁者,无需cas
return free;
}
unparkSuccessor唤醒
是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,而不是本节点的waitStatus 决定
从后向前的唤醒的原因:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失尾节点
//AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) //-1,有唤醒职责
compareAndSetWaitStatus(node, ws, 0); //改为0
Node s = node.next;
if (s == null || s.waitStatus > 0) { //没有后继,或是取消了
s = null;
//AQS 队列【从后tail至前】找需要 unpark 的节点,直到 t == node 为止,找不到就不唤醒
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒后继
}
唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 会清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收(图中有错误,原来的头节点的 waitStatus 被改为 0 了)

如果这时有其它线程来竞争(非公平),例如这时有 Thread-4 来了并抢占了锁
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

可重入
源码解析参考:nonfairTryAcquire(int acquires)) 和 tryRelease(int releases)
- 加锁两次解锁两次:正常执行
- 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明申请几把锁,最后需要解除几把锁
- 加锁一次解锁两次:运行程序会直接报错
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; //重入state++
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //state--
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可打断
lockInterruptibly():获得可打断的锁
- 如果没有竞争此方法就会获取 lock 对象锁
- 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断
不可打断模式
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 如果打断标记已经是 true, 则 park 会失效
return Thread.interrupted();// interrupted 会清除打断标记
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果打断状态为 true
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt(); // 重新产生一次中断
}
可打断模式
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// ㈠ 可打断的获取锁流程
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&//先检查 AQS 队列中是否有前驱节点, 没有才去竞争
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//h != t 时头不等于尾,表示队列中有 Node
//没有后继 或者 后继不是本线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
条件变量
await
总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,每个 Condition 对象都包含一个等待队列
开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里不存在并发
调用 await,进入 ConditionObject 的 addConditionWaiter 流程,创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

image-20220923161430336 接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

image-20220923161549941 park 阻塞 Thread-0

image-20220923161606085
private transient Node firstWaiter; // 第一个等待节点
private transient Node lastWaiter; // 最后一个等待节点
private static final int REINTERRUPT = 1;// 打断模式 - 在退出等待时重新设置打断状态
private static final int THROW_IE = -1;// 打断模式 - 在退出等待时抛出异常
public ConditionObject() { }
//5.1 转移后取消等待,这个方法只有在线程是被打断唤醒时才会调用
final boolean transferAfterCancelledWait(Node node) {
// 条件成立说明当前node一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 把【中断唤醒的 node 加入到阻塞队列中】
enq(node);
// 表示是在条件队列内被中断了,设置为 THROW_IE 为 -1
return true;
}
//执行到这里的情况:
//1.当前node已经被外部线程调用 signal 方法将其迁移到 阻塞队列 内了
//2.当前node正在被外部线程调用 signal 方法将其迁移至 阻塞队列 进行中状态
// 如果当前线程还没到阻塞队列,一直释放 CPU
while (!isOnSyncQueue(node))
Thread.yield();
// 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 为 1
return false;
}
//--------------------- 5. 检查阻塞打断 --------------------------
private int checkInterruptWhileWaiting(Node node) {
// Thread.interrupted() 返回当前线程中断标记位,并且重置当前标记位 为 false
// 如果被中断了,根据是否在条件队列被中断的,设置中断状态码
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
//2.1 清理条件队列内所有已取消(不是CONDITION)的 node,【链表删除的逻辑】
private void unlinkCancelledWaiters() {
// 从头节点开始遍历【FIFO】
Node t = firstWaiter;
// 指向正常的 CONDITION 节点
Node trail = null;
// 等待队列不空
while (t != null) {
// 获取当前节点的后继节点
Node next = t.nextWaiter;
// 判断 t 节点是不是 CONDITION 节点,条件队列内不是 CONDITION 就不是正常的
if (t.waitStatus != Node.CONDITION) {
// 不是正常节点,需要 t 与下一个节点断开
t.nextWaiter = null;
// 条件成立说明遍历到的节点还未碰到过正常节点
if (trail == null)
// 更新 firstWaiter 指针为下个节点
firstWaiter = next;
else
// 让上一个正常节点指向 当前取消节点的 下一个节点,【删除非正常的节点】
trail.nextWaiter = next;
// t 是尾节点了,更新 lastWaiter 指向最后一个正常节点
if (next == null)
lastWaiter = trail;
} else {
// trail 指向的是正常节点
trail = t;
}
// 把 t.next 赋值给 t,循环遍历
t = next;
}
}
//------------------ 2. 添加一个 Node 至等待队列 ---------------
private Node addConditionWaiter() {
Node t = lastWaiter;
// 所有已取消的 Node 从队列链表删除 Node.CONDITION = -2
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); //2.1
t = lastWaiter;
}
// 创建一个关联当前线程的新 Node, 添加至队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node; //首节点 = null,就添加为首
else
t.nextWaiter = node;//不为null,就添加为后继节点
lastWaiter = node;
return node;
}
//-------------------------- 1. await --------------------------
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); //被打断,抛异常
//2. 将调用 await 的线程包装成 Node,添加到条件队列并返回
Node node = addConditionWaiter();
//3. 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】
int savedState = fullyRelease(node);
int interruptMode = 0; // 设置打断模式为没有被打断,状态码为 0
//4. 如果该节点还没有转移至 AQS 阻塞队列, park 阻塞,等待进入阻塞队列
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//5. 如果被打断,退出等待队列,对应的 node 【也会被迁移到阻塞队列】尾部,状态设置为0
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 逻辑到这说明当前线程退出等待队列,进入【阻塞队列】
// 尝试枪锁,释放了多少锁就【重新获取多少锁】,获取锁成功判断打断模式
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// node 在条件队列时,如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设 nextWaiter = null
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();// 清理条件队列内所有已取消的 Node
if (interruptMode != 0) // 条件成立说明挂起期间发生过中断
reportInterruptAfterWait(interruptMode);//6. 应用打断模式
}
//-------------------------- 3. 释放锁 --------------------------
final int fullyRelease(Node node) {
boolean failed = true; // 完全释放锁是否成功,false 代表成功
try {
int savedState = getState();
// release -> tryRelease 解锁重入锁
if (release(savedState)) {
failed = false;
return savedState; // 释放成功,返回解锁的深度
} else { // 解锁失败抛出异常
throw new IllegalMonitorStateException();
}
} finally {
if (failed) // 没有释放成功,将当前 node 设置为取消状态
node.waitStatus = Node.CANCELLED;
}
}
//--------------------------4. --------------------------
final boolean isOnSyncQueue(Node node) {
// node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还【没有完成迁移】
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 说明当前节点已经成功入队到阻塞队列,且当前节点后面已经有其它 node,因为条件队列的 next 指针为 null
if (node.next != null) // If has successor, it must be on queue
return true;
// 说明【可能在阻塞队列,但是是尾节点】
// 从阻塞队列的尾节点开始向前【遍历查找 node】,如果查找到返回 true,查找不到返回 false
return findNodeFromTail(node);
}
//6. --------------- 应用打断模式,开始处理中断状态 -----------------
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
// 条件成立说明【在条件队列内发生过中断,此时 await 方法抛出中断异常】
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 条件成立说明【在条件队列外发生的中断,此时设置当前线程的中断标记位为 true】
else if (interruptMode == REINTERRUPT)
// 进行一次自己打断,产生中断的效果
selfInterrupt();
}
signal
设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,必须持有锁才能唤醒, 因此 doSignal 内线程安全

image-20220923162122046 public final void signal() { // 判断调用 signal 方法的线程是否是独占锁持有线程 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 获取条件队列中第一个 Node Node first = firstWaiter; // 不为空就将第该节点【迁移到阻塞队列】 if (first != null) doSignal(first); }进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

image-20220923162148268 // 唤醒 - 【将没取消的第一个节点转移至 AQS 队列尾部】 private void doSignal(Node first) { do { // 成立说明当前节点的下一个节点是 null,当前节点是尾节点了,队列中只有当前一个节点了 if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 将等待队列中的 Node 转移至 AQS 队列,不成功且还有节点则继续循环 } while (!transferForSignal(first) && (first = firstWaiter) != null); } // signalAll() 会调用这个函数,唤醒所有的节点 private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; // 唤醒所有的节点,都放到阻塞队列中 } while (first != null); }执行 transferForSignal 流程,先将节点的 waitStatus 改为 0,然后加入 AQS 阻塞队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1

image-20220923162232675 // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 final boolean transferForSignal(Node node) { // CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了 // 如果状态已经不是 CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire) if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 返回函数调用处继续寻找下一个节点 return false; // 【先改状态,再进行迁移】 // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的【前驱节点】 Node p = enq(node); int ws = p.waitStatus; // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL,就 unpark 取消当前节点线程的阻塞状态, // 让 thread-0 线程竞争锁,重新同步状态 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }Thread-1 释放锁,进入 unlock 流程
ReentrantReadWriteLock
成员属性
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
读写锁:
private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock;构造方法:默认是非公平锁,可以指定参数创建公平锁
public ReentrantReadWriteLock(boolean fair) { // true 为公平锁 sync = fair ? new FairSync() : new NonfairSync(); // 这两个lock共享同一个sync实例,都是由ReentrantReadWriteLock的sync提供同步实现 readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
Sync 类的属性:
统计变量:
// 用来移位 static final int SHARED_SHIFT = 16; // 高16位的1 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 65535,16个1,代表写锁的最大重入次数 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;获取读写锁的次数:
// 获取读写锁的读锁分配的总次数 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 写锁(独占)锁的重入次数 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }内部类:
// 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量 static final class HoldCounter { int count = 0; // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); } // 线程安全的存放线程各自的 HoldCounter 对象 static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } }内部类实例:
// 当前线程持有的可重入读锁的数量,计数为 0 时删除 private transient ThreadLocalHoldCounter readHolds; // 记录最后一个获取【读锁】线程的 HoldCounter 对象 private transient HoldCounter cachedHoldCounter;首次获取锁:
// 第一个获取读锁的线程 private transient Thread firstReader = null; // 记录该线程持有的读锁次数(读锁重入次数) private transient int firstReaderHoldCount;Sync 构造方法:
Sync() { readHolds = new ThreadLocalHoldCounter(); // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】 setState(getState()); }
加锁
t1 线程:w.lock(写锁),成功上锁 state = 0_1

// lock() -> sync.acquire(1); public void lock() { sync.acquire(1); } public final void acquire(int arg) { // 尝试获得写锁,获得写锁失败,将当前线程关联到一个 Node 对象上, 模式为独占模式 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); // 获得低 16 位, 代表写锁的 state 计数 int w = exclusiveCount(c); // 说明有读锁或者写锁 if (c != 0) { // c != 0 and w == 0 表示有读锁,【读锁不能升级】,直接返回 false // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败 if (w == 0 || current != getExclusiveOwnerThread()) return false; // 执行到这里只有一种情况:【写锁重入】,所以下面几行代码不存在并发 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 写锁重入, 获得锁成功,没有并发,所以不使用 CAS setState(c + acquires); return true; } // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 获得锁成功,设置锁的持有线程为当前线程 setExclusiveOwnerThread(current); return true; } // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞 final boolean writerShouldBlock() { return false; } // 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 final boolean writerShouldBlock() { return hasQueuedPredecessors(); }t2 r.lock(读锁),进入 tryAcquireShared 流程:
tryAcquireShared 返回值表示
- -1 表示失败
- 0 表示成功,但后继节点不会继续唤醒
- 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1

public void lock() { sync.acquireShared(1); } public final void acquireShared(int arg) { // tryAcquireShared 返回负数, 表示获取读锁失败 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }// 尝试以共享模式获取 protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); // exclusiveCount(c) 代表低 16 位, 写锁的 state,成立说明有线程持有写锁 // 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 高 16 位,代表读锁的 state,共享锁分配出去的总次数 int r = sharedCount(c); // 读锁是否应该阻塞 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // 尝试增加读锁计数 // 加锁成功 // 加锁之前读锁为 0,说明当前线程是第一个读锁线程 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; // 第一个读锁线程是自己就发生了读锁重入 } else if (firstReader == current) { firstReaderHoldCount++; } else { // cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程 HoldCounter rh = cachedHoldCounter; // 说明还没设置 rh if (rh == null || rh.tid != getThreadId(current)) // 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter cachedHoldCounter = rh = readHolds.get(); // 还没重入 else if (rh.count == 0) readHolds.set(rh); // 重入 + 1 rh.count++; } // 读锁加锁成功 return 1; } // 逻辑到这 应该阻塞,或者 cas 加锁失败 // 会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 return fullTryAcquireShared(current); } // 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 // 防止一直有读锁线程,导致写锁线程饥饿 // true 则该阻塞, false 则不阻塞 final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); }final int fullTryAcquireShared(Thread current) { // 当前读锁线程持有的读锁次数对象 HoldCounter rh = null; for (;;) { int c = getState(); // 说明有线程持有写锁 if (exclusiveCount(c) != 0) { // 写锁不是自己则获取锁失败 if (getExclusiveOwnerThread() != current) return -1; } else if (readerShouldBlock()) { // 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入 if (firstReader == current) { // assert firstReaderHoldCount > 0; } else { if (rh == null) { // 最后一个读锁的 HoldCounter rh = cachedHoldCounter; // 说明当前线程也不是最后一个读锁 if (rh == null || rh.tid != getThreadId(current)) { // 获取当前线程的 HoldCounter rh = readHolds.get(); // 条件成立说明 HoldCounter 对象是上一步代码新建的 // 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队 if (rh.count == 0) // 防止内存泄漏 readHolds.remove(); } } if (rh.count == 0) return -1; } } // 越界判断 if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 读锁加锁,条件内的逻辑与 tryAcquireShared 相同 if (compareAndSetState(c, c + SHARED_UNIT)) { if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; cachedHoldCounter = rh; // cache for release } return 1; } } }获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态

private void doAcquireShared(int arg) { // 将当前线程关联到一个 Node 对象上, 模式为共享模式 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { // 获取前驱节点 final Node p = node.predecessor(); // 如果前驱节点就头节点就去尝试获取锁 if (p == head) { // 再一次尝试获取读锁 int r = tryAcquireShared(arg); // r >= 0 表示获取成功 if (r >= 0) { //【这里会设置自己为头节点,唤醒相连的后序的共享节点】 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } // 是否在获取读锁失败时阻塞 park 当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处park

解锁
t1 w.unlock, 写锁解锁
public void unlock() { // 释放锁 sync.release(1); } public final boolean release(int arg) { // 尝试释放锁 if (tryRelease(arg)) { Node h = head; // 头节点不为空并且不是等待状态不是 0,唤醒后继的非取消节点 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; // 因为可重入的原因, 写锁计数为 0, 才算释放成功 boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一
接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,唤醒连续的所有的共享节点,下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 设置自己为 head 节点 setHead(node); // propagate 表示有共享资源(例如共享读锁或信号量),为 0 就没有资源 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 获取下一个节点 Node s = node.next; // 如果当前是最后一个节点,或者下一个节点是【等待共享读锁的节点】 if (s == null || s.isShared()) // 唤醒后继节点 doReleaseShared(); } }private void doReleaseShared() { // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==> Node.PROPAGATE for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; // SIGNAL 唤醒后继 if (ws == Node.SIGNAL) { // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0 // 防止 unparkSuccessor 被多次执行 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // 唤醒后继节点 unparkSuccessor(h); } // 如果已经是 0 了,改为 -3,用来解决传播性 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head, // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 if (h == head) break; } }t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点

public void unlock() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }protected final boolean tryReleaseShared(int unused) { for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程,计数为 0 才是真正释放 if (compareAndSetState(c, nextc)) // 返回是否已经完全释放了 return nextc == 0; } }t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束
Semaphore
加锁

Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源
Sync(int permits) { setState(permits); }假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞
// acquire() -> sync.acquireSharedInterruptibly(1),可中断 public final void acquireSharedInterruptibly(int arg) { if (Thread.interrupted()) throw new InterruptedException(); // 尝试获取通行证,获取成功返回 >= 0的值 if (tryAcquireShared(arg) < 0) // 获取许可证失败,进入阻塞 doAcquireSharedInterruptibly(arg); } // tryAcquireShared() -> nonfairTryAcquireShared() // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有临头节点(第二个节点) final int nonfairTryAcquireShared(int acquires) { for (;;) { // 获取 state ,state 这里【表示通行证】 int available = getState(); // 计算当前线程获取通行证完成之后,通行证还剩余数量 int remaining = available - acquires; // 如果许可已经用完, 返回负数, 表示获取失败, if (remaining < 0 || // 许可证足够分配的,如果 cas 重试成功, 返回正数, 表示获取成功 compareAndSetState(available, remaining)) return remaining; } }private void doAcquireSharedInterruptibly(int arg) { // 将调用 Semaphore.aquire 方法的线程,包装成 node 加入到 AQS 的阻塞队列中 final Node node = addWaiter(Node.SHARED); // 获取标记 boolean failed = true; try { for (;;) { final Node p = node.predecessor(); // 前驱节点是头节点可以再次获取许可 if (p == head) { // 再次尝试获取许可,【返回剩余的许可证数量】 int r = tryAcquireShared(arg); if (r >= 0) { // 成功后本线程出队(AQS), 所在 Node设置为 head // r 表示【可用资源数】, 为 0 则不会继续传播 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { // 被打断后进入该逻辑 if (failed) cancelAcquire(node); } }private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 设置自己为 head 节点 setHead(node); // propagate 表示有【共享资源】(例如共享读锁或信号量) // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE,doReleaseShared 函数中设置的 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 if (s == null || s.isShared()) doReleaseShared(); } }这时 Thread-4 释放了 permits,状态如下
// release() -> releaseShared() public final boolean releaseShared(int arg) { // 尝试释放锁 if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } protected final boolean tryReleaseShared(int releases) { for (;;) { // 获取当前锁资源的可用许可证数量 int current = getState(); int next = current + releases; // 索引越界判断 if (next < current) throw new Error("Maximum permit count exceeded"); // 释放锁 if (compareAndSetState(current, next)) return true; } } private void doReleaseShared() { // PROPAGATE 详解 // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==> Node.PROPAGATE }
image-20220923172645499 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态

image-20220923172707469
PROPAGATE
假设存在某次循环中队列里排队的结点情况为 head(-1) → t1(-1) → t2(0),存在将要释放信号量的 T3 和 T4,释放顺序为先 T3 后 T4
// 老版本代码
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
// 有空闲资源
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
// 下一个
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
正常流程:
- T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0
- T1 由于 T3 释放信号量被唤醒,然后 T4 释放,唤醒 T2
BUG 流程:
- T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0
- T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量)
- T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),不满足条件,因此不调用 unparkSuccessor(head)
- T1 获取信号量成功,调用 setHeadAndPropagate(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, T2 线程得不到唤醒
更新后流程:
T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0
T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量)
T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)
T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置自己为 head 节点
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
// 唤醒
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
CountDownLatch
阻塞等待:
线程调用 await() 等待其他线程完成任务:支持打断
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } // AbstractQueuedSynchronizer#acquireSharedInterruptibly public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 判断线程是否被打断,抛出打断异常 if (Thread.interrupted()) throw new InterruptedException(); // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源 // 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } // CountDownLatch.Sync#tryAcquireShared protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 将调用latch.await()方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { // 获取当前节点的前驱节点 final Node p = node.predecessor(); // 前驱节点时头节点就可以尝试获取锁 if (p == head) { // 再次尝试获取锁,获取成功返回 1 int r = tryAcquireShared(arg); if (r >= 0) { // 获取锁成功,设置当前节点为 head 节点,并且向后传播 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } // 阻塞在这里 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { // 阻塞线程被中断后抛出异常,进入取消节点的逻辑 if (failed) cancelAcquire(node); } }获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点:
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null setHead(node); // propagate = 1,条件一成立 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 获取当前节点的后继节点 Node s = node.next; // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式 if (s == null || s.isShared()) // 唤醒所有的等待共享锁的节点 doReleaseShared(); } }
计数减一:
线程进入 countDown() 完成计数器减一(释放锁)的操作
public void countDown() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { // 尝试释放共享锁 if (tryReleaseShared(arg)) { // 释放锁成功开始唤醒阻塞节点 doReleaseShared(); return true; } return false; }更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true
protected boolean tryReleaseShared(int releases) { for (;;) { int c = getState(); // 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false if (c == 0) return false; // 计数器减一 int nextc = c-1; if (compareAndSetState(c, nextc)) // 计数器为 0 时返回 true return nextc == 0; } }state = 0 时,当前线程需要执行唤醒阻塞节点的任务
private void doReleaseShared() { for (;;) { Node h = head; // 判断队列是否是空队列 if (h != null && h != tail) { int ws = h.waitStatus; // 头节点的状态为 signal,说明后继节点没有被唤醒过 if (ws == Node.SIGNAL) { // cas 设置头节点的状态为 0,设置失败继续自旋 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // 唤醒后继节点 unparkSuccessor(h); } // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 if (h == head) break; } }
CyclicBarrier
成员属性
全局锁:利用可重入锁实现的工具类
// barrier 实现是依赖于Condition条件队列,condition 条件队列必须依赖lock才能使用 private final ReentrantLock lock = new ReentrantLock(); // 线程挂起实现使用的 condition 队列,当前代所有线程到位,这个条件队列内的线程才会被唤醒 private final Condition trip = lock.newCondition();线程数量:
private final int parties; // 代表多少个线程到达屏障开始触发线程任务 private int count; // 表示当前“代”还有多少个线程未到位,初始值为 parties当前代中最后一个线程到位后要执行的事件:
private final Runnable barrierCommand;代:
// 表示 barrier 对象当前 代 private Generation generation = new Generation(); private static class Generation { // 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常 // 且在这一代挂起的线程都会被唤醒,然后抛出 BrokerException 异常。 boolean broken = false; }构造方法:
public CyclicBarrie(int parties, Runnable barrierAction) { // 因为小于等于 0 的 barrier 没有任何意义 if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; // 可以为 null this.barrierCommand = barrierAction; }

成员方法
await():阻塞等待所有线程到位
public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } }// timed:表示当前调用await方法的线程是否指定了超时时长,如果 true 表示线程是响应超时的 // nanos:线程等待超时时长,单位是纳秒 private int dowait(boolean timed, long nanos) { final ReentrantLock lock = this.lock; // 加锁 lock.lock(); try { // 获取当前代 final Generation g = generation; // 【如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常】 if (g.broken) throw new BrokenBarrierException(); // 如果当前线程被中断了,则打破当前代,然后当前线程抛出中断异常 if (Thread.interrupted()) { // 设置当前代的状态为 broken 状态,唤醒在 trip 条件队列内的线程 breakBarrier(); throw new InterruptedException(); } // 逻辑到这说明,当前线程中断状态是 false, 当前代的 broken 为 false(未打破状态) // 假设 parties 给的是 5,那么index对应的值为 4,3,2,1,0 int index = --count; // 条件成立说明当前线程是最后一个到达 barrier 的线程,【需要开启新代,唤醒阻塞线程】 if (index == 0) { // 栅栏任务启动标记 boolean ranAction = false; try { final Runnable command = barrierCommand; if (command != null) // 启动触发的任务 command.run(); // run()未抛出异常的话,启动标记设置为 true ranAction = true; // 开启新的一代,这里会【唤醒所有的阻塞队列】 nextGeneration(); // 返回 0 因为当前线程是此代最后一个到达的线程,index == 0 return 0; } finally { // 如果 command.run() 执行抛出异常的话,会进入到这里 if (!ranAction) breakBarrier(); } } // 自旋,一直到条件满足、当前代被打破、线程被中断,等待超时 for (;;) { try { // 根据是否需要超时等待选择阻塞方法 if (!timed) // 当前线程释放掉 lock,【进入到 trip 条件队列的尾部挂起自己】,等待被唤醒 trip.await(); else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { // 被中断后来到这里的逻辑 // 当前代没有变化并且没有被打破 if (g == generation && !g.broken) { // 打破屏障 breakBarrier(); // node 节点在【条件队列】内收到中断信号时 会抛出中断异常 throw ie; } else { // 等待过程中代变化了,完成一次自我打断 Thread.currentThread().interrupt(); } } // 唤醒后的线程,【判断当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常】 if (g.broken) throw new BrokenBarrierException(); // 当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑 if (g != generation) return index; // 当前线程 trip 中等待超时,然后主动转移到阻塞队列 if (timed && nanos <= 0L) { breakBarrier(); // 抛出超时异常 throw new TimeoutException(); } } } finally { // 解锁 lock.unlock(); } }breakBarrier():打破 Barrier 屏障
private void breakBarrier() { // 将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常 generation.broken = true; // 重置 count 为 parties count = parties; // 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常 trip.signalAll(); }nextGeneration():开启新的下一代
private void nextGeneration() { // 将在 trip 条件队列内挂起的线程全部唤醒 trip.signalAll(); // 重置 count 为 parties count = parties; // 开启新的一代,使用一个新的generation对象,表示新的一代,新的一代和上一代【没有任何关系】 generation = new Generation(); }
参考视频:https://space.bilibili.com/457326371/
ConcurrentHashMap
三种集合:
- HashMap 是线程不安全的,性能好
- Hashtable 线程安全基于 synchronized,综合性能差,已经被淘汰
- ConcurrentHashMap 保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好
集合对比
Hashtable 继承 Dictionary 类,HashMap、ConcurrentHashMap 继承 AbstractMap,均实现 Map 接口
Hashtable 底层是数组 + 链表,JDK8 以后 HashMap 和 ConcurrentHashMap 底层是数组 + 链表 + 红黑树
HashMap 线程非安全,Hashtable 线程安全,Hashtable 的方法都加了 synchronized 关来确保线程同步
ConcurrentHashMap、Hashtable 不允许 null 值,HashMap 允许 null 值
ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity * 2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1

工作步骤
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
初始化,使用 cas 来保证并发安全,懒惰初始化 table
树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
说明:锁住某个槽位的对象头,是一种很好的细粒度的加锁方式,类似 MySQL 中的行锁
put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索
扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容
size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加
//需求:多个线程同时往HashMap容器中存入数据会出现安全问题
public class ConcurrentHashMapDemo{
public static Map<String,String> map = new ConcurrentHashMap();
public static void main(String[] args){
new AddMapDataThread().start();
new AddMapDataThread().start();
Thread.sleep(1000 * 5);//休息5秒,确保两个线程执行完毕
System.out.println("Map大小:" + map.size());//20万
}
}
public class AddMapDataThread extends Thread{
@Override
public void run() {
for(int i = 0 ; i < 1000000 ; i++ ){
ConcurrentHashMapDemo.map.put("键:"+i , "值"+i);
}
}
}
并发死链
JDK1.7 的 HashMap 采用的头插法(拉链法)进行节点的添加,HashMap 的扩容长度为原来的 2 倍
resize() 中节点(Entry)转移的源代码:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;//得到新数组的长度
// 遍历整个数组对应下标下的链表,e代表一个节点
for (Entry<K,V> e : table) {
// 当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表
while(null != e) {
// 先把e节点的下一节点存起来
Entry<K,V> next = e.next;
if (rehash) { //得到新的hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
// 在新数组下得到新的数组下标
int i = indexFor(e.hash, newCapacity);
// 将e的next指针指向新数组下标的位置
e.next = newTable[i];
// 将该数组下标的节点变为e节点
newTable[i] = e;
// 遍历链表的下一节点
e = next;
}
}
}
JDK 8 虽然将扩容算法做了调整,改用了尾插法,但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
B站视频解析:https://www.bilibili.com/video/BV1n541177Ea
成员属性
变量
存储数组:
transient volatile Node<K,V>[] table;散列表的长度:
private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大长度 private static final int DEFAULT_CAPACITY = 16; // 默认长度并发级别,JDK7 遗留下来,1.8 中不代表并发级别:
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;负载因子,JDK1.8 的 ConcurrentHashMap 中是固定值:
private static final float LOAD_FACTOR = 0.75f;阈值:
static final int TREEIFY_THRESHOLD = 8; // 链表树化的阈值 static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转化为链表的阈值 static final int MIN_TREEIFY_CAPACITY = 64; // 当数组长度达到64且某个桶位中的链表长度超过8,才会真正树化扩容相关:
private static final int MIN_TRANSFER_STRIDE = 16; // 线程迁移数据【最小步长】,控制线程迁移任务的最小区间 private static int RESIZE_STAMP_BITS = 16; // 用来计算扩容时生成的【标识戳】 private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;// 65535-1并发扩容最多线程数 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 扩容时使用节点哈希值:
static final int MOVED = -1; // 表示当前节点是 FWD 节点 static final int TREEBIN = -2; // 表示当前节点已经树化,且当前节点为 TreeBin 对象 static final int RESERVED = -3; // 表示节点时临时节点 static final int HASH_BITS = 0x7fffffff; // 正常节点的哈希值的可用的位数扩容过程:volatile 修饰保证多线程的可见性
// 扩容过程中,会将扩容中的新 table 赋值给 nextTable 保持引用,扩容结束之后,这里会被设置为 null private transient volatile Node<K,V>[] nextTable; // 记录扩容进度,所有线程都要从 0 - transferIndex 中分配区间任务,简单说就是老表转移到哪了,索引从高到低转移 private transient volatile int transferIndex;累加统计:
// LongAdder 中的 baseCount 未发生竞争时或者当前LongAdder处于加锁状态时,增量累到到 baseCount 中 private transient volatile long baseCount; // LongAdder 中的 cellsBuzy,0 表示当前 LongAdder 对象无锁状态,1 表示当前 LongAdder 对象加锁状态 private transient volatile int cellsBusy; // LongAdder 中的 cells 数组, private transient volatile CounterCell[] counterCells;控制变量:
sizeCtl < 0:
-1 表示当前 table 正在初始化(有线程在创建 table 数组),当前线程需要自旋等待
其他负数表示当前 map 的 table 数组正在进行扩容,高 16 位表示扩容的标识戳;低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + 1
sizeCtl = 0,表示创建 table 数组时使用 DEFAULT_CAPACITY 为数组大小
sizeCtl > 0:
如果 table 未初始化,表示初始化大小
如果 table 已经初始化,表示下次扩容时的触发条件(阈值,元素个数,不是数组的长度)
private transient volatile int sizeCtl; // volatile 保持可见性
内部类
Node 节点:
static class Node<K,V> implements Entry<K,V> { // 节点哈希值 final int hash; final K key; volatile V val; // 单向链表 volatile Node<K,V> next; }TreeBin 节点:
static final class TreeBin<K,V> extends Node<K,V> { // 红黑树根节点 TreeNode<K,V> root; // 链表的头节点 volatile TreeNode<K,V> first; // 等待者线程 volatile Thread waiter; volatile int lockState; // 写锁状态 写锁是独占状态,以散列表来看,真正进入到 TreeBin 中的写线程同一时刻只有一个线程 static final int WRITER = 1; // 等待者状态(写线程在等待),当 TreeBin 中有读线程目前正在读取数据时,写线程无法修改数据 static final int WAITER = 2; // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi 对象中获取数据,每一个线程都给 lockState + 4 static final int READER = 4; }TreeNode 节点:
static final class TreeNode<K,V> extends Node<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; //双向链表 boolean red; }ForwardingNode 节点:转移节点
static final class ForwardingNode<K,V> extends Node<K,V> { // 持有扩容后新的哈希表的引用 final Node<K,V>[] nextTable; ForwardingNode(Node<K,V>[] tab) { // ForwardingNode 节点的 hash 值设为 -1 super(MOVED, null, null, null); this.nextTable = tab; } }
代码块
变量:
// 表示sizeCtl属性在 ConcurrentHashMap 中内存偏移地址 private static final long SIZECTL; // 表示transferIndex属性在 ConcurrentHashMap 中内存偏移地址 private static final long TRANSFERINDEX; // 表示baseCount属性在 ConcurrentHashMap 中内存偏移地址 private static final long BASECOUNT; // 表示cellsBusy属性在 ConcurrentHashMap 中内存偏移地址 private static final long CELLSBUSY; // 表示cellValue属性在 CounterCell 中内存偏移地址 private static final long CELLVALUE; // 表示数组第一个元素的偏移地址 private static final long ABASE; // 用位移运算替代乘法 private static final int ASHIFT;赋值方法:
// 表示数组单元所占用空间大小,scale 表示 Node[] 数组中每一个单元所占用空间大小,int 是 4 字节 int scale = U.arrayIndexScale(ak); // 判断一个数是不是 2 的 n 次幂,比如 8:1000 & 0111 = 0000 if ((scale & (scale - 1)) != 0) throw new Error("data type scale not a power of two"); // numberOfLeadingZeros(n):返回当前数值转换为二进制后,从高位到低位开始统计,看有多少个0连续在一起 // 8 → 1000 numberOfLeadingZeros(8) = 28 // 4 → 100 numberOfLeadingZeros(4) = 29 int 值就是占4个字节 ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); // ASHIFT = 31 - 29 = 2 ,int 的大小就是 2 的 2 次方,获取次方数 // ABASE + (5 << ASHIFT) 用位移运算替代了乘法,获取 arr[5] 的值
构造方法
无参构造, 散列表结构延迟初始化,默认的数组大小是 16:
public ConcurrentHashMap() { }有参构造:
public ConcurrentHashMap(int initialCapacity) { // 指定容量初始化 if (initialCapacity < 0) throw new IllegalArgumentException(); int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : // 假如传入的参数是 16,16 + 8 + 1 ,最后得到 32 // 传入 12, 12 + 6 + 1 = 19,最后得到 32,尽可能的大,与 HashMap不一样 tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 this.sizeCtl = cap; }private static final int tableSizeFor(int c) { int n = c - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }HashMap 部分详解了该函数,核心思想就是把最高位是 1 的位以及右边的位全部置 1,结果加 1 后就是 2 的 n 次幂
多个参数构造方法:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); // 初始容量小于并发级别 if (initialCapacity < concurrencyLevel) // 把并发级别赋值给初始容量 initialCapacity = concurrencyLevel; // loadFactor 默认是 0.75 long size = (long)(1.0 + (long)initialCapacity / loadFactor); int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 this.sizeCtl = cap; }集合构造方法:
public ConcurrentHashMap(Map<? extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY; // 默认16 putAll(m); } public void putAll(Map<? extends K, ? extends V> m) { // 尝试触发扩容 tryPresize(m.size()); for (Entry<? extends K, ? extends V> e : m.entrySet()) putVal(e.getKey(), e.getValue(), false); }private final void tryPresize(int size) { // 扩容为大于 2 倍的最小的 2 的 n 次幂 int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1); int sc; while ((sc = sizeCtl) >= 0) { Node<K,V>[] tab = table; int n; // 数组还未初始化,【一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的】 if (tab == null || (n = tab.length) == 0) { n = (sc > c) ? sc : c; if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if (table == tab) { Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = nt; sc = n - (n >>> 2);// 扩容阈值:n - 1/4 n } } finally { sizeCtl = sc; // 扩容阈值赋值给sizeCtl } } } // 未达到扩容阈值或者数组长度已经大于最大长度 else if (c <= sc || n >= MAXIMUM_CAPACITY) break; // 与 addCount 逻辑相同 else if (tab == table) { } } }
成员方法
数据访存
tabAt():获取数组某个槽位的头节点,类似于数组中的直接寻址 arr[i]
// i 是数组索引 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { // (i << ASHIFT) + ABASE == ABASE + i * 4 (一个 int 占 4 个字节),这就相当于寻址,替代了乘法 return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }casTabAt():指定数组索引位置修改原值为指定的值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }setTabAt():指定数组索引位置设置值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); }
添加方法
public V put(K key, V value) {
// 第三个参数 onlyIfAbsent 为 false 表示哈希表中存在相同的 key 时【用当前数据覆盖旧数据】
return putVal(key, value, false);
}
putVal()
final V putVal(K key, V value, boolean onlyIfAbsent) { // 【ConcurrentHashMap 不能存放 null 值】 if (key == null || value == null) throw new NullPointerException(); // 扰动运算,高低位都参与寻址运算 int hash = spread(key.hashCode()); // 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中的所属链表的下标位置 int binCount = 0; // tab 引用当前 map 的数组 table,开始自旋 for (Node<K,V>[] tab = table;;) { // f 表示桶位的头节点,n 表示哈希表数组的长度 // i 表示 key 通过寻址计算后得到的桶位下标,fh 表示桶位头结点的 hash 值 Node<K,V> f; int n, i, fh; // 【CASE1】:表示当前 map 中的 table 尚未初始化 if (tab == null || (n = tab.length) == 0) //【延迟初始化】 tab = initTable(); // 【CASE2】:i 表示 key 使用【寻址算法】得到 key 对应数组的下标位置,tabAt 获取指定桶位的头结点f else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 对应的数组为 null 说明没有哈希冲突,直接新建节点添加到表中 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; } // 【CASE3】:逻辑说明数组已经被初始化,并且当前 key 对应的位置不为 null // 条件成立表示当前桶位的头结点为 FWD 结点,表示目前 map 正处于扩容过程中 else if ((fh = f.hash) == MOVED) // 当前线程【需要去帮助哈希表完成扩容】 tab = helpTransfer(tab, f); // 【CASE4】:哈希表没有在扩容,当前桶位可能是链表也可能是红黑树 else { // 当插入 key 存在时,会将旧值赋值给 oldVal 返回 V oldVal = null; // 【锁住当前 key 寻址的桶位的头节点】 synchronized (f) { // 这里重新获取一下桶的头节点有没有被修改,因为可能被其他线程修改过,这里是线程安全的获取 if (tabAt(tab, i) == f) { // 【头节点的哈希值大于 0 说明当前桶位是普通的链表节点】 if (fh >= 0) { // 当前的插入操作没出现重复的 key,追加到链表的末尾,binCount表示链表长度 -1 // 插入的key与链表中的某个元素的 key 一致,变成替换操作,binCount 表示第几个节点冲突 binCount = 1; // 迭代循环当前桶位的链表,e 是每次循环处理节点,e 初始是头节点 for (Node<K,V> e = f;; ++binCount) { // 当前循环节点 key K ek; // key 的哈希值与当前节点的哈希一致,并且 key 的值也相同 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 把当前节点的 value 赋值给 oldVal oldVal = e.val; // 允许覆盖 if (!onlyIfAbsent) // 新数据覆盖旧数据 e.val = value; // 跳出循环 break; } Node<K,V> pred = e; // 如果下一个节点为空,把数据封装成节点插入链表尾部,【binCount 代表长度 - 1】 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 当前桶位头节点是红黑树 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // 条件成立说明当前是链表或者红黑树 if (binCount != 0) { // 如果 binCount >= 8 表示处理的桶位一定是链表,说明长度是 9 if (binCount >= TREEIFY_THRESHOLD) // 树化 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 统计当前 table 一共有多少数据,判断是否达到扩容阈值标准,触发扩容 // binCount = 0 表示当前桶位为 null,node 可以直接放入,2 表示当前桶位已经是红黑树 addCount(1L, binCount); return null; }spread():扰动函数
将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,与树化节点和转移节点区分,把高低位都利用起来减少哈希冲突,保证散列的均匀性
static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; // 0111 1111 1111 1111 1111 1111 1111 1111 }initTable():初始化数组,延迟初始化
private final Node<K,V>[] initTable() { // tab 引用 map.table,sc 引用 sizeCtl Node<K,V>[] tab; int sc; // table 尚未初始化,开始自旋 while ((tab = table) == null || tab.length == 0) { // sc < 0 说明 table 正在初始化或者正在扩容,当前线程可以释放 CPU 资源 if ((sc = sizeCtl) < 0) Thread.yield(); // sizeCtl 设置为 -1,相当于加锁,【设置的是 SIZECTL 位置的数据】, // 因为是 sizeCtl 是基本类型,不是引用类型,所以 sc 保存的是数据的副本 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 线程安全的逻辑,再进行一次判断 if ((tab = table) == null || tab.length == 0) { // sc > 0 创建 table 时使用 sc 为指定大小,否则使用 16 默认值 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 创建哈希表数组 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; // 扩容阈值,n >>> 2 => 等于 1/4 n ,n - (1/4)n = 3/4 n => 0.75 * n sc = n - (n >>> 2); } } finally { // 解锁,把下一次扩容的阈值赋值给 sizeCtl sizeCtl = sc; } break; } } return tab; }treeifyBin():树化方法
private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; if (tab != null) { // 条件成立:【说明当前 table 数组长度未达到 64,此时不进行树化操作,进行扩容操作】 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // 当前容量的 2 倍 tryPresize(n << 1); // 条件成立:说明当前桶位有数据,且是普通 node 数据。 else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // 【树化加锁】 synchronized (b) { // 条件成立:表示加锁没问题。 if (tabAt(tab, index) == b) { TreeNode<K,V> hd = null, tl = null; for (Node<K,V> e = b; e != null; e = e.next) { TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val,null, null); if ((p.prev = tl) == null) hd = p; else tl.next = p; tl = p; } setTabAt(tab, index, new TreeBin<K,V>(hd)); } } } } }addCount():添加计数,代表哈希表中的数据总量
private final void addCount(long x, int check) { // 【上面这部分的逻辑就是 LongAdder 的累加逻辑】 CounterCell[] as; long b, s; // 判断累加数组 cells 是否初始化,没有就去累加 base 域,累加失败进入条件内逻辑 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; // true 未竞争,false 发生竞争 boolean uncontended = true; // 判断 cells 是否被其他线程初始化 if (as == null || (m = as.length - 1) < 0 || // 前面的条件为 fasle 说明 cells 被其他线程初始化,通过 hash 寻址对应的槽位 (a = as[ThreadLocalRandom.getProbe() & m]) == null || // 尝试去对应的槽位累加,累加失败进入 fullAddCount 进行重试或者扩容 !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // 与 Striped64#longAccumulate 方法相同 fullAddCount(x, uncontended); return; } // 表示当前桶位是 null,或者一个链表节点 if (check <= 1) return; // 【获取当前散列表元素个数】,这是一个期望值 s = sumCount(); } // 表示一定 【是一个 put 操作调用的 addCount】 if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; // 条件一:true 说明当前 sizeCtl 可能为一个负数表示正在扩容中,或者 sizeCtl 是一个正数,表示扩容阈值 // false 表示哈希表的数据的数量没达到扩容条件 // 然后判断当前 table 数组是否初始化了,当前 table 长度是否小于最大值限制,就可以进行扩容 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { // 16 -> 32 扩容 标识为:1000 0000 0001 1011,【负数,扩容批次唯一标识戳】 int rs = resizeStamp(n); // 表示当前 table,【正在扩容】,sc 高 16 位是扩容标识戳,低 16 位是线程数 + 1 if (sc < 0) { // 条件一:判断扩容标识戳是否一样,fasle 代表一样 // 勘误两个条件: // 条件二是:sc == (rs << 16 ) + 1,true 代表扩容完成,因为低16位是1代表没有线程扩容了 // 条件三是:sc == (rs << 16) + MAX_RESIZERS,判断是否已经超过最大允许的并发扩容线程数 // 条件四:判断新表的引用是否是 null,代表扩容完成 // 条件五:【扩容是从高位到低位转移】,transferIndex < 0 说明没有区间需要扩容了 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // 设置当前线程参与到扩容任务中,将 sc 低 16 位值加 1,表示多一个线程参与扩容 // 设置失败其他线程或者 transfer 内部修改了 sizeCtl 值 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) //【协助扩容线程】,持有nextTable参数 transfer(tab, nt); } // 逻辑到这说明当前线程是触发扩容的第一个线程,线程数量 + 2 // 1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010 else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) //【触发扩容条件的线程】,不持有 nextTable,初始线程会新建 nextTable transfer(tab, null); s = sumCount(); } } }resizeStamp():扩容标识符,每次扩容都会产生一个,不是每个线程都产生,16 扩容到 32 产生一个,32 扩容到 64 产生一个
/** * 扩容的标识符 * 16 -> 32 从16扩容到32 * numberOfLeadingZeros(16) => 1 0000 => 32 - 5 = 27 => 0000 0000 0001 1011 * (1 << (RESIZE_STAMP_BITS - 1)) => 1000 0000 0000 0000 => 32768 * --------------------------------------------------------------- * 0000 0000 0001 1011 * 1000 0000 0000 0000 * 1000 0000 0001 1011 * 永远是负数 */ static final int resizeStamp(int n) { // 或运算 return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); // (16 -1 = 15) }
扩容方法
扩容机制:
- 当链表中元素个数超过 8 个,数组的大小还未超过 64 时,此时进行数组的扩容,如果超过则将链表转化成红黑树
- put 数据后调用 addCount() 方法,判断当前哈希表的容量超过阈值 sizeCtl,超过进行扩容
- 增删改线程发现其他线程正在扩容,帮其扩容
常见方法:
transfer():数据转移到新表中,完成扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // n 表示扩容之前 table 数组的长度 int n = tab.length, stride; // stride 表示分配给线程任务的步长,默认就是 16 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // 如果当前线程为触发本次扩容的线程,需要做一些扩容准备工作,【协助线程不做这一步】 if (nextTab == null) { try { // 创建一个容量是之前【二倍的 table 数组】 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return; } // 把新表赋值给对象属性 nextTable,方便其他线程获取新表 nextTable = nextTab; // 记录迁移数据整体位置的一个标记,transferIndex 计数从1开始不是 0,所以这里是长度,不是长度-1 transferIndex = n; } // 新数组的长度 int nextn = nextTab.length; // 当某个桶位数据处理完毕后,将此桶位设置为 fwd 节点,其它写线程或读线程看到后,可以从中获取到新表 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 推进标记 boolean advance = true; // 完成标记 boolean finishing = false; // i 表示分配给当前线程任务,执行到的桶位 // bound 表示分配给当前线程任务的下界限制,因为是倒序迁移,16 迁移完 迁移 15,15完成去迁移14 for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; // 给当前线程【分配任务区间】 while (advance) { // 分配任务的开始下标,分配任务的结束下标 int nextIndex, nextBound; // --i 让当前线程处理下一个索引,true说明当前的迁移任务尚未完成,false说明线程已经完成或者还未分配 if (--i >= bound || finishing) advance = false; // 迁移的开始下标,小于0说明没有区间需要迁移了,设置当前线程的 i 变量为 -1 跳出循环 else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } // 逻辑到这说明还有区间需要分配,然后给当前线程分配任务, else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, // 判断区间是否还够一个步长,不够就全部分配 nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // 当前线程的结束下标 bound = nextBound; // 当前线程的开始下标,上一个线程结束的下标的下一个索引就是这个线程开始的下标 i = nextIndex - 1; // 任务分配结束,跳出循环执行迁移操作 advance = false; } } // 【分配完成,开始数据迁移操作】 // 【CASE1】:i < 0 成立表示当前线程未分配到任务,或者任务执行完了 if (i < 0 || i >= n || i + n >= nextn) { int sc; // 如果迁移完成 if (finishing) { nextTable = null; // help GC table = nextTab; // 新表赋值给当前对象 sizeCtl = (n << 1) - (n >>> 1);// 扩容阈值为 2n - n/2 = 3n/2 = 0.75*(2n) return; } // 当前线程完成了分配的任务区间,可以退出,先把 sizeCtl 赋值给 sc 保留 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 判断当前线程是不是最后一个线程,不是的话直接 return, if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; // 所以最后一个线程退出的时候,sizeCtl 的低 16 位为 1 finishing = advance = true; // 【这里表示最后一个线程需要重新检查一遍是否有漏掉的区间】 i = n; } } // 【CASE2】:当前桶位未存放数据,只需要将此处设置为 fwd 节点即可。 else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); // 【CASE3】:说明当前桶位已经迁移过了,当前线程不用再处理了,直接处理下一个桶位即可 else if ((fh = f.hash) == MOVED) advance = true; // 【CASE4】:当前桶位有数据,而且 node 节点不是 fwd 节点,说明这些数据需要迁移 else { // 【锁住头节点】 synchronized (f) { // 二次检查,防止头节点已经被修改了,因为这里才是线程安全的访问 if (tabAt(tab, i) == f) { // 【迁移数据的逻辑,和 HashMap 相似】 // ln 表示低位链表引用 // hn 表示高位链表引用 Node<K,V> ln, hn; // 哈希 > 0 表示当前桶位是链表桶位 if (fh >= 0) { // 和 HashMap 的处理方式一致,与老数组长度相与,16 是 10000 // 判断对应的 1 的位置上是 0 或 1 分成高低位链表 int runBit = fh & n; Node<K,V> lastRun = f; // 遍历链表,寻找【逆序看】最长的对应位相同的链表,看下面的图更好的理解 for (Node<K,V> p = f.next; p != null; p = p.next) { // 将当前节点的哈希 与 n int b = p.hash & n; // 如果当前值与前面节点的值 对应位 不同,则修改 runBit,把 lastRun 指向当前节点 if (b != runBit) { runBit = b; lastRun = p; } } // 判断筛选出的链表是低位的还是高位的 if (runBit == 0) { ln = lastRun; // ln 指向该链表 hn = null; // hn 为 null } // 说明 lastRun 引用的链表为高位链表,就让 hn 指向高位链表头节点 else { hn = lastRun; ln = null; } // 从头开始遍历所有的链表节点,迭代到 p == lastRun 节点跳出循环 for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) // 【头插法】,从右往左看,首先 ln 指向的是上一个节点, // 所以这次新建的节点的 next 指向上一个节点,然后更新 ln 的引用 ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } // 高低位链设置到新表中的指定位置 setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); // 老表中的该桶位设置为 fwd 节点 setTabAt(tab, i, fwd); advance = true; } // 条件成立:表示当前桶位是 红黑树结点 else if (f instanceof TreeBin) { TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null, loTail = null; TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; // 迭代 TreeBin 中的双向链表,从头结点至尾节点 for (Node<K,V> e = t.first; e != null; e = e.next) { // 迭代的当前元素的 hash int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); // 条件成立表示当前循环节点属于低位链节点 if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else //【尾插法】 loTail.next = p; // loTail 指向尾节点 loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } // 拆成的高位低位两个链,【判断是否需要需要转化为链表】,反之保持树化 ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } }链表处理的 LastRun 机制,可以减少节点的创建

image-20220923174859772 helpTransfer():帮助扩容机制
```java final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; // 数组不为空,节点是转发节点,获取转发节点指向的新表开始协助主线程扩容 if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { // 扩容标识戳 int rs = resizeStamp(tab.length); // 判断数据迁移是否完成,迁移完成会把 新表赋值给 nextTable 属性 while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; // 设置扩容线程数量 + 1 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 协助扩容 transfer(tab, nextTab); break; } } return nextTab; } return table; } ```
获取方法
ConcurrentHashMap 使用 get() 方法获取指定 key 的数据
get():获取指定数据的方法
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // 扰动运算,获取 key 的哈希值 int h = spread(key.hashCode()); // 判断当前哈希表的数组是否初始化 if ((tab = table) != null && (n = tab.length) > 0 && // 如果 table 已经初始化,进行【哈希寻址】,映射到数组对应索引处,获取该索引处的头节点 (e = tabAt(tab, (n - 1) & h)) != null) { // 对比头结点 hash 与查询 key 的 hash 是否一致 if ((eh = e.hash) == h) { // 进行值的判断,如果成功就说明当前节点就是要查询的节点,直接返回 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } // 当前槽位的【哈希值小于0】说明是红黑树节点或者是正在扩容的 fwd 节点 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; // 当前桶位是【链表】,循环遍历查找 while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }ForwardingNode#find:转移节点的查找方法
Node<K,V> find(int h, Object k) { // 获取新表的引用 outer: for (Node<K,V>[] tab = nextTable;;) { // e 表示在扩容而创建新表使用寻址算法得到的桶位头结点,n 表示为扩容而创建的新表的长度 Node<K,V> e; int n; if (k == null || tab == null || (n = tab.length) == 0 || // 在新表中重新定位 hash 对应的头结点,表示在 oldTable 中对应的桶位在迁移之前就是 null (e = tabAt(tab, (n - 1) & h)) == null) return null; for (;;) { int eh; K ek; // 【哈希相同值也相同】,表示新表当前命中桶位中的数据,即为查询想要数据 if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; // eh < 0 说明当前新表中该索引的头节点是 TreeBin 类型,或者是 FWD 类型 if (eh < 0) { // 在并发很大的情况下新扩容的表还没完成可能【再次扩容】,在此方法处再次拿到 FWD 类型 if (e instanceof ForwardingNode) { // 继续获取新的 fwd 指向的新数组的地址,递归了 tab = ((ForwardingNode<K,V>)e).nextTable; continue outer; } else // 说明此桶位为 TreeBin 节点,使用TreeBin.find 查找红黑树中相应节点。 return e.find(h, k); } // 逻辑到这说明当前桶位是链表,将当前元素指向链表的下一个元素,判断当前元素的下一个位置是否为空 if ((e = e.next) == null) // 条件成立说明迭代到链表末尾,【未找到对应的数据,返回 null】 return null; } } }
删除方法
remove():删除指定元素
public V remove(Object key) { return replaceNode(key, null, null); }replaceNode():替代指定的元素,会协助扩容,增删改(写)都会协助扩容,查询(读)操作不会,因为读操作不涉及加锁
final V replaceNode(Object key, V value, Object cv) { // 计算 key 扰动运算后的 hash int hash = spread(key.hashCode()); // 开始自旋 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 【CASE1】:table 还未初始化或者哈希寻址的数组索引处为 null,直接结束自旋,返回 null if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) break; // 【CASE2】:条件成立说明当前 table 正在扩容,【当前是个写操作,所以当前线程需要协助 table 完成扩容】 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 【CASE3】:当前桶位可能是 链表 也可能是 红黑树 else { // 保留替换之前数据引用 V oldVal = null; // 校验标记 boolean validated = false; // 【加锁当前桶位头结点】,加锁成功之后会进入代码块 synchronized (f) { // 双重检查 if (tabAt(tab, i) == f) { // 说明当前节点是链表节点 if (fh >= 0) { validated = true; //遍历所有的节点 for (Node<K,V> e = f, pred = null;;) { K ek; // hash 和值都相同,定位到了具体的节点 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 当前节点的value V ev = e.val; if (cv == null || cv == ev || (ev != null && cv.equals(ev))) { // 将当前节点的值 赋值给 oldVal 后续返回会用到 oldVal = ev; if (value != null) // 条件成立说明是替换操作 e.val = value; else if (pred != null) // 非头节点删除操作,断开链表 pred.next = e.next; else // 说明当前节点即为头结点,将桶位头节点设置为以前头节点的下一个节点 setTabAt(tab, i, e.next); } break; } pred = e; if ((e = e.next) == null) break; } } // 说明是红黑树节点 else if (f instanceof TreeBin) { validated = true; TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> r, p; if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) { V pv = p.val; if (cv == null || cv == pv || (pv != null && cv.equals(pv))) { oldVal = pv; // 条件成立说明替换操作 if (value != null) p.val = value; // 删除操作 else if (t.removeTreeNode(p)) setTabAt(tab, i, untreeify(t.first)); } } } } } // 其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象,validated 为 false,会进入下次 for 自旋 if (validated) { if (oldVal != null) { // 替换的值为 null,【说明当前是一次删除操作,更新当前元素个数计数器】 if (value == null) addCount(-1L, -1); return oldVal; } break; } } } return null; }
参考视频:https://space.bilibili.com/457326371/
JDK7原理
ConcurrentHashMap 对锁粒度进行了优化,分段锁技术,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。允许多个修改操作并发进行,Segment 是一种可重入锁,继承 ReentrantLock,并发时锁住的是每个 Segment,其他 Segment 还是可以操作的,这样不同 Segment 之间就可以实现并发,大大提高效率。
底层结构: Segment 数组 + HashEntry 数组 + 链表(数组 + 链表是 HashMap 的结构)
优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 JDK8 中是类似的
缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

CopyOnWriteArrayList
原理分析
CopyOnWriteArrayList 采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,在新数组上执行操作,不影响其它线程的并发读,读写分离
CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
存储结构:
private transient volatile Object[] array; // volatile 保证了读写线程之间的可见性全局锁:保证线程的执行安全
final transient ReentrantLock lock = new ReentrantLock();新增数据:需要加锁,创建新的数组操作
public boolean add(E e) { final ReentrantLock lock = this.lock; // 加锁,保证线程安全 lock.lock(); try { // 获取旧的数组 Object[] elements = getArray(); int len = elements.length; // 【拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)】 Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加新元素 newElements[len] = e; // 替换旧的数组,【这个操作以后,其他线程获取数组就是获取的新数组了】 setArray(newElements); return true; } finally { lock.unlock(); } }读操作:不加锁,在原数组上操作
public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }适合读多写少的应用场景
迭代器:CopyOnWriteArrayList 在返回迭代器时,创建一个内部数组当前的快照(引用),即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见
public Iterator<E> iterator() { // 获取到数组引用,整个遍历的过程该数组都不会变,一直引用的都是老数组, return new COWIterator<E>(getArray(), 0); } // 迭代器会创建一个底层array的快照,故主类的修改不影响该快照 static final class COWIterator<E> implements ListIterator<E> { // 内部数组快照 private final Object[] snapshot; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; // 数组的引用在迭代过程不会改变 snapshot = elements; } // 【不支持写操作】,因为是在快照上操作,无法同步回去 public void remove() { throw new UnsupportedOperationException(); } }
弱一致性
数据一致性就是读到最新更新的数据:
强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值
弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到

| 时间点 | 操作 |
|---|---|
| 1 | Thread-0 getArray() |
| 2 | Thread-1 getArray() |
| 3 | Thread-1 setArray(arrayCopy) |
| 4 | Thread-0 array[index] |
Thread-0 读到了脏数据
不一定弱一致性就不好
- 数据库的事务隔离级别就是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡
安全失败
在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败
快速失败:在 A 线程使用迭代器对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常
- AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化
- 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常
安全失败:采用安全失败机制的集合容器,在迭代器遍历时直接在原集合数组内容上访问,但其他线程的增删改都会新建数组进行修改,就算修改了集合底层的数组容器,迭代器依然引用着以前的数组(快照思想),所以不会出现异常
ConcurrentHashMap 不会出现并发时的迭代异常,因为在迭代过程中 CHM 的迭代器并没有判断结构的变化,迭代器还可以根据迭代的节点状态去寻找并发扩容时的新表进行迭代
ConcurrentHashMap map = new ConcurrentHashMap(); // KeyIterator Iterator iterator = map.keySet().iterator();Traverser(Node<K,V>[] tab, int size, int index, int limit) { // 引用还是原来集合的 Node 数组,所以其他线程对数据的修改是可见的 this.tab = tab; this.baseSize = size; this.baseIndex = this.index = index; this.baseLimit = limit; this.next = null; }public final boolean hasNext() { return next != null; } public final K next() { Node<K,V> p; if ((p = next) == null) throw new NoSuchElementException(); K k = p.key; lastReturned = p; // 在方法中进行下一个节点的获取,会进行槽位头节点的状态判断 advance(); return k; }
Collections
Collections类是用来操作集合的工具类,提供了集合转换成线程安全的方法:
public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
return new SynchronizedCollection<>(c);
}
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
源码:底层也是对方法进行加锁
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
SkipListMap
底层结构
跳表 SkipList 是一个有序的链表,默认升序,底层是链表加多级索引的结构。跳表可以对元素进行快速查询,类似于平衡树,是一种利用空间换时间的算法
对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 O(logn),空间复杂度 O(n)
ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全
平衡树和跳表的区别:
- 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个结构的局部进行操作
- 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能

BaseHeader 存储数据,headIndex 存储索引,纵向上所有索引都指向链表最下面的节点
成员变量
标识索引头节点位置
private static final Object BASE_HEADER = new Object();跳表的顶层索引
private transient volatile HeadIndex<K,V> head;比较器,为 null 则使用自然排序
final Comparator<? super K> comparator;Node 节点
static final class Node<K, V>{ final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 一般不会改动 key volatile Object value; // 对应的 value volatile Node<K, V> next; // 下一个节点,单向链表 }索引节点 Index,只有向下和向右的指针
static class Index<K, V>{ final Node<K, V> node; // 索引指向的节点,每个都会指向数据节点 final Index<K, V> down; // 下边level层的Index,分层索引 volatile Index<K, V> right; // 右边的Index,单向 // 在 index 本身和 succ 之间插入一个新的节点 newSucc final boolean link(Index<K, V> succ, Index<K, V> newSucc){ Node<K, V> n = node; newSucc.right = succ; // 把当前节点的右指针从 succ 改为 newSucc return n.value != null && casRight(succ, newSucc); } // 断开当前节点和 succ 节点,将当前的节点 index 设置其的 right 为 succ.right,就是把 succ 删除 final boolean unlink(Index<K, V> succ){ return node.value != null && casRight(succ, succ.right); } }头索引节点 HeadIndex
static final class HeadIndex<K,V> extends Index<K,V> { final int level; // 表示索引层级,所有的 HeadIndex 都指向同一个 Base_header 节点 HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) { super(node, down, right); this.level = level; } }
成员方法
其他方法
构造方法:
public ConcurrentSkipListMap() { this.comparator = null; // comparator 为 null,使用 key 的自然序,如字典序 initialize(); }private void initialize() { keySet = null; entrySet = null; values = null; descendingMap = null; // 初始化索引头节点,Node 的 key 为 null,value 为 BASE_HEADER 对象,下一个节点为 null // head 的分层索引 down 为 null,链表的后续索引 right 为 null,层级 level 为第 1 层 head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null), null, null, 1); }cpr:排序
// x 是比较者,y 是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回 0 static final int cpr(Comparator c, Object x, Object y) { return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y); }
添加方法
findPredecessor():寻找前置节点
从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的 key 大于要查找的 key,则头索引移到下层链表,在下层链表查找,以此反复,一直查找到没有下层的分层索引为止,返回该索引的节点。如果后续索引的节点的 key 小于要查找的 key,则在该层链表中向后查找。由于查找的 key 可能永远大于索引节点的 key,所以只能找到目标的前置索引节点。如果遇到空值索引的存在,通过 CAS 来断开索引
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) { if (key == null) throw new NullPointerException(); // don't postpone errors for (;;) { // 1.初始数据 q 是 head,r 是最顶层 h 的右 Index 节点 for (Index<K,V> q = head, r = q.right, d;;) { // 2.右索引节点不为空,则进行向下查找 if (r != null) { Node<K,V> n = r.node; K k = n.key; // 3.n.value 为 null 说明节点 n 正在删除的过程中,此时【当前线程帮其删除索引】 if (n.value == null) { // 在 index 层直接删除 r 索引节点 if (!q.unlink(r)) // 删除失败重新从 head 节点开始查找,break 一个 for 到步骤 1,又从初始值开始 break; // 删除节点 r 成功,获取新的 r 节点, r = q.right; // 回到步骤 2,还是从这层索引开始向右遍历 continue; } // 4.若参数 key > r.node.key,则继续向右遍历, continue 到步骤 2 处获取右节点 // 若参数 key < r.node.key,说明需要进入下层索引,到步骤 5 if (cpr(cmp, key, k) > 0) { q = r; r = r.right; continue; } } // 5.先让 d 指向 q 的下一层,判断是否是 null,是则说明已经到了数据层,也就是第一层 if ((d = q.down) == null) return q.node; // 6.未到数据层, 进行重新赋值向下扫描 q = d; // q 指向 d r = d.right;// r 指向 q 的后续索引节点,此时(q.key < key < r.key) } } }
image-20220923223202786 put():添加数据
public V put(K key, V value) { // 非空判断,value不能为空 if (value == null) throw new NullPointerException(); return doPut(key, value, false); }private V doPut(K key, V value, boolean onlyIfAbsent) { Node<K,V> z; // 非空判断,key 不能为空 if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; // outer 循环,【把待插入数据插入到数据层的合适的位置,并在扫描过程中处理已删除(value = null)的数据】 outer: for (;;) { //0.for (;;) //1.将 key 对应的前继节点找到, b 为前继节点,是数据层的, n 是前继节点的 next, // 若没发生条件竞争,最终 key 在 b 与 n 之间 (找到的 b 在 base_level 上) for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { // 2.n 不为 null 说明 b 不是链表的最后一个节点 if (n != null) { Object v; int c; // 3.获取 n 的右节点 Node<K,V> f = n.next; // 4.条件竞争,并发下其他线程在 b 之后插入节点或直接删除节点 n, break 到步骤 0 if (n != b.next) break; // 若节点 n 已经删除, 则调用 helpDelete 进行【帮助删除节点】 if ((v = n.value) == null) { n.helpDelete(b, f); break; } // 5.节点 b 被删除中,则 break 到步骤 0, // 【调用findPredecessor帮助删除index层的数据, node层的数据会通过helpDelete方法进行删除】 if (b.value == null || v == n) break; // 6.若 key > n.key,则进行向后扫描 // 若 key < n.key,则证明 key 应该存储在 b 和 n 之间 if ((c = cpr(cmp, key, n.key)) > 0) { b = n; n = f; continue; } // 7.key 的值和 n.key 相等,则可以直接覆盖赋值 if (c == 0) { // onlyIfAbsent 默认 false, if (onlyIfAbsent || n.casValue(v, value)) { @SuppressWarnings("unchecked") V vv = (V)v; // 返回被覆盖的值 return vv; } // cas失败,break 一层循环,返回 0 重试 break; } // else c < 0; fall through } // 8.此时的情况 b.key < key < n.key,对应流程图1中的7,创建z节点指向n z = new Node<K,V>(key, value, n); // 9.尝试把 b.next 从 n 设置成 z if (!b.casNext(n, z)) // cas失败,返回到步骤0,重试 break; // 10.break outer 后, 上面的 for 循环不会再执行, 而后执行下面的代码 break outer; } } // 【以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引】 // 随机数 int rnd = ThreadLocalRandom.nextSecondarySeed(); // 如果随机数的二进制与 10000000000000000000000000000001 进行与运算为 0 // 即随机数的二进制最高位与最末尾必须为 0,其他位无所谓,就进入该循环 // 如果随机数的二进制最高位与最末位不为 0,不增加新节点的层数 // 11.判断是否需要添加 level,32 位 if ((rnd & 0x80000001) == 0) { // 索引层 level,从 1 开始,就是最底层 int level = 1, max; // 12.判断最低位前面有几个 1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 // 【最大有30个就是 1 + 30 = 31 while (((rnd >>>= 1) & 1) != 0) ++level; // 最终会指向 z 节点,就是添加的节点 Index<K,V> idx = null; // 指向头索引节点 HeadIndex<K,V> h = head; // 13.判断level是否比当前最高索引小,图中 max 为 3 if (level <= (max = h.level)) { for (int i = 1; i <= level; ++i) // 根据层数level不断创建新增节点的上层索引,索引的后继索引留空 // 第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引,【类似头插法】 idx = new Index<K,V>(z, idx, null); // 循环以后的索引结构 // index-3 ← idx // ↓ // index-2 // ↓ // index-1 // ↓ // z-node } // 14.若 level > max,则【只增加一层 index 索引层】,3 + 1 = 4 else { level = max + 1; //创建一个 index 数组,长度是 level+1,假设 level 是 4,创建的数组长度为 5 Index<K,V>[] idxs = (Index<K,V>[])new Index<?,?>[level+1]; // index[0]的数组 slot 并没有使用,只使用 [1,level] 这些数组的 slot for (int i = 1; i <= level; ++i) idxs[i] = idx = new Index<K,V>(z, idx, null); // index-4 ← idx // ↓ // ...... // ↓ // index-1 // ↓ // z-node for (;;) { h = head; // 获取头索引的层数,3 int oldLevel = h.level; // 如果 level <= oldLevel,说明其他线程进行了 index 层增加操作,退出循环 if (level <= oldLevel) break; // 定义一个新的头索引节点 HeadIndex<K,V> newh = h; // 获取头索引的节点,就是 BASE_HEADER Node<K,V> oldbase = h.node; // 升级 baseHeader 索引,升高一级,并发下可能升高多级 for (int j = oldLevel + 1; j <= level; ++j) // 参数1:底层node,参数二:down,为以前的头节点,参数三:right,新建 newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j); // 执行完for循环之后,baseHeader 索引长这个样子,这里只升高一级 // index-4 → index-4 ← idx // ↓ ↓ // index-3 index-3 // ↓ ↓ // index-2 index-2 // ↓ ↓ // index-1 index-1 // ↓ ↓ // baseHeader → .... → z-node // cas 成功后,head 字段指向最新的 headIndex,baseHeader 的 index-4 if (casHead(h, newh)) { // h 指向最新的 index-4 节点 h = newh; // 让 idx 指向 z-node 的 index-3 节点, // 因为从 index-3 - index-1 的这些 z-node 索引节点 都没有插入到索引链表 idx = idxs[level = oldLevel]; break; } } } // 15.【把新加的索引插入索引链表中】,有上述两种情况,一种索引高度不变,另一种是高度加 1 // 要插入的是第几层的索引 splice: for (int insertionLevel = level;;) { // 获取头索引的层数,情况 1 是 3,情况 2 是 4 int j = h.level; // 【遍历 insertionLevel 层的索引,找到合适的插入位置】 for (Index<K,V> q = h, r = q.right, t = idx;;) { // 如果头索引为 null 或者新增节点索引为 null,退出插入索引的总循环 if (q == null || t == null) // 此处表示有其他线程删除了头索引或者新增节点的索引 break splice; // 头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 if (r != null) { // 获取r的节点 Node<K,V> n = r.node; // 插入的key和n.key的比较值 int c = cpr(cmp, key, n.key); // 【删除空值索引】 if (n.value == null) { if (!q.unlink(r)) break; r = q.right; continue; } // key > r.node.key,向右扫描 if (c > 0) { q = r; r = r.right; continue; } } // 执行到这里,说明 key < r.node.key,判断是否是第 j 层插入新增节点的前置索引 if (j == insertionLevel) { // 【将新索引节点 t 插入 q r 之间】 if (!q.link(r, t)) break; // 如果新增节点的值为 null,表示该节点已经被其他线程删除 if (t.node.value == null) { // 找到该节点 findNode(key); break splice; } // 插入层逐层自减,当为最底层时退出循环 if (--insertionLevel == 0) break splice; } // 其他节点随着插入节点的层数下移而下移 if (--j >= insertionLevel && j < level) t = t.down; q = q.down; r = q.right; } } } return null; }findNode()
private Node<K,V> findNode(Object key) { // 原理与doGet相同,无非是 findNode 返回节点,doGet 返回 value if ((c = cpr(cmp, key, n.key)) == 0) return n; }
获取方法
get(key):获取对应的数据
public V get(Object key) { return doGet(key); }doGet():扫描过程会对已 value = null 的元素进行删除处理
private V doGet(Object key) { if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; outer: for (;;) { // 1.找到最底层节点的前置节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 2.【如果该前置节点的链表后续节点为 null,说明不存在该节点】 if (n == null) break outer; // b → n → f Node<K,V> f = n.next; // 3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 if (n != b.next) break; // 4.如果后续节点的值为null,【需要帮助删除该节点】 if ((v = n.value) == null) { n.helpDelete(b, f); break; } // 5.如果前置节点已被其他线程删除,重新循环 if (b.value == null || v == n) break; // 6.如果要获取的key与后续节点的key相等,返回节点的value if ((c = cpr(cmp, key, n.key)) == 0) { @SuppressWarnings("unchecked") V vv = (V)v; return vv; } // 7.key < n.key,因位 key > b.key,b 和 n 相连,说明不存在该节点或者被其他线程删除了 if (c < 0) break outer; b = n; n = f; } } return null; }
删除方法
remove()
public V remove(Object key) { return doRemove(key, null); } final V doRemove(Object key, Object value) { if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; outer: for (;;) { // 1.找到最底层目标节点的前置节点,b.key < key for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 2.如果该前置节点的链表后续节点为 null,退出循环,说明不存在这个元素 if (n == null) break outer; // b → n → f Node<K,V> f = n.next; if (n != b.next) // inconsistent read break; if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } if (b.value == null || v == n) // b is deleted break; //3.key < n.key,说明被其他线程删除了,或者不存在该节点 if ((c = cpr(cmp, key, n.key)) < 0) break outer; //4.key > n.key,继续向后扫描 if (c > 0) { b = n; n = f; continue; } //5.到这里是 key = n.key,value 不为空的情况下判断 value 和 n.value 是否相等 if (value != null && !value.equals(v)) break outer; //6.【把 n 节点的 value 置空】 if (!n.casValue(v, null)) break; //7.【给 n 添加一个删除标志 mark】,mark.next = f,然后把 b.next 设置为 f,成功后n出队 if (!n.appendMarker(f) || !b.casNext(n, f)) // 对 key 对应的 index 进行删除,调用了 findPredecessor 方法 findNode(key); else { // 进行操作失败后通过 findPredecessor 中进行 index 的删除 findPredecessor(key, cmp); if (head.right == null) // 进行headIndex 对应的index 层的删除 tryReduceLevel(); } @SuppressWarnings("unchecked") V vv = (V)v; return vv; } } return null; }经过 findPredecessor() 中的 unlink() 后索引已经被删除

image-20220923223302624 appendMarker():添加删除标记节点
boolean appendMarker(Node<K,V> f) { // 通过 CAS 让 n.next 指向一个 key 为 null,value 为 this,next 为 f 的标记节点 return casNext(f, new Node<K,V>(f)); }helpDelete():将添加了删除标记的节点清除,参数是该节点的前驱和后继节点
void helpDelete(Node<K,V> b, Node<K,V> f) { // this 节点的后续节点为 f,且本身为 b 的后续节点,一般都是正确的,除非被别的线程删除 if (f == next && this == b.next) { // 如果 n 还还没有被标记 if (f == null || f.value != f) casNext(f, new Node<K,V>(f)); else // 通过 CAS,将 b 的下一个节点 n 变成 f.next,即成为图中的样式 b.casNext(this, f.next); } }tryReduceLevel():删除索引
private void tryReduceLevel() { HeadIndex<K,V> h = head; HeadIndex<K,V> d; HeadIndex<K,V> e; if (h.level > 3 && (d = (HeadIndex<K,V>)h.down) != null && (e = (HeadIndex<K,V>)d.down) != null && e.right == null && d.right == null && h.right == null && // 设置头索引 casHead(h, d) && // 重新检查 h.right != null) // 重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 casHead(d, h); }
参考文章:https://my.oschina.net/u/3768341/blog/3135659
参考视频:https://www.bilibili.com/video/BV1Er4y1P7k1
NoBlocking
非阻塞队列
并发编程中,需要用到安全的队列,实现安全队列可以使用 2 种方式:
- 加锁,这种实现方式是阻塞队列
- 使用循环 CAS 算法实现,这种方式是非阻塞队列
ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素
补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列
ConcurrentLinkedQueue 使用约定:
- 不允许 null 入列
- 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到
- 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点
- head 节点跟 tail 不一定指向头节点或尾节点,可能存在滞后性
ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点由节点元素和指向下一个节点的引用组成,组成一张链表结构的队列
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
private static class Node<E> {
volatile E item;
volatile Node<E> next;
//.....
}
构造方法
无参构造方法:
public ConcurrentLinkedQueue() { // 默认情况下 head 节点存储的元素为空,dummy 节点,tail 节点等于 head 节点 head = tail = new Node<E>(null); }有参构造方法
public ConcurrentLinkedQueue(Collection<? extends E> c) { Node<E> h = null, t = null; // 遍历节点 for (E e : c) { checkNotNull(e); Node<E> newNode = new Node<E>(e); if (h == null) h = t = newNode; else { // 单向链表 t.lazySetNext(newNode); t = newNode; } } if (h == null) h = t = new Node<E>(null); head = h; tail = t; }
入队方法
与传统的链表不同,单线程入队的工作流程:
- 将入队节点设置成当前队列尾节点的下一个节点
- 更新 tail 节点,如果 tail 节点的 next 节点不为空,则将入队节点设置成 tail 节点;如果 tail 节点的 next 节点为空,则将入队节点设置成 tail 的 next 节点,所以 tail 节点不总是尾节点,存在滞后性
public boolean offer(E e) {
checkNotNull(e);
// 创建入队节点
final Node<E> newNode = new Node<E>(e);
// 循环 CAS 直到入队成功
for (Node<E> t = tail, p = t;;) {
// p 用来表示队列的尾节点,初始情况下等于 tail 节点,q 是 p 的 next 节点
Node<E> q = p.next;
// 条件成立说明 p 是尾节点
if (q == null) {
// p 是尾节点,设置 p 节点的下一个节点为新节点
// 设置成功则 casNext 返回 true,否则返回 false,说明有其他线程更新过尾节点,继续寻找尾节点,继续 CAS
if (p.casNext(null, newNode)) {
// 首次添加时,p 等于 t,不进行尾节点更新,所以尾节点存在滞后性
if (p != t)
// 将 tail 设置成新入队的节点,设置失败表示其他线程更新了 tail 节点
casTail(t, newNode);
return true;
}
}
else if (p == q)
// 当 tail 不指向最后节点时,如果执行出列操作,可能将 tail 也移除,tail 不在链表中
// 此时需要对 tail 节点进行复位,复位到 head 节点
p = (t != (t = tail)) ? t : head;
else
// 推动 tail 尾节点往队尾移动
p = (p != t && t != (t = tail)) ? t : q;
}
}
图解入队:


当 tail 节点和尾节点的距离大于等于 1 时(每入队两次)更新 tail,可以减少 CAS 更新 tail 节点的次数,提高入队效率
线程安全问题:
- 线程 1 线程 2 同时入队,无论从哪个位置开始并发入队,都可以循环 CAS,直到入队成功,线程安全
- 线程 1 遍历,线程 2 入队,所以造成 ConcurrentLinkedQueue 的 size 是变化,需要加锁保证安全
- 线程 1 线程 2 同时出列,线程也是安全的
出队方法
出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新 head 节点
- 当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点
- 当 head 节点里没有元素时,出队操作才会更新 head 节点
批处理方式可以减少使用 CAS 更新 head 节点的消耗,从而提高出队效率
public E poll() {
restartFromHead:
for (;;) {
// p 节点表示首节点,即需要出队的节点,FIFO
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 如果 p 节点的元素不为 null,则通过 CAS 来设置 p 节点引用元素为 null,成功返回 item
if (item != null && p.casItem(item, null)) {
if (p != h)
// 对 head 进行移动
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 逻辑到这说明头节点的元素为空或头节点发生了变化,头节点被另外一个线程修改了
// 那么获取 p 节点的下一个节点,如果 p 节点的下一节点也为 null,则表明队列已经空了
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// 第一轮操作失败,下一轮继续,调回到循环前
else if (p == q)
continue restartFromHead;
// 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
else
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
// 将旧结点 h 的 next 域指向为 h,help gc
h.lazySetNext(h);
}
在更新完 head 之后,会将旧的头结点 h 的 next 域指向为 h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item 为 null 的节点)会被 GC 回收

如果这时,有一个线程来添加元素,通过 tail 获取的 next 节点则仍然是它本身,这就出现了p == q 的情况,出现该种情况之后,则会触发执行 head 的更新,将 p 节点重新指向为 head
参考文章:https://www.jianshu.com/p/231caf90f30b
成员方法
peek():会改变 head 指向,执行 peek() 方法后 head 会指向第一个具有非空元素的节点
// 获取链表的首部元素,只读取而不移除 public E peek() { restartFromHead: for (;;) { for (Node<E> h = head, p = h, q;;) { E item = p.item; if (item != null || (q = p.next) == null) { // 更改h的位置为非空元素节点 updateHead(h, p); return item; } else if (p == q) continue restartFromHead; else p = q; } } }size():用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用 size 方法到返回结果期间有可能增删元素,导致统计的元素个数不精确
public int size() { int count = 0; // first() 获取第一个具有非空元素的节点,若不存在,返回 null // succ(p) 方法获取 p 的后继节点,若 p == p.next,则返回 head // 类似遍历链表 for (Node<E> p = first(); p != null; p = succ(p)) if (p.item != null) // 最大返回Integer.MAX_VALUE if (++count == Integer.MAX_VALUE) break; return count; }remove():移除元素
public boolean remove(Object o) { // 删除的元素不能为null if (o != null) { Node<E> next, pred = null; for (Node<E> p = first(); p != null; pred = p, p = next) { boolean removed = false; E item = p.item; // 节点元素不为null if (item != null) { // 若不匹配,则获取next节点继续匹配 if (!o.equals(item)) { next = succ(p); continue; } // 若匹配,则通过 CAS 操作将对应节点元素置为 null removed = p.casItem(item, null); } // 获取删除节点的后继节点 next = succ(p); // 将被删除的节点移除队列 if (pred != null && next != null) // unlink pred.casNext(p, next); if (removed) return true; } } return false; }
练习题
单例
实现1:饿汉式
为什么加 final
破坏继承性,实现不可变性,防止子类重写方法破坏单例
如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
反序列化时会创建新的对象,不再是之前单例出来的对象,添加一个public Object readResolve()方法,来返回单例对象,如下
为什么设置为私有? 是否能防止反射创建新的实例?
私有的才能保证不能在外部随便调用,创建更多的对象
不能防止反射
这样初始化是否能保证单例对象创建时的线程安全?
静态成员变量,在类加载阶段进行初始化和赋值,可以保证线程安全
为什么提供静态方法而不是直接将 INSTANCE 设置为 public
可更好体现封装性,通过内部方法,实现懒惰的初始化
方法可以提供泛型支持
// 问题1:为什么加 final // 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例 public final class Singleton implements Serializable { // 问题3:为什么设置为私有? 是否能防止反射创建新的实例? private Singleton() {} // 问题4:这样初始化是否能保证单例对象创建时的线程安全? private static final Singleton INSTANCE = new Singleton(); // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由 public static Singleton getInstance() { return INSTANCE; } //防止反序列化破坏单例 public Object readResolve() { return INSTANCE; } }实现2:枚举饿汉式
枚举单例是如何限制实例个数的
INSTANCE; //等价于 public static final Singleton INSTANCE = new Singleton();枚举单例在创建时是否有并发问题_无,同上_
枚举单例能否被反射破坏单例_不能_
枚举单例能否被反序列化破坏单例_默认实现序列化接口,且防反序列化破坏_
枚举单例属于懒汉式还是饿汉式_饿汉式_
枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做_加成员变量、构造方法等_
enum Singleton { INSTANCE; }实现3:懒汉式
分析这里的线程安全, 并说明有什么缺点
线程安全,但synchronized耗时,每次都要上锁,效率低
public final class Singleton { private Singleton() {} private static Singleton INSTANCE = null; // 分析这里的线程安全, 并说明有什么缺点 public static synchronized Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } }实现4:双检锁懒汉式
- 解释为什么要加 volatile 解决第一次非空判断的有序性、可见性问题
- 对比实现3, 说出这样做的意义_仅在第一次并发创建实例时synchronized,效率高_
- 为什么还要在这里加为空判断, 之前不是判断过了吗_第一次创建时,避免等锁的重复创建_
public final class Singleton { private Singleton() {} // 问题1:解释为什么要加 volatile ? private static volatile Singleton INSTANCE = null; // 问题2:对比实现3, 说出这样做的意义 public static Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } synchronized (Singleton.class) { // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗 if (INSTANCE != null) { return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } }实现5:内部类
- 属于懒汉式还是饿汉式_懒汉式_
- 在创建时是否有并发问题_线程安全,静态变量类加载时完成_
public final class Singleton { private Singleton() {} // 问题1:属于懒汉式还是饿汉式 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 问题2:在创建时是否有并发问题 public static Singleton getInstance() { return LazyHolder.INSTANCE; } }

