JUC(java.util.current)由浅入深
一、线程基础知识复习
1. java.util.current的作者
- Doug Lea (道格.利)
2.JUC四大口诀
- 高内聚低耦合的前提下,封装思想
- 线程
- 操作
- 资源
- 资源类的设计
- 判断
- 操作
- 通知
- 防止虚假唤醒,wait系列方法需要注意使用while循环
- 注意标志为flag,可能是volatie
3. 为什么多线程及其重要???(面试题)
3.1 硬件方面
- 摩尔定律失效
- 摩尔定律
- 它是由英特尔创始人之一Gordon Moore(戈登.摩尔)提出来的,其内容为:
- 当价格不变时,集成电路上可容纳的元器件的数目约为每个18-24(18)个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买的电脑性能,将每隔18-24(18)个月翻一倍以上。这一定律揭示了信息技术进步的速度。
- 可是从2003年开始CPU主频已经不再翻倍,而是采用多核不是更快的主频(因为CPU已经放不下元器件了,只能采用多核来提高效率)。此时摩尔定律已经失效了。
- 在主频不再提高且核数在不断增加的情况下,想让程序更快就要用到并行和并发编程。
3.2 软件方面
- 高并发系统,异步+回调等生产需求(原版)
- 提升版
- 高并发系统的具体应用场景(如网络服务器、分布式系统)
- 异步编程模型如何配合多线程提升性能
- 用户体验方面的提升
4. 从start一个线程说起
4.1 Java线程理解以及Openjdk中的实现
首先
查看Java源码文件Thread.java,start方法中是调用了start0,到了private native void start0()就结束了
Java语言本身的底层就是c++语言
OpenJDK源码
- 网址:https://openjdk.org/
4.2 C++源码解读
jdk8u-ri-master\jdk\src\share\native\java\lang – Thread.c
java线程是通过start的方法启动执行的,主要内容在native方法start0中
OpenJDK写的JNI一般是一一对应的,Thread.java对应的就是Thread.c
start0其实就是JVM_StartThread,此时查看源代码可以看到jvm.h中找到了声明,jvm.cpp中有实现。
jdk8u-ri-master\hotspot\src\share\vm\prims – jvm.h jvm.cpp
jdk8u-ri-master\hotspot\src\share\vm\runtime – Thread.cpp
5. java多线程相关概念
5.1 进程
- 是系统进行资源分配的基本单位,每个进程都有自己的内存空间和系统资源
5.2 线程
- 一个进程中可以有多个线程,线程所使用的资源可以共享也可以独享,而线程是CPU进行调度和分派的基本单位
5.3 管程
- 管程的描述为“Monitor”,操作系统中又称“监视器”,在线程中称之为“锁”
- Java多线程中的管程(Monitor)是一种同步机制,用于协调多个线程对共享资源的访问。作为Java并发编程的核心概念,管程提供了一种结构化的方式来实现线程间的互斥和同步。
- JVM中的同步是基于进入和离开监视器Monitor对象,每个java对象都会有一个对应Monitor对象
- Monitor和java对象是一起创建和销毁的,底层是通过c++来实现的。
5.3.1 基本概念
- 管程本质上是一种抽象数据类型,它封装了共享变量和对这些变量进行操作的过程集合。在Java中,每个对象都有一个与之关联的管程,用于控制对该对象的并发访问。
5.3.2 Java中管程的实现
synchronized关键字:Java中最基本的管程实现机制。当一个线程执行synchronized代码块或方法时,它获取对象的管程锁,其他线程必须等待锁被释放才能执行同样的代码块。
synchornized(Object){ //临界区 } //或者 public synchornized void method(){ //临界区 }
Object类的wait()、notify()和notifyAll()方法:这些方法提供了线程间协作的机制。wait()使线程进入等待状态并释放锁,notify()/notifyAll()唤醒等待的线程。
Lock接口和Condition接口:Java 5引入的更灵活的锁机制,允许更精细的线程控制。
Look look = new ReentrantLock(); Condition condition = lock.newCondition(); lock.lock(); try{ // 临界区代码 condition.await(); // 类似于wait() condition.signal(); // 类似于notify() }finally{ lock.unlock(); //确保锁被释放 }
5.3.3 管程特性
- 互斥访问:保证任一时刻只有一个线程能够执行临界区代码。
- 条件同步:通过条件变量(在Java中是wait/notify机制或Condition)支持线程间的协作。
- 封装性:将共享数据和对数据的操作封装在一起,提高代码的可维护性。
- 可重入性:Java的管程是可重入的,即同一个线程可以多次获取同一个锁。
5.3.4 管程与信号量的比较
- 相比于信号量,管程具有更高的抽象级别和更好的封装性,使并发编程更加结构化和安全。而信号量更为底层和灵活,但也更容易出错
5.4 用户线程和守护线程
5.4.1 基本定义
- 用户线程:
- 也称为非守护线程,是Java应用程序的主要执行线程,如主线程(main thread)以及由它创建的子线程(默认情况下),可以理解为“自定义线程”
- 守护线程:
- 是一种在后台提供服务的线程,不会阻止JVM的退出。当所有用户线程结束时,无论守护线程是否结束,JVM都会退出。如垃圾回收
5.4.2 区别
5.4.2.1 生命周期管理
- 用户线程
- JVM会等待所有用户线程执行完毕后才会退出
- 用户线程的存在会阻止JVM的正常终止
- 当主线程结束时,如果有用户线程仍在运行,应用程序会继续执行
- 守护线程
- JVM不会等待守护线程执行完成
- 当所有用户线程执行完毕后,守护线程会被强制终止
- 守护线程不会阻止JVM的退出
5.4.2.2 创建方式
用户线程
Java中,线程默认创建为用户线程
守护线程
创建守护线程,需要在线程启动之前调用setDeamon()方法,传入true的boolean值
Thread thread2 = new Thread(() -> { for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "--" + i); } }, "守护线程"); // 设置为守护线程 thread2.setDaemon(true); // 启动用户线程 thread.start(); // 启动守护线程 thread2.start();
注意:setDeamon()方法必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常
5.4.2.3 优先级和资源分配
- 虽然从技术上讲,守护线程和用户线程的优先级机制相同,但在实际执行时:
- 用户线程通常获得更多的CPU时间和系统资源
- 守护线程的优先级往往较低,因为它们被设计为在后台执行辅助任务
5.4.2.4 应用场景
- 用户线程的应用场景:
- 执行应用程序的核心业务逻辑
- 处理用户交互
- 执行需要完整生命周期保证的任务
- 数据处理和计算任务
- 守护线程的应用场景:
- 后台支持服务,如垃圾回收器(GC)
- 定时任务调度
- 监控和维护任务
- 日志记录
- 资源清理
- 缓存维护
5.4.3 实际应用中的守护线程示例
- Java平台中的几个重要守护线程示例:
- 垃圾回收器线程(GC) - 最著名的守护线程,负责内存管理和垃圾回收
- JIT编译器线程 - 负责即时编译字节码为机器码
- Finalizer线程 - 执行对象的
finalize()
方法 - 引用处理线程 - 处理软引用、弱引用和虚引用
- 监控和管理线程 - 如JMX相关线程
5.4.4 使用守护线程的注意事项
- 资源清理问题:守护线程可能在任何时刻被终止,无法保证执行
finally
块或完成资源清理 - 不适合执行重要任务:不应将关键业务逻辑或需要可靠完成的任务放在守护线程中
- 状态继承:由守护线程创建的子线程默认也是守护线程
- 线程转换限制:线程启动后不能更改其守护状态,即不能将运行中的用户线程转换为守护线程,反之亦然
二、CompletableFuture
1. Future和Callable接口
案例演示
public class FutureTaskAndCallable { public static void main(String[] args) throws Exception { // 创建一个FutureTask对象 FutureTask<Integer> futureTask = new FutureTask<>(() -> { // 打印日志 System.out.println(Thread.currentThread().getName()+"---> call"); // 使用睡眠凸显效果 TimeUnit.SECONDS.sleep(3); // 返回结果 return 1024 ; }); // 传入future对象,并启动启动线程 new Thread(futureTask,"AA").start(); // 获取返回结果 // 如果get方法在其他线程之前,则会阻塞其他线程,因为get方法会阻塞其他线程 //System.out.println(futureTask.get()); // 打印日志,提示主线程死亡 System.out.println("main ---> over"); // 放到主线程后面 System.out.println("返回结果:-->" + futureTask.get()); } }
2. FutureTask
- 使用get()方法获取计算结果
- 问题一:会阻塞其他线程,如果结果一直在计算,则会一直阻塞其他线程。
- 问题二:如果设置超时时间,如果到了时间没有获取到结果,则会抛出异常,也会影响其他线程的执行。
package com.leon.juc.completablefuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* ClassName:FutureTaskAndCallable
* Package:com.leon.juc.completablefuture
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class FutureTaskAndCallable {
public static void main(String[] args) throws Exception {
// 创建一个FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
// 打印日志
System.out.println(Thread.currentThread().getName()+"---> call");
// 使用睡眠凸显效果
TimeUnit.SECONDS.sleep(3);
// 返回结果
return 1024 ;
});
// 传入future对象,并启动启动线程
new Thread(futureTask,"AA").start();
// 获取返回结果
// 如果get方法在其他线程之前,则会阻塞其他线程,因为get方法会阻塞其他线程
System.out.println(futureTask.get());
// 因为使用get方法会阻塞其他线程,所以使用超时设置,进行升级
// 虽然有超时设置,但是会抛出异常,会影响后续代码执行
System.out.println(futureTask.get(2,TimeUnit.SECONDS));
// 打印日志,提示主线程死亡
System.out.println("main ---> over");
// 放到主线程后面
System.out.println("返回结果:-->" + futureTask.get());
}
}
- 使用轮询
- 问题一:虽然使用轮询,但是还是要使用到get方法来获取到计算结果
- 问题二:使用了while,是一个死循环如果一直获取不到结果则一直循环,也会阻塞其他线程
package com.leon.juc.completablefuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* ClassName:FutureTaskAndCallable
* Package:com.leon.juc.completablefuture
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class FutureTaskAndCallable {
public static void main(String[] args) throws Exception {
// 创建一个FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
// 打印日志
System.out.println(Thread.currentThread().getName()+"---> call");
// 使用睡眠凸显效果
TimeUnit.SECONDS.sleep(30);
// 返回结果
return 1024 ;
});
// 传入future对象,并启动启动线程
new Thread(futureTask,"AA").start();
// 获取返回结果
// 如果get方法在其他线程之前,则会阻塞其他线程,因为get方法会阻塞其他线程
//System.out.println(futureTask.get());
// 因为使用get方法会阻塞其他线程,所以使用超时设置,进行升级
// 虽然有超时设置,但是会抛出异常,会影响后续代码执行
//System.out.println(futureTask.get(2,TimeUnit.SECONDS));
// 使用轮询,进行获取计算结果
while (true){
// 判断是否计算完成
if(futureTask.isDone()){
// 打印日志,把返回的结果输出
System.out.println("返回的结果:---> " + futureTask.get());
// 跳出循环
break;
}/*else {
System.out.println("-----------wait-----------");
}*/
}
// 打印日志,提示主线程死亡
System.out.println("main ---> over");
// 放到主线程后面
//System.out.println("返回结果:-->" + futureTask.get());
}
}
3. 对Future的改进
3.1 ComplatableFuture和CompletionStage源码分别介绍
3.1.1 类架构说明
3.1.2 接口CompletionStage
- CompletionStage代表计算过程中的某一个阶段,一阶段完成以后可能会触发另外一个阶段
- 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。比如:stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println())
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。(比如说:第一个阶段的结果收集到以后,触发第二阶段,第二个阶段处理完之后,再触发第三个阶段,第三个阶段直接返回,或者继续触发下一个阶段)
3.1.3 类CompletableFuture
- 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。
- 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持计算完成以后触发一些函数或执行某些动作。
- 它实现了Future和CompletionStage接口。
3.2 核心的四个静态方法,来创建异步操作
异步无返回值
public static CompletableFuture
runAsync(Runnable runnable) public static CompletableFuture
runAsync(Runnable runnable, Executor executor) // 异步没有返回值,没有传入线程池,使用其默认的线程池 CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> { // 打印日志 System.out.println(Thread.currentThread().getName() + "runAsync"); }); // 获取返回结果 System.out.println(completableFuture.get()); // 创建一个自定义的线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 5, 3L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 异步没有返回值,传入自定义线程池 CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> { // 打印日志 System.out.println(Thread.currentThread().getName() + "runAsync --- ThreadPoolExecutor"); }, threadPoolExecutor); // 获取返回的结果 System.out.println(completableFuture2.get()); // 关闭线程池 threadPoolExecutor.shutdown();
异步有返回值
public static CompletableFuture supplyAsync(Supplier supplier)
public static CompletableFuture supplyAsync(Supplier supplier, Executor executor)
// 创建一个自定义的线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 5, 3L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 异步有返回值,使用默认线程池 CompletableFuture<Integer> completableFuture3 = CompletableFuture.supplyAsync(() -> { // 打印日志 System.out.println(Thread.currentThread().getName() + "supplyAsync"); // 返回数据 return 1024; }); // 获取返回结果 System.out.println(completableFuture3.get()); // 异步有返回值,传入之定义线程池 CompletableFuture<Integer> completableFuture4 = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "supplyAsync --- ThreadPoolExecutor"); return 100; }, threadPoolExecutor); // 获取返回结果 System.out.println(completableFuture4.get()); // 关闭线程池 threadPoolExecutor.shutdown();
Executor参数说明
如果没有携带Executor参数,则会使用CompletableFuture默认的线程池
ForkJoinPool.commonPool()
作为异步线程池如果携带了Executor参数,则使用传入的自定义线程池
ThreadPoolExecutor
作为异步线程池。减少阻塞和轮询
从Java8开始引入CompletableFuture,它是Future的功能增强版,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
package com.leon.juc.completablefuture; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /**
ClassName:CompletableFutureTest02
Package:com.leon.juc.completablefuture
Description:
*@Author: leon
@Version: 1.0
*/
public class CompletableFutureTest02 {public static void main(String[] args) throws ExecutionException, InterruptedException {
// 异步有返回值,使用默认线程池 CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { // 打印日志 System.out.println(Thread.currentThread().getName() + "-------"); try { // 睡眠 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } // 返回数据 return 1024; }).thenApply((r) -> { // 进行下一步操作 return r + 128; }).whenComplete((r, e) -> { // 询问是否完成 System.out.println(Thread.currentThread().getName() + "------- r " + r); System.out.println(Thread.currentThread().getName() + "------- e " + e); }); // 主线程日志 System.out.println("main --- over" );
}
}
CompletableFuture的优点
异步任 务结束时,会自动回调某个对象方法
异步任务出错时,会自动回调某个对象方法
主线程设置回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
4. 案例精讲-从电商网站的比价需求
4.1 join和get方法对比
package com.leon.juc.completablefuture;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* ClassName:CompletableFutureTest03
* Package:com.leon.juc.completablefuture
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class CompletableFutureTest03 {
public static void main(String[] args) throws InterruptedException {
// 异步有返回值
CompletableFuture<Integer> exceptionally = CompletableFuture.supplyAsync(() -> {
// 打印日志
System.out.println(Thread.currentThread().getName() + "====================");
try {
// 睡眠
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 返回值
return 1024;
}).whenComplete((r, e) -> {
// 判断是否有异常
if (e == null) {
// 输出返回结果
System.out.println("======= return " + r);
}
}).exceptionally((e) -> {
// 捕获异常,输出异常
System.out.println(" exception --> " + e.getMessage());
// 返回值
return null;
});
// join和get一样会阻塞线程
System.out.println(exceptionally.join());
// 进行睡眠防止主线程过快的执行完毕,导致异步结果返回不了
//TimeUnit.SECONDS.sleep(2);
System.out.println("main --- over");
}
}
- 总结
- join和get一样也会阻塞其他线程,但与get不一样的是join不会抛出异常
4.2 大厂业务需求说明
功能—>性能
对内微服务多系统调用
比价需求的实现
package com.leon.juc.completablefuture; import lombok.Getter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /**
ClassName:CompletableFutureTest04
Package:com.leon.juc.completablefuture
Description:
*@Author: leon
@Version: 1.0
*/
public class CompletableFutureTest04 {public static void main(String[] args) {
// 存放数据的集合 List<NetMall> list = new ArrayList<>(); list.add(new NetMall("jd")); list.add(new NetMall("taobao")); list.add(new NetMall("tianmao")); list.add(new NetMall("pinduoduo")); // 开始时间 long startTime = System.currentTimeMillis(); // 调用非异步的方法 List<String> mysql = NetMall.getPrice(list, "mysql"); //结束时间 long endTime = System.currentTimeMillis(); // 打印时间结果 System.out.println("-------time: " + (endTime - startTime)); // 循环遍历集合 for (String string : mysql) { System.out.println(string); } // 开始时间 long startTime2 = System.currentTimeMillis(); // 调用异步方法 List<String> async = NetMall.getPriceAsync(list, "mysql"); // 结束时间 long endTime2 = System.currentTimeMillis(); // 打印时间结果 System.out.println("-------time: " + (endTime2 - startTime2)); // 循环遍历结果 for (String string : async) { System.out.println(string); }
}
}
class NetMall{
@Getter
private String mallName;public NetMall(String mallName) {
this.mallName = mallName;
}
public double calPrice(String productName){
try { // 模拟检索 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } // 随机生产价格 return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
// 非异步
public static ListgetPrice(List mallList, String productName){ return mallList.stream() .map((m) -> String.format(productName + "in %s price is %.2f ", m.getMallName(), m.calPrice(productName))) .collect(Collectors.toList());
}
// 异步
public static ListgetPriceAsync(List mallList, String productName){ return mallList.stream() .map((m) -> CompletableFuture.supplyAsync(() -> String.format(productName + "in %s price is %.2f", m.getMallName(), m.calPrice(productName)))) .collect(Collectors.toList()) .stream() .map(CompletableFuture::join) .collect(Collectors.toList());
}
}
5. CompletableFuture常用方法
5.1 获得结果和触发计算
获取结果
public T get()
- 此方法获取结果会阻塞其他线程
public T get(long timeout, TimeUnit unit)
- 此方法获取结果会阻塞其他线程,虽然设置了超时时间,但是会抛异常
public T getNow(T valueIfAbsent)
- 此方法是立马获取结果,如果没有得到结果,则会返回设置的默认结果
public T join()
- 此方法和get方法差不多,唯一的区别就是没有异常抛出
主动触发结果
complete
- 此方法执行会尝试打断计算,如果成功则返回设置的默认结果,如果失败则返回计算得到的结果
package com.leon.juc.completablefuture; import org.junit.Test; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /**
ClassName:CompletableFutureMothedTest
Package:com.leon.juc.completablefuture
Description:
*@Author: leon
@Version: 1.0
*/
public class CompletableFutureMethodTest {@Test
public void test() throws ExecutionException, InterruptedException, TimeoutException {CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "获取结果"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } return 1024; }); // get System.out.println(completableFuture.get()); System.out.println(completableFuture.get(1L, TimeUnit.SECONDS)); System.out.println(completableFuture.getNow(100)); // join System.out.println(completableFuture.join()); TimeUnit.SECONDS.sleep(3); System.out.println(completableFuture.complete(-100)); System.out.println(completableFuture.join()); System.out.println("main --- over");
}
}
5.2 对计算结果进行处理
- public CompletableFuture thenApply( Function fn)
- 此方法是将上一步的结果返回,加入到下一步的运算,如果发生异常则下一步不再进行
- public CompletableFuture handle( BiFunction fn)
- 此方法发生异常也可以继续往下执行,也就是说一直携带异常
@Test
public void test3(){
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "thenApply");
return 2 ;})
.handle((r,e) -> {
System.out.println(Thread.currentThread().getName()+ "-----------------1");
int i = 1 / 0;
return r+3;})
.handle((r,e) -> {
System.out.println(Thread.currentThread().getName()+"-----------------2");
return r+4;})
.handle((r,e) -> {
System.out.println(Thread.currentThread().getName()+"-----------------3");
return r+5;})
.whenComplete((r,e)-> {
System.out.println("thenApply -- " + r);
System.out.println("thenApply -- " + e);
}).exceptionally((e) -> {System.out.println("thenApply -- " + e);return null;});
}
总结
总结1
exceptionally —-> try/catch
whenComplete —-> try/finally
handle —-> try/finaly
总结2
- 如果你在handle方法中打印了e,也就是异常,后续的handle以及其他方法是不回暖被执行的。
- 原因:
- 当访问e(打印它)时,可能导致JVM对异常处理路径的特殊优化,使得后续的handle、whenComplete和exceptionally不执行
- 当不访问e时,CompletableFuture的标准异常处理机制生效,后续的handle会接收到前一个阶段的异常,并继续执行
CompletableFuture中带Async和不带的区别
- 不带,表示执行前一个任务的线程继续执行下一个任务,也就是前一个任务的线程和后一个任务的线程是同一个线程
- 带,表示执行前一个任务的线程执行完任务之后,下一个任务由线程池中其他的线程来完成,也就是前一个任务的线程和后一个任务的线程不是同一个线程
- 注意:但是不建议使用带Async的方法,这样会比较消耗性能,因为要进行上下文的切换
5.3 对计算结果进行消费
public CompletableFuture
thenAccept(Consumer action) CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "thenApply"); return 1024; }).thenAccept( (r) -> { System.out.println(Thread.currentThread().getName() + r); });
补充(任务之间的顺序执行)
public CompletableFuture
thenRun(Runnable action) - 前一个任务执行完之后,执行下一个任务,并且下一个任务不需要上一个任物的结果,没有返回值
public CompletableFuture
thenAccept(Consumer action) - 前一个任务执行完之后的结果,下个任务会接收,下一个任务需要上一个任务的结果,没有返回值
public CompletableFuture thenApply( Function fn)
- 前一个任务执行完之后的结果,下个任务会接收,下一个任务需要上一个任务的结果,有返回值
@Test public void test4(){ CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "thenApply"); return 1024; }).thenAccept( (r) -> { System.out.println(Thread.currentThread().getName() + r); }); CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName()+ "thenRun"); return 1024; }).thenRun(() -> { System.out.println("消耗型--> 无接收值,无返回值"); }); }
5.4 对计算速度进行选用
public CompletableFuture applyToEither(CompletionStage other,Function fn)
@Test public void test5() throws ExecutionException, InterruptedException { CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "applyToEither"); return 1024; }).applyToEither(CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "applyToEither 2"); return 128; }), (r) -> { return r; }); System.out.println(completableFuture.get()); }
这个方法是通过比对两个线程谁先执行完,最后返回的结果就是先执行完线程的计算结果
5.5 对计算结果进行合并
- public CompletableFuture
thenCombine(CompletionStage other, BiFunction fn)
@Test
public void test6(){
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "");
return 1024;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "");
return 1024;
}), (r1, r2) -> r1 + r2);
System.out.println(completableFuture.join());
}
- 总结
- 该方法是将两个ComplationStage任务的结果汇总到一起,先完成的任务需要等待没有完成的任务。
三、Java中的"锁"
1. 乐观锁和悲观锁
- 悲观锁
- 在多线程中,当有线程获取到锁之后,对资源进行操作。其他线程需等待,等其操作数据完毕,剩余的线程去获取锁,然后操作数据。
- 乐观锁
- 在多线程中,所有线程去操作数据,当有线程修改数据之后,会设置新的版本号,如果其他线程需要修改,则需要进行版本号判断,查看当前线程所持有的版本号是否匹配,匹配则可以进行操作数据,然后设置新的版本号,不匹配则重新获取数据,持有最新的版本号去修改数据,最后再设置新的版本号。
2. synchronized
2. 1 synchronized八锁问题
八锁演示
package com.leon.juc.eightlock; /**
ClassName:EigthLockTest
Package:com.leon.juc.eightlock
Description:
*@Author: leon
@Version: 1.0
*//*
标准访问,先打印短信还是邮件
先打印短信,再打印邮件停4秒在短信方法内,先打印短信还是邮件
先打印短信,再打印邮件新增普通的hello方法,先打印短信还是hello
先打印hello,再打印短信现在有两部手机,先打印短信还是邮件
先打印邮件,再打印短信两个静态同步方法,1部手机,先打印短信还是邮件
先打印短信,再打印邮件两个静态同步方法,2部手机,先打印短信还是邮件
先打印短信,再打印邮件一个静态同步方法,一个普通同步方法,一部手机,先打印短信还是邮件
先打印邮件,在打印短信一个静态同步方法,一个普通同步方法,二部手机,先打印短信还是邮件
先打印邮件,再打印短信
*/public class EightLockTest {
public static void main(String[] args) {
Phone phone = new Phone(); //Phone phone2 = new Phone(); new Thread(() -> { phone.sendSMS(); },"AA").start(); new Thread(() -> { phone.sendEmail(); },"AA").start();
}
}
class Phone{
public static synchronized void sendSMS(){
try { Thread.sleep(4000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("Phone ........ sendSMS");
}
public /static/ synchronized void sendEmail(){
System.out.println("Phone ........ sendEmail");
}
public void getHello(){
System.out.println("Phone ........ getHello");
}
}
总结
同步代码块
- 在实例方法中使用synchronized代码块,所持有的锁对象是当前对象。
- 在静态方法中使用synchronized代码块,所持有的锁对象是类的class对象。
同步方法
- 在实例方法中使用synchronized代码块,所持有的锁对象是当前对象。
- 在静态方法中使用synchronized代码块,所持有的锁对象是类的class对象。
2.2.synchronized底层分析
2.2.1 class文件反编译
javap -c ***.class
文件反编译- 对class文件进行反汇编
javap -v ***.class
文件反编译- 可以看到更多信息,比
-c
更详细
2.2.2 synchronized同步代码块
使用
javap -v
指令进行反编译,要到指定的class目录下进行反编译总结
为什么会出现两次monitorexit?
- 这样作是为了保证即使发生异常也能释放锁对象。
它这个一直都是一个monitorenter两个monitorexit?
- 当在代码块中抛出异常时,则处理器会执行monitorexit,所以会缺少一个正常执行的monitorexit
2.2.3 synchronized普通同步方法
使用
javap -v
反编译class文件总结
查看图中标识:flags:ACC_PUBLIC,ACC_SYCHRONIZED
如果是同步方法,则是通过flag标识来确定是否上锁,,因为标识中没有静态标识,所以锁对象是当前对象,也就是只有持有锁对象的线程才能访问临界资源。
2.2.4 synchronized静态同步方法
使用
javap -v
反编译class文件总结
查看图中标识:flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
如果是静态同步方法,则是通过flags标识来确定是否上锁,通过ACC_STATIC来标识是否是静态方法,从而确定锁对象类型,也就是类的class对象,就是只有持有锁对象的线程才可以访问临界资源。
2.3 synchronized锁是什么
- 在操作系统层面,将锁称之为管程(Motinor)
2.3.1 为什么任何一个对象都可以成为一个锁
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
ObjectMonitor.java在OpenJDk中的:jdk8u-ri-master\hotspot\agent\src\share\classes\sun\jvm\hotspot\runtime位置
ObjectMonitor.java —-> objectMonitor.cpp —-> objectMonitor.hpp
- objectMonitor.cpp和objectMonitor.hpp所对应的位置为:jdk8u-ri-master\hotspot\src\share\vm\runtime
- objectMonitor.cpp
- objectMonitor.hpp
- objectMonitor.hpp中几个关键属性
| _owner | 指向持有ObjectMonitor对象的线程 |
| ————— | ————————————- |
| WaitSet | 存放处于wait状态的线程队列 |
| EntryList | 存放处于等待锁block状态的线程队列 |
| recursions | 锁的重入次数 |
| count | 用来记录该线程获取锁的次数 |总结
因此:每个对象都会带有一个对象监视器,也就是Monitor对象
3. 公平锁和非公平锁
3.1 使用ReentrantLock卖票编码演示公平和非公现象
package com.leon.juc.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ClassName:SellTicket
* Package:com.leon.juc.lock
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class SellTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 线程一
new Thread(() -> {
while (ticket.getTicket() > 0){
//try {
// Thread.sleep(100);
//} catch (InterruptedException e) {
// throw new RuntimeException(e);
//}
ticket.sellTickets();
}
},"AA").start();
// 线程二
new Thread(() -> {
while (ticket.getTicket() > 0){
//try {
// Thread.sleep(100);
//} catch (InterruptedException e) {
// throw new RuntimeException(e);
//}
ticket.sellTickets();
}
},"BB").start();
// 线程三
new Thread(() -> {
while (ticket.getTicket() > 0){
//try {
// Thread.sleep(100);
//} catch (InterruptedException e) {
// throw new RuntimeException(e);
//}
ticket.sellTickets();
}
},"CC").start();
}
}
class Ticket {
// 设置票的数量
private int ticket = 30;
// 创建Lock对象
// 在创建ReentrantLock对象时,构造器不传入值和传入false值时,都是非公平锁
// 传入true时为公平锁
private final Lock lock = new ReentrantLock(true);
// 卖票方法
public void sellTickets(){
try {
// 加锁
lock.lock();
// 判断是否有票
if( ticket > 0 ){
// 打印输入日志
System.out.println(Thread.currentThread().getName() +" -- lock --- " + "卖出一张票,还剩"+ --ticket);
}
} finally {
// 释放锁
lock.unlock();
}
}
public int getTicket() {
return ticket;
}
}
3.2 什么是公平锁和非公平锁
即是按照线程等待时间的长短来进行判断,也就是先来的线程先执行,后来的线程后执行,有个先来后到的意思,就是FIFO,否则则视为不公平。不公平容易出现线程饿死的情况,但是效率高 。如果线程公平则下效率低,因为多了一个判断。
公平锁
非公平锁
3.3 面试题
- 为什么有公平锁和非公平锁的设计,为什么默认非公平锁?
- 恢复挂起的线程到真正锁的获取还是有时间差,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量尽量减少CPU空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,因而减少线程切换的开销
- 使用公平锁会有什么问题
- 公平锁保证了线程执行的公平性,因而每个线程都能有执行次数,这样线程切换的开销就非常大,因而效率降低。
- 什么时候使用公平锁?什么时候使用非公平锁?
- 如果需要更高的吞吐量,使用非公平锁时比较合适的,因为节省了线程切换的时间,吞吐量自然而然就上去了,如果不是为了追求更高的吞吐量则可以使用公平锁。
4. 可重入锁(又名递归锁)
- 是指在同一个线程在外层获取锁的时候,再次进入该线程的内层方法会自动获取锁(前提是,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。
- 在Java中ReentrantLock和synchronized都是可重入锁。
4.1 synchronized锁
package com.leon.juc.synchro;
/**
* ClassName:ReturnSynchronized
* Package:com.leon.juc.synchro
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class ReturnSynchronized {
public static void main(String[] args) {
Object o = new Object();
new Thread(() ->{
synchronized (o){
System.out.println(Thread.currentThread().getName() + "---这是最外层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + "---这是中间层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + "---这是内层");
}
}
}
},"AA").start();
}
}
4.2 synchronized重入的实现机制
- 与为什么任何对象都可以成为一个锁有关,也就是objectMonitor.hpp类有关
- 每个对象都拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitor时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该所对象的持有线程设置为当前线程,并且将其计数器加一。
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java从虚拟机会将其计数器加一,否则需要等待,直至持有线程释放该锁
- 当执行monitorexit时,Java虚拟机则需要将锁对象的计数器减一。计数器为零代表锁已被释放。
4.3 ReentrantLock锁
package com.leon.juc.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ClassName:ReturnLock
* Package:com.leon.juc.lock
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class ReturnLock {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> {
try {
// 加锁
lock.lock();
System.out.println(Thread.currentThread().getName() + " -- 最外层" );
try {
// 加锁
lock.lock();
System.out.println(Thread.currentThread().getName() + " -- 中间层" );
try {
// 加锁
lock.lock();
System.out.println(Thread.currentThread().getName() + " -- 内层" );
} finally {
// 释放锁
lock.unlock();
}
} finally {
// 释放锁
lock.unlock();
}
} finally {
// 释放锁
lock.unlock();
}
},"AA").start();
}
}
5. 死锁及排查
- 线程A持有锁A,需要获取锁B,线程B持有锁B,需要获取锁A。两个线程在互相等带对方锁的释放,而形成的死锁,可以理解为两个线程在争抢对方的锁资源所造成的死锁。
5.1 死锁产生的原因
- 系统资源 不足
- 进程运行推进的顺序不合适
- 资源分配不当
5.2 死锁演示
package com.leon.juc.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ClassName:DeathLock
* Package:com.leon.juc.lock
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class DeathLock {
public static void main(String[] args) {
// 锁一
Lock lock1 = new ReentrantLock();
// 锁二
Lock lock2 = new ReentrantLock();
new Thread(() -> {
try {
// 加锁
lock1.lock();
System.out.println(Thread.currentThread().getName()+ "尝试获取锁 lock2");
try {
// 加锁
lock2.lock();
System.out.println(Thread.currentThread().getName()+ "获取锁 lock2 成功");
} finally {
// 释放锁
lock2.unlock();
}
} finally {
// 释放锁
lock1.unlock();
}
},"AA").start();
new Thread(() -> {
try {
// 加锁
lock2.lock();
System.out.println(Thread.currentThread().getName()+ "尝试获取锁 lock1");
try {
// 加锁
lock1.lock();
System.out.println(Thread.currentThread().getName()+ "获取锁 lock1 成功");
} finally {
// 释放锁
lock1.unlock();
}
} finally {
// 释放锁
lock2.unlock();
}
},"BB").start();
}
}
5.3 检查死锁
使用jconsole控制台
在运行中(win+r)输入jconsole指令
找到目标类
选择连接
然后再点击不安全连接
再选择线程选项卡
最后点击检测死锁,如果死锁则显示,没有则不会显示
- jps
- 通过该指令,获取到当前对象执行的进程号
- jstack
- jvm自带的指令,通过jps查询到的进程号,查询该进程号的堆栈信息
四、LockSupport与线程中断
1. 线程中断机制
1.1 什么是中断
- 首先
- 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop、Thread.suspend、Thread.resume都已经被废弃了。
- 其次
- 在Java中没有办法立即停止一条线程,然后停止线程却显得尤为重要,如果取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断
- 中断只是一种协作机制,Java没有给中断增加任何语法,中断过程完全需要程序员自己实现。若中断一个线程,需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true,接着需要写代码不断的检查当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要写代码实现
- 每个线程对象都有一个标识,用于表示线程是否被中断,标识位为true表示中断,为false表示未中断,通过调用线程对象的interrupt方法将该线程的标识位设置true,可以在别的线程中调用,也可以在自己的线程中调用。
1.2 中断相关的API方法
| public void interrupt() | 实例方法interrupt仅仅是设置线程的中断状态未true,不会停止线程 |
| —————————————- | ———————————————————— |
| public static boolean interrupted() | 静态方法interrupted判断线程是否被中断,并清除当前中断状态。
这个方法做了两件事:
1.返回当前线程的中断状态
2.将当前线程的中断状态设置为false |
| public boolean isInterrupted() | 实例方法isInterrupted判断当前线程是否被中断(通过检查中断标志位) |
1.3 面试题
如何使用中断标停止线程?
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑
方法
通过一个volatile变量实现
private volatile boolean flag = true; @Test public void test() { new Thread(() -> { while (flag){ System.out.println("volatile---------->flag"); } System.out.println("中断成功"); }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { flag = false; }).start(); }
通过AtomicBoolean
private AtomicBoolean atomicBoolean = new AtomicBoolean(true); @Test public void test2() { new Thread(() -> { while (atomicBoolean.get()){ System.out.println("atomic---------->boolean"); } System.out.println("中断成功"); }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { atomicBoolean.set(false); }).start(); }
通过Thread类自带的中断api实现
@Test public void test3() { Thread thread = new Thread(() -> { // 检查该线程的中断标志位 while (! Thread.currentThread().isInterrupted()){ System.out.println("isInterrupted---------->true"); } // 返回当前线程的中断状态 System.out.println("中断成功"+Thread.interrupted()); }); thread.start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { // 返回当前线程的中断状态 System.out.println(Thread.interrupted()); // 进行中断协商 thread.interrupt(); }).start(); }
当前线程的中断标识位true,是不是就立刻停止?
具体来说,当对一个线程,调用interrupt()时:
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置位true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行
- 如果线程处于被阻塞的状态(例如处于:sleep,wait,join等状态),在别的线程中调用当前线程的对象interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptExecption异常,然后清除中断状态。
@Test public void test4() { Thread thread = new Thread(() -> { for (int i = 0; i < 300; i++) { System.out.println("i=" + i); } // 打印中断标志位为什么状态 System.out.println("thread调用interrupt方法之后" + Thread.currentThread().isInterrupted()); }); // 启动线程 thread.start(); // 查看默认情况下的中断标志位 System.out.println("thread默认的中断状态 ==> " + thread.isInterrupted()); try { // 睡眠一段时间,方便查看效果 TimeUnit.MILLISECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } // 进行中断协商 thread.interrupt(); // 显示中断之后的状态 System.out.println("中断之后的状态 ==> " + thread.isInterrupted()); try { // 将主线程进行睡眠 TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } // 线程死亡以后,中断标志为为false System.out.println("打印最后的中断状态" +thread.isInterrupted()); }
如果线程没有进行自我线程中断检测,即使其他线程设置中断状态为true,线程也不会中断
如果在睡眠时设置中断状态会发生什么?
@Test public void test5() { Thread thread = new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("i=" + i); if (i == 5) { /* * 如果在睡眠的时候设置了中断状态,会报错,且这时会情况中断状态,也就是设置位false * 如果想要中断的话,需要在catch块中设置中断状态 * */ try { Thread.sleep(300); } catch (InterruptedException e) { // 设置中断状态 Thread.currentThread().interrupt(); e.printStackTrace(); } //try { // wait(); //} catch (InterruptedException e) { // e.printStackTrace(); //} } } }); thread.start(); try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } // 进行中断协商 thread.interrupt(); // 返回当前线程的中断状态 System.out.println(thread.interrupted()); }
总结
- 如果在线程sleep时设置中断状态,线程会抛出异常,并且将中断状态进行清除,也就是将中断状态设置为false,如果要消除这样的效果,还需要再catch块中重新设置中断状态。
Thread.interrupted()方法
@Test public void test6() { System.out.println(Thread.currentThread().getName()+ "当前线程状态==> "+ Thread.interrupted()); //false System.out.println(Thread.currentThread().getName()+ "当前线程状态==> "+ Thread.interrupted()); //false Thread.currentThread().interrupt(); // false --> ture System.out.println(Thread.currentThread().getName()+ "当前线程状态==> "+ Thread.interrupted()); // true System.out.println(Thread.currentThread().getName()+ "当前线程状态==> "+ Thread.interrupted()); // false }
与isInterrupted()的区别
- 参数ClearInterrupted表示是否重置中断状态
- 静态方法interrupted(),返回当前线程中断状态,并重置中断状态
- 实例方法isInterrupted(),返回当前线程中断状态,不重置中断状态
2. LockSupport是什么
2.1 是什么
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
- LockSupport中的park()和unpark()
3. 线程等待唤醒机制
3.1 3种让线程等待和唤醒的方法
- 使用Object中的wait()方法让线程等待,使用Object中的notify()或notifyAll()方法唤醒线程
- 使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
- LockSupprot类可以阻塞当前线程以及唤醒指定被阻塞的线程
3.2 Object类种的wait和notify方法实现线程等待和唤醒
案例演示
@Test public void test() { new Thread( () -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (object){ for (int i = 0; i < 300; i++) { System.out.println(Thread.currentThread().getName() + "------" + i); if(i == 6){ System.out.println("====================="); try { object.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { synchronized (object){ System.out.println("唤醒线程"); // 唤醒线程 object.notify(); } }).start(); }
总结
wait()和notify()和notifyAll()需要在synchronized同步锁中才能使用,不然会报错
wait()和notify()先后顺序不能错了,否则唤醒也没有用。
3.3 Condition接口中的await和signal方法实现线程的等待和唤醒
实例演示
public static void test2() { // 创建ReentrantLock对象 Lock lock = new ReentrantLock(); // 创建Condition Condition condition = lock.newCondition(); new Thread( () -> { try { // 上锁 lock.lock(); for (int i = 0; i < 300; i++) { System.out.println(Thread.currentThread().getName() + "------" + i); if(i == 6){ System.out.println("进入等待======================="); try { // 等待 condition.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } finally { // 解锁 lock.unlock(); } }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { try { // 上锁 lock.lock(); // 唤醒 condition.signal(); System.out.println("唤醒=================="); } finally { // 释放锁 lock.unlock(); } }).start(); }
总结
Condition类需要获取锁之后采用使用,否则会报错
await()方法需要在signal()方法之前
3.4 Object和Condition使用的限制条件
- 线程先要获得并持有锁
- 必须在锁块(synchronized或lock)中
- 必须要先等待唤醒后,线程才能够被唤醒,也就是唤醒要在等待后面执行
3.5 LockSupport类中的park等待和unpark唤醒
3.5.1 是什么
- 通过park()和unpark(Thread)方法来实现阻塞和唤醒线程的操作
3.5.2 官网解释
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
- LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞线程和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认为0。
- 可以把许可看成一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
3.5.3 主要方法
- 阻塞
- park()/park(Object blocker)
- 阻塞当前线程或阻塞传入的目标线程
- permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park()方法会被唤醒,然后会将permit再次设置为零并返回。
- 唤醒
- unpark(Thread thread)
- 唤醒处于阻塞状态的指定线程
- 调用unpark(Thread)方法之后,就会将Thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒Thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
public static void test3() {
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "------" + i);
if (i == 6) {
System.out.println("进入等待=======================");
// 阻塞线程
LockSupport.park();
//LockSupport.park();
//LockSupport.park();
}
}
});
// 启动线程
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
// 唤醒线程
LockSupport.unpark(thread);
//LockSupport.unpark(thread);
//LockSupport.unpark(thread);
System.out.println("唤醒==================");
}).start();
Thread thread2 = new Thread(() -> {
for (int i = 100; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "------" + i);
if (i == 188) {
System.out.println("进入等待=======================2");
// 阻塞线程
LockSupport.park();
//LockSupport.park();
//LockSupport.park();
}
}
});
// 启动线程
thread2.start();
new Thread(() -> {
// 唤醒线程
LockSupport.unpark(thread2);
//LockSupport.unpark(thread);
//LockSupport.unpark(thread);
System.out.println("唤醒==================3");
}).start();
}
- 总结
- 一个park()对应一个unpark(),不管谁前前后都可以进行唤醒,如果多个park()则需要多个线程去唤醒,因为同一个线程中调用unpark上限为1,不管怎样调用都只能唤醒一个park()
五、Java内存模型之JMM
1.面试题
- 你知道什么是Java内存模型JMM吗?
- JMM与volatile它们两个之间的关系?
- JMM有哪些特性以及它们的三大特性是什么?
- 为什么要有JMM,它为什么出现?作用和功能是什么?
- happens-before先行发生原则你有了解过吗?
2. 计算机硬件存储体系
- 因为有多级的缓存(CPU和物理主内存的速度不一致)
- CPU的运行不是直接操作内存而是先把内存里边的数据读到缓存,因而在内存的读和写操作的时候就会造成不一致的问题
- Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出需要知道的JMM
3. Java内存模型Java Memory Model
- JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定和规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变的写入时,如何对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性、有序性展开的
- 原则
- JMM的关键技术点都是围绕者多线程的原子性、可见性和有序性展开的
- 能干嘛?
- 通过JMM来实现线程和主内存之间的抽象关系
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各中平台下都能达到一致的内存访问效果
4. JMM规范下,三大特性
- 可见性
- 是指当一个线程修改某一个共享变量的值,其他线程是否能够立即知道该变量,JMM规定了所有的变量都存储在主内存中
- Java中的普通变量的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现“脏读”,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在线程自己的工作内存中进行,而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
- 如果线程没有可见性保证
- 主内存中有变量x,初始值为0
- 线程A要将x加1,先将x=0拷贝到自己的私有内存中,然后更新x的值
- 线程A将更新的x值回刷到主内存的时间是不固定的
- 刚好线程A没有会刷x到主内存时,线程B同样从主内存中读取x,此时为0,和线程A一样的操作,最后期盼的x=2就会变成x=1
- 原子性
- 指一个操作是不可中断的,及多线程环境下,操作不能被其他线程干扰
- 有序性
- 对于一个线程的执行代码而言,我们总是习惯认为代码的执行总是从上到下,有序执行。但是为了提供性能,编译器和处理器通常会对指令序列进行重新排序。
- 指令重排可以保证串行语义一致,但是没有义务保证多线程的语义也一致,即可能产生“脏读”,简单说,两个以上不相干的代码在执行的时候可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化
- 源代码 —->编译器优化重排 —-> 指令并行重排 —-> 内存系统的重排 —-> 最终执行的指令
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
- 处理器在进行重排序时必须要考虑指令之间恶的数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保证一致性是无法确定的结果无法预测
5. JMM规范下,多线程对变量的读写过程
5.1 读取过程
- 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称之为栈空间),工作线程是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取、赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
- JMM定义了线程和主内存之间的抽象关系
- 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
- 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
5.2 总结
- 定义的所有共享变量都存储在物理内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
6. JMM规范下,多线程先行发生原则值happens-before
- 在JMM中,如果一个操作执行的结果需要对另一个操作有可见性或者代码重排序,那么这两个操作之间必须存在happens-before关系
6.1 先行发生原则说明
- 如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常繁琐,但是在编写Java并发代码的时候并没有察觉到这一点
- 我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下有一个“先行发生原则”(happens-before)的原则限制和规矩。
- 这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的非常有用手段。依赖这个原则,可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
6.2 happens-before总原则
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二操作之前
- 两个操作之间存在happens-before关系,并不意味者一定要按照happens-before原则制定的顺序来执行。如果过重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
6.3 happens-before之8条
- 次序规则:
- 可以理解为定义在前面的属性锁被赋予的值,在后面的属性赋值或者方法中可以获取到
- 锁定规则:
- 可以理解为unlock一定先行发生在,下一个lock之前,也就是说解锁一定发生在下一个加锁之前
- volatile变量规则:
- 一个volatile写操作一定发生在,volatile读操作之前,volatile写操作对读操作是可见的
- 传递规则:
- 操作A执行完之后才能到操作B,操作B执行完之后才能执行到操作C
- 线程启动规则(Thread Start Rule):
- 可以理解为线程的start方法一定发生在所有线程操作之前。
- 线程中断规则(Thread Interruption Rule):
- 对于一个线程的中断操作,一定先发生于这个线程的中断检测操作,使用interrupt()设置一个线程的中断状态,使用isInterrupted()方法检测或获取当前线程的中断状态。
- 线程终止规则(Thread Termination Rule):
- 一个线程的所有操作一定先发生于线程的终止操作,可以通过Thread::join是否结束、Thread::isAlive()的返回值等手段来进行检测线程是否终止。
- 对象终结规则(Finalizer):
- 对象的所有操作一定发生在对象的销毁之前。也可以理解为一个对象的初始化(构造函数的调用)一定发生在finalize()方法之前。
六、volatile与Java内存模型
1. 被volatile修改的变量有2大特点
1.1 两大特性
- 可见性
- 有序性
1.2 volatile的内存语义
- 当写一个volatile变量时,JMM会立刻将线程工作内中的数据刷新到主内存中
- 当读一个volatile变量时,JMM会将线程中工作内存中的数据失效,让所有线程去读取主内存中的新数据
- volatile写是将线程中的工作数据立刻刷新到主线程中,volatile读是将所有工作线程中的数据都失效,让所有线程去主线程中重新获取数据。
2. 内存屏障
2.1 是什么
- 内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,,java内存模型的重排序规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。
- 内存屏障之前的所有写操作都要回写到主内存
- 内存屏障之后的所有读操作都能获得内存屏障的所有写操作的最新结果(实现了可见性)
- 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
- 一句话:对一个volatile域的写,happens-before于任意后续对这个volatile域的读,也叫做写后读
2.2 volatile凭什么可以保证可见性和有序性???
- 为什么可以保证有序性和可见性呢?
- 通过底层的内存品璋(Memory Barriers / Fences)
2.3 JVM中提供了四类内存屏障指令
2.3.1 C++底层源码
查看JDK8中的Unsafe.class,得到三个native方法分别是:loadFence、storeFence、fullFence
查看OpenJDK中Unsafe.java,位置为:jdk8u-ri-master\jdk\src\share\classes\sun\misc
查看OpenJDK中的unsafe.cpp,位置为:jdk8u-ri-master\hotspot\src\share\vm\prims
查看OpenJDK中的orderAccess.hpp,位置为:jdk8u-ri-master\hotspot\src\share\vm\runtime
查看OpenJDK中的orderAccess_linux_x86.inline.hpp,位置为:jdk8u-ri-master\hotspot\src\os_cpu\linux_x86\vm
2.3.2 四大屏障分别是什么意思
| 屏障类型 | 指令示例 | 说明 |
| ———- | ———————— | ———————————————————— |
| LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
| StoreStore | Store1;StoreStore;Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存中 |
| LoadStore | Load1;LoadStore;Store2 | 在store2及其后的写操作执行前,保证Load1的读取操作已经读取结束 |
| StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
2.3.3 happens-before之volatile变量规则
| 第一个操作 | 第二个操作:普通写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
| ———- | —————— | ———————- | ———————- |
| 普通读写 | 可以重排 | 可以重排 | 不可以重排 |
| volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
| volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
| 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。 |
| ———————————————————— |
| 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。 |
| 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排 |
2.3.4 JMM就将内存屏障插入策略分为4种
volatile写
在每个volatile写操作之前插入一个StoreStore屏障
在每个volatile写操作之后插入一个StoreLoad屏障
总结
volatile读
在每个volatile读操作之后插入一个LoadLoad屏障
在每个volatile读操作之后插入一个LoadStore屏障
总结
3.volatile两大特性
3.1 保证可见性
说明
保证不同线程对这个变量进行操作时的可见性,就是变量一发生改变所有线程立即可见
不加volatile,没有可见性,程序有可能无法停止
加volatile,保证可见性,程序可以停止
package com.leon.juc.volatiletest; import org.junit.Test; import java.util.concurrent.TimeUnit; /** * ClassName:VolatileTest * Package:com.leon.juc.volatiletest * Description: * * @Author: leon * @Version: 1.0 */ public class VolatileTest { //static boolean flag = true; static volatile boolean flag = true ; public static void main(String[] args) { test(); } public static void test() { new Thread(() -> { while (flag) { System.out.println("运行中 ============"); } System.out.println("运行结束 ==========================="); }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { flag = false; }).start(); } }
volatile变量的读写过程
Java内存模型中定义的8种工作内存于主存之间的原子操作
read(读取) —-> load(加载)—-> use(使用) —-> assign(赋值) —-> store(存储) —-> write(写入) —-> lock(锁定) —-> unlock(解锁)
图解
- read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
- load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
- use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
- assign:作用于工作内存,将从执行引擎收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
- store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
- write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
- 由于上述只能保证单条指令的原子性,针对多条指令的组合原子性无法保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
- lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
- unlock:作用于主内存,把一个处于锁状态的变量释放,然后才能被其他线程占用
3.2 没有原子性
3.2.1 volatile变量的复合操作(如i++)不具有原子性
- 代码演示
static volatile int sum = 0 ;
public static void add(){
sum++;
}
public static void main(String[] args) {
test2();
}
public static void test2() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
System.out.println("sum = " + sum);
}
从i++的字节码角度说明
sum++被拆分成3个指令,getstati获取原始sum,执行iadd进行加1操作,执行putstatic写把累加后的值写回
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响
public static void add(){ sum++; //不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分成3步完成 }
如果第二个线程在第一个线程读取旧值和写回新值期间读取sum的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同的加1操作,这也就造成了线程安全失败,因而对于add方法必须使用synchronized修饰,以便保证线程安全
不保证原子性
多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存中变量不同步,进而导致数据不一致对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值最新的,也就是数据加载时是最新的。
由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步
读取赋值一个普通变量的情况
当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套操作
既然一修改就是可见,为什么还不能保证原子性?
volatile主要对其中部分指令做了处理
- 要use(使用)一个变量的时候必须load(载入),要载入的时候必须从主内存read(读取)这样就解决了读的可见性
- 写操作是把assign和store做了关联(在assign(赋值)后必需store(存储))。store(存储)后write(写入)。也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存
- 就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性
3.2.2 总结
读取赋值一个volatile变量的情况
read-load-use和assign-store-write成为了两个不可分割的原子操作,但是在use和assign之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次。但是无论在哪一个时间点主内存的变量的值都是相等的,这个特性就导致了volatile变量不合适参与到依赖当前值的运算,如i = i + 1 ;i++;之类的那么依靠可见性的特点volatile可以用在什么地方呢?通常volatile用做保存某个状态的Boolean值或者int值
《深入理解Java虚拟机》提到:
3.2.3 JVM的字节码,i++分成三步,间隙期不同步非原子操作(i++)
3.3 指令禁重排(保证有序性)
3.3.1 说明与案例
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序,存在数据依赖关系,禁止重排序。但是重排序后的指令绝不能改变原有的串行语义!这点在并发设计中必须要中重点考虑
重排序的分类和执行流程
源代码 —-> 1:编译器优化重排序 —-> 2:指令级并行重排序 —-> 3:内存系统重排序 —-> 4:最终执行的指令序列
编译器优化的重排序:
- 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
指令级并行的重排序:
- 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序:
- 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
数据依赖:
- 若两个操作访问同一个变量,且这两个操作中有一个为写操作,此时两个操作就存在数据依赖性
不存在依赖关系,可以重排序 ====> 不影响最终结果
| 重排前 | 重排后 |
| ———————————————————- | ———————————————————- |
| int a = 1 // 1
int b = 20 // 2
int c = a+b // 3 | int b = 20 // 2
int a = 1 // 1
int c = a+b // 3 |
| 结论:编译器调整了语句顺序,但是不影响程序的最终结果 | 重排序可以 |存在依赖关系,禁止重排序 ====> 重排序发生,会导致程序运行结果不同
编译器和处理器在重排序时,会遵循数据依赖性,不会改变存在依赖关系的两个操作的执行,但是不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变
3.3.2 volatile的底层实现是通过内存屏障
volatile有关的禁止指令重排的行为
| 第一个操作 | 第二个操作:普通写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
| ———- | —————— | ———————- | ———————- |
| 普通读写 | 可以重排 | 可以重排 | 不可以重排 |
| volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
| volatile写 | 可以重排 | 不可以重排 | 不可以重排 || 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。 |
| ———————————————————— |
| 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。 |
| 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排 |四大屏障的插入情况
在每一个volatile写操作前面插入一个StoreStore屏障,StoreSotre屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中
在每一个volatile写操作后面插入一个StoreLoad屏障,StoreLoad屏障的作用时避免volatile写与后面可能有的volatile读/写操作重排序
在每一个volatile读操作后面插入一个LoadLoad屏障,LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
在每一个volatile读操作后面插入一个LoadStore屏障,LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
案例演示
package com.leon.juc.volatiletest; /**
ClassName:VolatileTest2
Package:com.leon.juc.volatiletest
Description:
*@Author: leon
@Version: 1.0
*/
public class VolatileTest2 {int i = 0 ;
volatile boolean flag = true ;
public void write(){
i = 2 ; flag = true ;
}
public void read(){
if(flag){ System.out.println("i ===============" + i); }
}
}
在每一个volatile写操作前面插入一个StoreStore屏障 在每一个volatile写操作后面插入一个StoreLoad屏障
在每一个volatile读操作后面插入一个LoadLoad屏障 在每一个volatile读操作后面插入一个LoadStore屏障
4. 如何正确使用volatile
单一赋值可以,但是含复合运算赋值不可以(i++之类)
volatile int a = 0
volatile boolean falg = false
状态标志,判断业务是否结束
package com.leon.juc.volatiletest; import java.util.concurrent.TimeUnit; /**
ClassName:VolatileTest3
Package:com.leon.juc.volatiletest
Description:
*@Author: leon
@Version: 1.0
*/
public class VolatileTest3 {static volatile boolean flag = true ;
public static void product(){
new Thread(() -> { // 通过标志位来决定业务是否能继续执行 while (flag){ // 业务处理 System.out.println("进行业务操作"); } }).start(); try { // 模拟业务时间 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { // 结束业务 flag = false; }).start();
}
}
开销较低的读,写锁策略
package com.leon.juc.volatiletest; /**
ClassName:VolatileTest4
Package:com.leon.juc.volatiletest
Description:
使用:当读多于写,结合使用内部锁和volatile变量来减少同步的开销
理由:利用volatile保证读取操作的可见性;利用synchronized保证符合操作的原子性
*
@Author: leon
@Version: 1.0
*/
public class VolatileTest4 {private volatile int value ;
public int getValue(){
return value; // 利用volatile保证读取操作的可见性
}
public synchronized void setValue(){
value++; // 利用synchronized来保证复合操作的原子性
}
}
DCL双端锁的发布
问题
package com.leon.juc.volatiletest; /** * ClassName:SingletonObject * Package:com.leon.juc.volatiletest * Description: * * @Author: leon * @Version: 1.0 */ public class SingletonObject { private static SingletonObject singletonObject; private SingletonObject() {} public static SingletonObject getInstance() { // 判断对象是否为空 if(singletonObject == null){ // 加锁 synchronized (SingletonObject.class) { // 判断对象是否为空 if(singletonObject == null){ // 创建对象实例 // 隐患:多线程环境不下,由于重排序,该对象可能还未完成初始化就被其他线程读取 singletonObject = new SingletonObject(); } } } // 对象创建完毕,将对象返回 return singletonObject; } }
单线程看问题代码
单线程环境下(或者正常情况下),在“问题代码处”,会执行如下操作,保证能获取到已完成初始化的实例
多线程看问题代码
隐患:多线程环境下,在“问题代码处”,会执行如下操作重排序导致2,3乱序,后果就是其他线程得到的是一个没有完成初始化的对象,而不是完成初始化的对象
在这种情况下,对象创建成功了,但是没有进行初始化,因为第二步初始化被重排序到第三步去了。也就是对象不是null,但是里面的成员都是默认值,没有初始化
解决方式:加上volatile关键字
package com.leon.juc.volatiletest; /** * ClassName:SingletonObject * Package:com.leon.juc.volatiletest * Description: * * @Author: leon * @Version: 1.0 */ public class SingletonObject { private static volatile SingletonObject singletonObject; private SingletonObject() {} public static SingletonObject getInstance() { // 判断对象是否为空 if(singletonObject == null){ // 加锁 synchronized (SingletonObject.class) { // 判断对象是否为空 if(singletonObject == null){ // 创建对象实例 // 隐患:多线程环境不下,由于重排序,该对象可能还未完成初始化就被其他线程读取 singletonObject = new SingletonObject(); } } } // 对象创建完毕,将对象返回 return singletonObject; } }
解决问题二:采用静态内部类的方式实现
package com.leon.juc.volatiletest; /** * ClassName:SingletonObject2 * Package:com.leon.juc.volatiletest * Description: * * @Author: leon * @Version: 1.0 */ public class SingletonObject2 { private SingletonObject2() {} private static class Singleton { private static SingletonObject2 singletonObject2 = new SingletonObject2(); } public static SingletonObject2 getInstance() { return Singleton.singletonObject2; } }
5. 总结
内存屏障是什么
是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也叫内存栅栏或栅栏指令
内存屏障能干嘛
阻止屏障两边的指令重排
写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
内存屏障四大指令
在每一个volatile写操作前面插入一个StoreStore屏障
- 禁止重排序:一定是Store1的数据写出到主内存完成后,才能让Store2及其之后的写出操作的数据,被其它线程看到
- 保证Store1指令写出去的数据,会强制被刷新回到主内存中
在每一个volatile写操作后面插入一个StoreLoad屏障
- 禁止重排序:一定是Store1的数据写出到主内存完成后,才能让Load2来读取数据
- 同时保证:强制把写缓冲区的数据刷回到主内存中,让工作内存/CPU高速缓存当中缓存的数据失效,重新到主内存中获取新的数据
在每一个volatile读操作后面插入一个LoadLoad屏障
- 禁止重排序:访问Load2的读取操作一定不会重排到Load1之前
- 保证Load2在读取的时候,自己缓存内相应的数据失效,Load2会区主内存中获取新的数据
在每一个volatile读操作后面插入一个LoadStore屏障
- 禁止重排序:一定时Load1读取数据完成后,才能让Store2及其之后的写去操作数据,被其他线程看到
凭什么Java写了一个volatile关键字系统底层加入内存屏障?两者关系怎么勾搭上的?
字节码层面
关键字
- 它影响的是Class内的Field的flags:添加了一个ACC_VOLATILE
- JVM在把字节码生成为机器码的时候,发现操作是volatile的变量的话,就会根据JMM要求,在相应的位置去插入内存屏障指令
volatile可见性
对一个volatile修饰的变量进行读操作的话,总能够读到这个变量的最新的值,也就是这个变量最后被修改的值
一个线程修改了volatile修饰的变量的值的时候,那么这个变量的新的值,会立即刷新回主内存中
一个线程去读取volatile修饰的变量的值的时候,该变量在工作内存中的数据无效,需要重新到主内存去读取最新的数据
volatile禁止重排(有序性)
写指令
读指令
对比java.util.concurrent.locks.Lock来理解
cpu执行机器码指令的时候,是使用lock前缀指令来实现volatile的功能的
Lock指令,相当于内存屏障,功能也类似内存屏障的功能:
- 首先对总线/缓存加锁,然后去执行后面的指令,最后,释放锁,同时把高速缓存的数据刷新回到主内存
- 在lock锁住总线/缓存的时候,其它CPU的读写请求就会被阻塞,直到锁释放。lock过后的写操作,会让它CPU的高速缓存中相应的数据失效,这样后续这些CPU在读取数据的时候,就会从主内存去加载最新的数据
- 加了Lock指令过后的具体表现,就跟JMM添加内存屏障后一样。
一句话总结
volatile写之前的操作,都禁止重排序到volatile之后
volatile读之后的操作,都禁止重排序到volatile之后
volatile写之后volatile读,禁止重排序
七、CAS(compare and swap)
1. 没有CAS之前
1.1 多线程环境不使用原子类保证线程安全(基本数据类型)
class BaseDataType {
private volatile int number;
public int getNumber() {
return number;
}
public synchronized void add() {
number++;
}
}
1.2 多线程环境使用原子类保证线程安全(基本数据类型)
class AtomDateType{
private AtomicInteger atomicInteger = new AtomicInteger();
public int getNumber() {
return atomicInteger.get();
}
public void add(){
atomicInteger.getAndIncrement();
}
}
2. 是什么
2.1 说明
- CAS
- compare and swap的缩写,中文翻译成比较交换,实现并发算法时常用到的一种技术。它包含三个操作数—-内存位置、预期原值及更新值。
- 如执行CAS操作的时候,将内存位置的值与预期原值比较
- 如果相匹配,那么处理器会自动化将该位置值更新为新值
- 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
2.2 原理
CAS
CAS有操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
2.3 硬件级别保证
- CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身原子性,也就是说CAS效率更高且通过硬件保证,说明CAS更可靠
- CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现几位CPU指令cmpxchg
- 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性实际上是CPU实现的,其实在这一点上还是有排他锁的,只是比起用synchronized,这里的排他时间要短的多,所以在多线程情况下性能会比较好
2.4 代码示例
package com.leon.juc.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* ClassName:CASTest2
* Package:com.leon.juc.cas
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class CASTest2 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,100)+ "最新值为:" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,200) + "最新值为:" + atomicInteger.get());
}
}
2.5 源码分析compareAndSet()
- var1:表示要操作的对象
- var2:表示要操作对象中属性地址的偏移量
- var4:表示需要修改数据的期望值
- var5/var6:表示需要修改什么值
3. CAS底层原理?如果知道,谈谈你对Unsafe的理解
3.1 Unsafe
Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法
注意:Unsafe类中的所有方法都native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源相应任务。
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
变量value用volatile修饰,保证了多线程之间的内存可见性。
3.2 i++线程不安全,那getAndIncrement()
CAS的全称为Compare-And-Swap,它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
AtomicInteger类主要利用CAS(compare and swap)+ volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
3.3 源码分析
OpenJDK中的Unsafe.java,位置为:jdk8u-ri-master\jdk\src\share\classes\sun\misc
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同的CPU上)
AtomicInteger里面的value原始值为3,即主内存中AtomicInterger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。
线程B也通过getIntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法,比较内存值也为3,成功修改内存值为4,线程B执行完成。
这时线程A恢复,执行getIntVolatile(var1,var2)方法比较,发现所持有的数据和主内存的数据不一致,说明该值被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重来一遍。
线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
3.4 底层汇编
native修饰的方法代表是底层方法
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中,位置为:jdk8u-ri-master\hotspot\src\share\vm\prims
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
cmpxchg
在不同的操作系统下会调用不同的cmpxchg重载函数
总结
CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性,实现方式是基于硬件平台的汇编指令,在intel的CPU中(x86机器上),使用的是汇编指令cmpxchg指令。
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来
4. 原子引用
AtomicReference示例演示
package com.leon.juc.cas; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicReference; /**
ClassName:AtomicRefercenceTest
Package:com.leon.juc.cas
Description:
*@Author: leon
@Version: 1.0
*/
public class AtomicReferenceTest {public static void main(String[] args) {
User user = new User("zs",12); User user2 = new User("ls",21); AtomicReference<User> atomicReference = new AtomicReference<>(user); System.out.println(atomicReference.compareAndSet(user,new User("zs",24)) + "最新值 ===> " + atomicReference.get()); System.out.println(atomicReference.compareAndSet(user,user2) + "最新值 ===> " + atomicReference.get());
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class User{private String name;
private int age;
}
5. 自旋锁,借鉴CAS思想
5.1 是什么
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
5.2 实现自旋锁
package com.leon.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* ClassName:AtomicReferenceTest2
* Package:com.leon.juc.cas
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class AtomicReferenceTest2 {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
System.out.println(Thread.currentThread().getName()+"尝试获取锁==============");
while (!atomicReference.compareAndSet(null,Thread.currentThread())){
}
System.out.println(Thread.currentThread().getName() + "尝试获取锁==============成功");
}
public void unlock(){
System.out.println(Thread.currentThread().getName() + "释放取锁==============");
atomicReference.compareAndSet(Thread.currentThread(),null);
System.out.println(Thread.currentThread().getName() + "释放取锁==============成功");
}
public static void main(String[] args) {
AtomicReferenceTest2 atomicReferenceTest2 = new AtomicReferenceTest2();
new Thread(() -> {
try {
atomicReferenceTest2.lock();
for (int i = 0; i < 100; i++) {
System.out.println("i = " + i);
}
} finally {
atomicReferenceTest2.unlock();
}
},"AA").start();
//try {
// TimeUnit.SECONDS.sleep(3);
//} catch (InterruptedException e) {
// throw new RuntimeException(e);
//}
new Thread(() -> {
atomicReferenceTest2.lock();
atomicReferenceTest2.unlock();
},"BB").start();
}
}
6. CAS缺点
6.1 循环时间长开销很大
因为线程是通过自旋的方式来进行比对,如果线程一直无法成功的比对,则会造成线程一直在循环
而线程一直在循环,会造成CPU空转,因此CPU的性能会因此下降
6.2 ABA问题
6.2.1 ABA问题是如何产生的
- ABA问题的产生
- 首先共享变量的值为A
- 线程A获取到执行权,然后通过compareAndSet方法将A设置成了B,然后释放CPU的执行权
- 线程A又抢到执行权,然后通过compareAndSet方法将B设置成A,然后释放CPU的执行权
- 线程B抢到执行权,然后通过conmpareAndSet方法就将A设置C,然后释放CPU的执行权
6.2.2 解决方式——版本号时间戳原子引用
AtomicStampedReference
package com.leon.juc.cas; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; /**
ClassName:AtomicStampedReferenceTest
Package:com.leon.juc.cas
Description:
*@Author: leon
@Version: 1.0
*/
public class AtomicStampedReferenceTest {public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(100); // 解决方案 - AtomicStampedReference AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); // ABA问题复现 new Thread(() -> { int stamp = atomicStampedReference.getStamp(); // 打印获取版本号 System.out.println(Thread.currentThread().getName() + "版本号为:" + stamp); try { // 等待线程二,获取版本号 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } // 设置数据 System.out.println(Thread.currentThread().getName() + "--" + atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1) + "版本号为:====> " + stamp + " 数据为 ====>" + atomicStampedReference.getReference()); System.out.println("第一次操作之后的版本号 ====> " + atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName() + "--" + atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1) + "版本号为:====> " + atomicStampedReference.getStamp() + " 数据为 ====>" + atomicStampedReference.getReference()); //System.out.println(atomicInteger.compareAndSet(100, 101) + " 数据修改成功,数据为===> " + atomicInteger.get()); // //System.out.println(atomicInteger.compareAndSet(101, 100) + " 数据修改成功,数据为===> " + atomicInteger.get()); }, "AA").start(); new Thread(() -> { // 获取版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + " 版本号为:" + stamp); try { // 让线程一执行成功,也就是修改版本号 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "--" + atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1) + "版本号为:====> " + stamp + " 数据为 ====>" + atomicStampedReference.getReference()); //System.out.println(atomicInteger.compareAndSet(100, 200) + " 数据修改成功,数据为===> " + atomicInteger.get()); }, "BB").start();
}
}
八、原子操作类
1. 是什么
java.util.current.atomic包下的16了原子操作类
atomic
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFiledUpdater
AtomicLong
AtomicLongArray
AtomicLongFiledUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFiledUpdater
AtomicStampedReference
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
2. 分类
2.1 基本类型原子类
原子类
AtomicInteger
AtomicBoolean
AtomicLong
常用API简介
public final int get() —- 获取当前的值
public final int getAndSet(int newValue) —- 获取当前的值,并设置新的值
public final int getAndIncrement() —- 获取当前的值,并自增
public final int getAndDecrement() —- 获取当前的值,并自减
public final int getAndAdd(int delta) —- 获取当前的值,并加上预期的值
public boolean compareAndSet(int expect, int update) —- 如果输入的数值等于预期值,则以原子的方式将新值设置进去
代码示例
package com.leon.juc.cas; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; /**
ClassName:BeasAtomicObject
Package:com.leon.juc.cas
Description:
*@Author: leon
@Version: 1.0
*/
public class BeasAtomicObject {public static void main(String[] args) {
// 创建integer基本类型原子类 AtomicInteger atomicInteger = new AtomicInteger(); // 创建减少计数对象 CountDownLatch countDownLatch = new CountDownLatch(10); for (int i = 1; i <= 10; i++) { new Thread(() -> { try { for (int j = 1; j <= 1000; j++) { // 进行累加 atomicInteger.getAndIncrement(); } } finally { // 进行减数 countDownLatch.countDown(); } }).start(); } try { // 等待结果 countDownLatch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } // 输出日志 System.out.println("计算结果为: ====> " + atomicInteger.get());
}
}
2.2 数组类型原子类
原子类
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
代码示例
package com.leon.juc.cas; import java.util.concurrent.atomic.AtomicIntegerArray; /**
ClassName:ArrayAtomicObject
Package:com.leon.juc.cas
Description:
*@Author: leon
@Version: 1.0
*/
public class ArrayAtomicObject {public static void main(String[] args) {
AtomicIntegerArray array = new AtomicIntegerArray(10); array.set(0,2010); array.set(1,2011); array.set(2,2012); array.set(3,2013); array.set(4,2014); array.set(5,2015); array.set(6,2016); array.set(7,2017); array.set(8,2018); array.set(9,2019); for (int i = 0; i < array.length(); i++) { System.out.println(array.get(i)); } System.out.println(array.getAndIncrement(9) + "====" + array.get(9));
}
}
2.3 引用类型原子类
AtomicReference
代码示例
package com.leon.juc.cas; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicReference; /** * ClassName:AtomicRefercenceTest * Package:com.leon.juc.cas * Description: * * @Author: leon * @Version: 1.0 */ public class AtomicReferenceTest { public static void main(String[] args) { User user = new User("zs",12); User user2 = new User("ls",21); AtomicReference<User> atomicReference = new AtomicReference<>(user); System.out.println(atomicReference.compareAndSet(user,new User("zs",24)) + "最新值 ===> " + atomicReference.get()); System.out.println(atomicReference.compareAndSet(user,user2) + "最新值 ===> " + atomicReference.get()); } } @Data @AllArgsConstructor @NoArgsConstructor class User{ private String name; private int age; }
AtomicStampedReference
通过携带版本号的引用类型原子类,可以解决ABA,是用来解决修改过几次
状态戳原子引用
package com.leon.juc.cas; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; /** * ClassName:AtomicStampedReferenceTest * Package:com.leon.juc.cas * Description: * * @Author: leon * @Version: 1.0 */ public class AtomicStampedReferenceTest { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(100); // 解决方案 - AtomicStampedReference AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); // ABA问题复现 new Thread(() -> { int stamp = atomicStampedReference.getStamp(); // 打印获取版本号 System.out.println(Thread.currentThread().getName() + "版本号为:" + stamp); try { // 等待线程二,获取版本号 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } // 设置数据 System.out.println(Thread.currentThread().getName() + "--" + atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1) + "版本号为:====> " + stamp + " 数据为 ====>" + atomicStampedReference.getReference()); System.out.println("第一次操作之后的版本号 ====> " + atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName() + "--" + atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1) + "版本号为:====> " + atomicStampedReference.getStamp() + " 数据为 ====>" + atomicStampedReference.getReference()); //System.out.println(atomicInteger.compareAndSet(100, 101) + " 数据修改成功,数据为===> " + atomicInteger.get()); // //System.out.println(atomicInteger.compareAndSet(101, 100) + " 数据修改成功,数据为===> " + atomicInteger.get()); }, "AA").start(); new Thread(() -> { // 获取版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + " 版本号为:" + stamp); try { // 让线程一执行成功,也就是修改版本号 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "--" + atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1) + "版本号为:====> " + stamp + " 数据为 ====>" + atomicStampedReference.getReference()); //System.out.println(atomicInteger.compareAndSet(100, 200) + " 数据修改成功,数据为===> " + atomicInteger.get()); }, "BB").start(); } }
AtomicMarkableReference
原子更新带有标记位的引用类型对象,它的定义就是将状态戳简化为true和false,用于解决是否修改过
状态戳(true/false)原子引用
package com.leon.juc.cas; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicMarkableReference; /** * ClassName:AtomicMarkableReferenceTest * Package:com.leon.juc.cas * Description: * * @Author: leon * @Version: 1.0 */ public class AtomicMarkableReferenceTest { public static void main(String[] args) { AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(100,false); new Thread(() -> { // 获取是否修改标识 boolean mark = atomicMarkableReference.isMarked(); System.out.println(Thread.currentThread().getName() + " 尝试去修改----修改状态为 " + mark); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } // 进行修改 System.out.println(" 修改 " + atomicMarkableReference.compareAndSet(100, 101, mark, !mark) ); }).start(); new Thread(() -> { // 获取是否修改标识 boolean mark = atomicMarkableReference.isMarked(); System.out.println(Thread.currentThread().getName() + " 尝试去修改----修改状态为 " + mark); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } // 进行修改 System.out.println(" 修改 " + atomicMarkableReference.compareAndSet(101, 200, mark, !mark) ); }).start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println( " 最后结果 "+ atomicMarkableReference.getReference()); System.out.println( " 最后状态 "+ atomicMarkableReference.isMarked()); } }
2.4 对象的属性修改原子类
原子类
AtomicIntegerFiledUpdater
- 原子更新对象中int类型的字段的值
AtomicLongFiledUpdater
- 原子更新对象中long类型的字段的值
AtomicReferenceFiledUpdater
- 原子跟新引用类型字段的值
使用目的
以线程安全的方式操作非线程安全对象内的某些字段
使用要求
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
代码示例
AtomicIntegerFieldUpdater
package com.leon.juc.cas; import lombok.Data; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** * ClassName:AtomicIntegerFiledUpdaterTest * Package:com.leon.juc.cas * Description: * * @Author: leon * @Version: 1.0 */ public class AtomicIntegerFieldUpdaterTest { public static void main(String[] args) { Account account = new Account(); new Thread(() -> { for (int i = 0; i < 10; i++) { account.incr(account,100); } }).start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(account.getMoney()); } } @Data class Account{ private String name; private String account; private volatile int money; private AtomicIntegerFieldUpdater<Account> atomicIntegerFiledUpdater = AtomicIntegerFieldUpdater.newUpdater(Account.class,"money"); public void incr(Account account,int amount){ atomicIntegerFiledUpdater.getAndAdd(account,amount); } }
AtomicReferenceFieldUpdater
package com.leon.juc.cas; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; /** * ClassName:AtomicReferenceFieldUpdater * Package:com.leon.juc.cas * Description: * * @Author: leon * @Version: 1.0 */ public class AtomicReferenceFieldUpdaterTest { public static void main(String[] args) { MyObject myObject = new MyObject(); for (int i = 0; i < 10; i++) { new Thread(() -> { myObject.init(); }).start(); } } } class MyObject{ private volatile Boolean intialized = Boolean.FALSE; private AtomicReferenceFieldUpdater<MyObject,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyObject.class,Boolean.class,"intialized"); public void init(){ // 判断是否被初始化过 if(atomicReferenceFieldUpdater.compareAndSet(this,Boolean.FALSE,Boolean.TRUE)){ System.out.println("开始初始化 ======================================="); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("初始化完成 ======================================="); }else{ System.out.println("初始化失败,已被初始化或正在初始化"); } } }
2.5 原子操作增强类原理深度解析
2.5.1 原子类
- DoubelAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
2.5.2 点赞计数器
常用API
| 方法名 | 说明 |
| ————————– | ———————————————————— |
| public void add(long x) | 将当前的value加x |
| public void increment() | 将当前的value加1 |
| public void decrement() | 将当前的value减1 |
| public long sum() | 返回当前值。特别注意,在没有并发更新value情况下,sum会返回一个精确值,在存在并发的情况下,sum不保证返回精确值。 |
| public void reset() | 将value重置为0,可用于代替重新new一个Long Adder,但此方法只可以在没有并发更新的情况下使用。 |
| public long sumThenReset() | 获取当前value,并将value重置为为0 |LongAdder —- 只能用来做加法
public static void longAdder(){
LongAdder adder = new LongAdder();
//adder.increment();
//adder.increment();
//adder.increment();
adder.add(1);
adder.add(2);
adder.add(3);
System.out.println(adder.longValue());
}
- LongAccumulator —- 提供自定义的运算
public static void longAccumulator(){
LongAccumulator accumulator = new LongAccumulator((x,y) -> x-y,100);
accumulator.accumulate(1);
accumulator.accumulate(2);
accumulator.accumulate(3);
System.out.println(accumulator.longValue());
}
性能对比
package com.leon.juc.cas; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAccumulator; import java.util.concurrent.atomic.LongAdder; /**
ClassName:LongAdderTest
Package:com.leon.juc.cas
Description:
*@Author: leon
@Version: 1.0
*/
public class LongAdderTest {public static void main(String[] args) {
CompareSpeed compareSpeed = new CompareSpeed(); CountDownLatch latch = new CountDownLatch(50); CountDownLatch latch2 = new CountDownLatch(50); CountDownLatch latch3 = new CountDownLatch(50); CountDownLatch latch4 = new CountDownLatch(50); CountDownLatch latch5 = new CountDownLatch(50); long start ; long end ; start = System.currentTimeMillis(); for (int i = 1 ; i <= 50; i++) { new Thread(() -> { try { for (int j = 1; j <= 10000; j++) { compareSpeed.add(); } } finally { latch.countDown(); } }).start(); } try { // 等待结果 latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } end = System.currentTimeMillis(); System.out.println("volatile 使用时间 ===> " + (end - start) + "==============" + compareSpeed.number); // ========================================================================== start = System.currentTimeMillis(); for (int i = 1 ; i <= 50; i++) { new Thread(() -> { try { for (int j = 1; j <= 10000; j++) { compareSpeed.add_AtomicInteger(); } } finally { latch2.countDown(); } }).start(); } try { // 等待结果 latch2.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } end = System.currentTimeMillis(); System.out.println("AtomicInteger 使用时间 ===> " + (end - start)+"==============" + compareSpeed.atomicInteger.get()); // ========================================================= start = System.currentTimeMillis(); for (int i = 1 ; i <= 50; i++) { new Thread(() -> { try { for (int j = 1; j <= 10000; j++) { compareSpeed.add_AtomicLong(); } } finally { latch3.countDown(); } }).start(); } try { // 等待结果 latch3.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } end = System.currentTimeMillis(); System.out.println("AtomicLong 使用时间 ===> " + (end - start) + "==============" + compareSpeed.atomicLong.get()); // ============================================ start = System.currentTimeMillis(); for (int i = 1 ; i <= 50; i++) { new Thread(() -> { try { for (int j = 1; j <= 10000; j++) { compareSpeed.add_LongAdder(); } } finally { latch4.countDown(); } }).start(); } try { // 等待结果 latch4.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } end = System.currentTimeMillis(); System.out.println("LongAdder 使用时间 ===> " + (end - start) + "==============" + compareSpeed.longAdder.sum()); // ========================================================== start = System.currentTimeMillis(); for (int i = 1 ; i <= 50; i++) { new Thread(() -> { try { for (int j = 1; j <= 10000; j++) { compareSpeed.add_longAccumulator(); } } finally { latch5.countDown(); } }).start(); } try { // 等待结果 latch5.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } end = System.currentTimeMillis(); System.out.println("LongAccumulator 使用时间 ===> " + (end - start) + "==============" + compareSpeed.longAccumulator.get());
}
}
class CompareSpeed {
volatile int number;
public synchronized void add() {
number++;
}
// 使用AtomicInteger
AtomicInteger atomicInteger = new AtomicInteger();public void add_AtomicInteger() {
atomicInteger.getAndIncrement();
}
// 使用AtomicLong
AtomicLong atomicLong = new AtomicLong();public void add_AtomicLong() {
atomicLong.getAndIncrement();
}
// 使用LongAdder
LongAdder longAdder = new LongAdder();public void add_LongAdder() {
longAdder.increment();
}
// 使用LongAccumulator
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);public void add_longAccumulator() {
longAccumulator.accumulate(1L);
}
}
2.5.3 源码、原理分析
架构
LongAdder是Striped64的子类
原理
官网说明
Striped64
Striped64有几个比较重要的成员函数
两个重要的字段
Striped64中一些变量或者方法的定义
Cell是java.util.concurrent.atomic包下Striped64中的一个内部类
LongAdder为什么这么快
一句话
LongAdder的基本思想就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组不同的槽中,各个线程只对自己槽中的哪个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多,如果要获取到真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分撒到多个value中去,从而降级更新热点。
数学表达
内部有一个base变量,一个Cell[]数组
base变量:非竞争状条件下,直接累加到该变量上
Cell[]:竞争状态下,累加各个线程自己的槽Cell[i]中的数据再加上base变量中的数据
公式
源码解读深度分析
小总结
LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,通过空间换时间,用一个数组cells,将一个value拆分进这个数组cells,当多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值。再根据hash值映射到这个数组cells的某个下标,再对改下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终的结果
increment()
add(1L)
public void add(long x) { // as 表示Cell数组的一个引用,在非竞争情况下,其为默认值null // b 是用来接收base的值的,默认为0 // v 是用来接收当个Cell对象数值的 // m 是用来接收Cell数组长度的 // a 是用来接收当个Cell对象的 // x 是一个固定的值1 Cell[] as; long b, v; int m; Cell a; // (as = cells) != null 表示将创建好的Cell数组赋值给as,并判断其是否为空,如果为空说明当前是非竞争状态,则进入第二部判断,不等于空则为竞争状态,就不进行第二步判断 // !casBase(b = base, b + x) 进行CAS操作,如果添加成功则进行取反,进不去if语句中,也表示当前是非竞争状态。如果不成功则取反进入if语句中 if ((as = cells) != null || !casBase(b = base, b + x)) { // 设置不竞争 boolean uncontended = true; // as == null 判断是否为空,如果为空说明还没有创建cells数组 // (m = as.length - 1) < 0 判断cells数组长度是否小于0,不大可能小于零,初始化长度为2,后面扩容是2倍扩容 // (a = as[getProbe() & m]) == null getProbe()获取当前线程所持有的hash值,然后与数cells组长度进行按位与运算,获取到对应的cells下标,将其赋给a,然后判断当前位置是否有数据 // uncontended = a.cas(v = a.value, v + x)) 首先是进行CAS操作,将单个Cell中的数据进行累加,如果成功则不进入if语句中,如果失败则进入if语句中进行扩容 if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) // 进行 1.新建 2. 赋值 3. 扩容 longAccumulate(x, null, uncontended); } }
最初无竞争时只更新base
如果更新base失败后,首次新建一个Cell数组
当多线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
longAccumulate
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { // x 是要添加的值 // LongBinaryOperator 是一个函数式接口,在外部的add方法中并未传入 // wasUncontended 表示是否有竞争 // h 表示线程所持有的hash值 int h; // (h = getProbe()) == 0 通过getProbe来获取线程所持有的hash值,来判断线程是否初始化hash值 if ((h = getProbe()) == 0) { // 强制线程初始化,也就是随机生成一个hash值 ThreadLocalRandom.current(); // force initialization // 获取hash值 h = getProbe(); // 将将竞争关系初始化 wasUncontended = true; } boolean collide = false; // True if last slot nonempty for (;;) { // as 用于接收cells数组 // a 用于接收cells数组中的单个Cell对象的 // n 用于接收cells数组长度的 // v 用于接收Cell和base的数值的 Cell[] as; Cell a; int n; long v; // as = cells) != null 将cells赋值给as并判断是否为空 // (n = as.length) > 0 将as的数组长度取出并赋值给n,然后判断是否大于0 if ((as = cells) != null && (n = as.length) > 0) { // a = as[(n - 1) & h]) == null 通过与数组长度减一和线程所持有的hash值进行按位与运算,判断该下标是否有数据 if ((a = as[(n - 1) & h]) == null) { // 判断是否上锁 if (cellsBusy == 0) { // Try to attach new Cell // 创建Cell对象然后将数据放入到Cell对象中 Cell r = new Cell(x); // Optimistically create // 双端校验,判断是否加锁,然后上锁 if (cellsBusy == 0 && casCellsBusy()) { // 默认创建失败 boolean created = false; try { // Recheck under lock // rs 用来接收cells // m 用来接收数组长度 // j 用来接收数组下标 Cell[] rs; int m, j; // (rs = cells) != null 将cells赋值给rs并判断是否为空 // (m = rs.length) > 0 将cells数组长度赋值给m,然后判断其长度是否大于零 // rs[j = (m - 1) & h] == null 将数组长度减一与线程所持有的hash值进行按位与运算,然后获取到数组下标,判断对应位置是否为空 if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { // 将创建的Cell对象赋值给对应下标对象 rs[j] = r; // 创建成功 created = true; } } finally { // 释放锁 cellsBusy = 0; } // 判断是否创建成功 if (created) break; continue; // Slot is now non-empty } } collide = false; } // 是否竞争,如果为false则表示竞争 else if (!wasUncontended) // CAS already known to fail // 重置为不竞争状态 wasUncontended = true; // Continue after rehash // ((fn == null) ? v + x : fn.applyAsLong(v, x))) 如果没有传入计算格式,则使用默认的计算格式 // 进行CAS添加数据 else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // n >= NCPU 判断当前Cell数组长度是否超过CPU的数量 // cells != as 判断当前的as是否不等于cells else if (n >= NCPU || cells != as) collide = false; // At max size or stale // 判断是否可以扩容 else if (!collide) collide = true; // cellsBusy == 0 判断是否加锁 // casCellsBusy 进行加锁 else if (cellsBusy == 0 && casCellsBusy()) { try { // 判断当前as是否和cells是否相等 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,释放锁 cellsBusy = 0; } collide = false; // 结束本次循环 continue; // Retry with expanded table } // 将每个线程的hash值重新初始化一下,让其再次进行CAS h = advanceProbe(h); } // cellsBusy == 0 判断是否加锁 // cells == as 判断当前as是否和cells是否相同 // casCellsBusy 进行加锁 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { // 默认设置初始化没有完成 boolean init = false; try { // Initialize table // 双端校验,通过多次判断,以防其它线程初始化完成,然后将其初始化数组进行覆盖 if (cells == as) { // 创建一个数组容量为2的Cell数组 Cell[] rs = new Cell[2]; // 将线程所持有的hash值与1进行按位与运算,然后将当个携带数据的Cell赋值给对应Cell数组下标的位置对象。 rs[h & 1] = new Cell(x); // 将新建的数组指向cells cells = rs; // 初始化成功 init = true; } } finally { // 将cellsBusy设置为0,也就是释放锁 cellsBusy = 0; } // 初始化成功,则跳出循环 if (init) break; } // 如果Cell数组还在初始化,则会进去这个判断语句,这个是用来兜底的,也就是既没有创建Cell数组,Cell也还没有完成初始化 // ((fn == null) ? v + x : fn.applyAsLong(v, x))) 如果没有自定义运算规则,则使用默认的规则,直接相加 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) // 执行成功跳出循环 break; // Fall back on using base } }
总结
sum
public long sum() { // as 用来接收cells数组的 // a 用来接收cells数组中单个Cell对象的 Cell[] as = cells; Cell a; // sum 用力接收base long sum = base; // 判断as是否为空 if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
- 结论
- sum()会将Cell数组中的value和base进行累加,然后作为返回值返回
- 核心思想就是将之前AtomicLong一个value的更新压力分担到多个value中去,从而降级更新热点,通过分散热点从而降低压力
- 为啥在并发情况下sum的值不精确
- 首先,最终返回的sum局部变量,初始化赋值时被赋值为base,而最终返回值时,很能base已经被更新了,此时的局部变量sum是不会实时更新的,因而造成不一致
- 其次,因为cell也不是实时更新的,所以直接获取的话并不是最后的值。因此,在没有并发的情况下,sum获取的是精确值。
使用总结
- AtomicLong
- 线程安全的,可允许一些性能损耗,精度要求高的时候可以使用
- 对精度要高,性能代价
- AtomicLong是多个线程对单个热点值value进行原子操作
- LongAdder
- 当需要高并发下有较好的性能表现,对精度要求不高,可以使用。
- 精度代价,保证性能
- LongAdder是每个线程都有自己的槽,各个线程一般只对槽中的数据进行CAS操作
总结
AtomicLong
- 原理
- CAS+自旋
- incrementAndGet
- 场景
- 低并发下的全局计算
- AtomicLong能保证并发情况下计数的准确性,其内部是通过CAS来解决锁问题
- 缺陷
- 高并发后性能急剧下降
- AtomicLong的自旋会成为瓶颈
LongAdder
- 原理
- CAS+Base+Cell数组分散
- 空间换时间并分散了热点数据
- 场景
- 高并发下的全局运算
- 缺陷
- 最后所获取的值不精确。
九、ThreadLocal
1. ThreadLocal简介
1.1 面试题
- ThreadLocal中的ThreadLocalMap的数据结构和关系?
- ThreadLocal的key是弱引用,这是为什么?
- ThreadLocal内存泄漏问题你知道吗?
- ThreadLocal中最后为什么要加remove方法
1.2 是什么
- ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
1.3 能干什么
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各有一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
1.4 常用API
1 .5 实例演示
package com.leon.juc.threadlocal;
import org.junit.experimental.theories.Theories;
/**
* ClassName:ThreadLocalTest
* Package:com.leon.juc.threadlocal
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class ThreadLocalTest {
public static void main(String[] args) {
// 创建House对象
House house = new House();
// 线程AA
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
// 进行操作
house.salaryHouse();
}
System.out.println(Thread.currentThread().getName() + " 售卖出去" + house.getThreadLocal().get());
} finally {
// 清空数据
house.remove();
}
},"AA").start();
// 线程BB
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
// 进行操作
house.salaryHouse();
}
System.out.println(Thread.currentThread().getName() + " 售卖出去 "+ house.getThreadLocal().get());
} finally {
// 清空数据
house.remove();
}
},"BB").start();
// 线程CC
new Thread(() -> {
try {
for (int i = 0; i < 8; i++) {
house.salaryHouse();
}
System.out.println(Thread.currentThread().getName() + " 售卖出去 "+ house.getThreadLocal().get());
} finally {
// 清空数据
house.remove();
}
},"CC").start();
System.out.println(Thread.currentThread().getName() + " 售卖出去 "+ house.getThreadLocal().get());
}
}
class House{
// 创建方式一
//private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
//
// @Override
// protected Integer initialValue() {
// return 0;
// }
//};
// 创建方式二 ---- 推荐
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void salaryHouse(){
// 获取数据
Integer number = threadLocal.get();
// 数据进行累加
++number;
// 设置数据
threadLocal.set(number);
}
public void remove(){
// 清除ThreadLocal中的数据
threadLocal.remove();
}
public ThreadLocal<Integer> getThreadLocal(){
return threadLocal;
}
}
1.6 总结
- 因为每个Thread内存有自己的实例副本且该副本只由当前线程自己使用,既然其它Thread不可访问,那就不存在多线程间共享的问题。统一设置初始值,但是每个线程对这个值的修改都是各自线程相互独立的。
- 如何才能不争抢
- 加入synchronized或者Lock控制资源访问顺序
- 每个线程都持有一份
2. 阿里ThreadLocal规范
2.1 非线程安全的SimpleDateFormat
官方文档
SimpleDateFromat中的日期格式不是同步的,推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。
案例演示
package com.leon.juc.threadlocal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /**
ClassName:SimpleDateFormatTest
Package:com.leon.juc.threadlocal
Description:
*@Author: leon
@Version: 1.0
*/
public class SimpleDateFormatTest {private static SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
public static Date parse(String date){
try { return sdf.parse(date); } catch (ParseException e) { throw new RuntimeException(e); }
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) { new Thread(() -> { parse("2011-11-11"); }).start(); }
}
}
bug
源码分析
SimpleDateFormate类内部有一个Calendar对象引用,它用来存储和这个SimpleDateFormat相关的日期的信息,例如sdf.parse(dateStr),sdf.format(date)诸如此类方法参数传入的日期相关String,Date等等,都是交由Calendar引用来存储的,这样就会导致一个问题,如果使用的SimpleDateFormat是个static的,那么多个Thread之间就会共享这个SimopleDateFormat,同时也是共享者Calendar引用
2.2 解决方式一
// 方式二
public static synchronized Date parse2(String date){
try {
return sdf.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
- 将SimpleDateFormat定义局部变量
- 缺点:每次用一次方法就会创建有个SimpleDateFormat对象,方法结束又要作为垃圾回收,消耗内存
2.3 解决方式二
// 方式三
private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse3(String dateSt){
try {
return threadLocal.get().parse(dateSt);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
2.4 其它
- 加锁
- 或者使用第三方时间库
2.5 使用DateTimeFormatter
// 使用DateTimeFormatter
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formater(LocalDateTime date){
return FORMATTER.format(date);
}
public static LocalDateTime format2(String date){
return LocalDateTime.parse(date,FORMATTER);
}
3. ThreadLocal源码分析
3.1 Thread、Thread Local、ThreadLocalMap之间的关系
Thread和ThreadLocal
ThreadLocal和ThreadLocalMap
三者总概括
ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry数组。
当为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,任意对象为value的Entry对象。往ThreadLocalMap中存放
3.2 总结
近似理解为
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为key),不过是经过了两层包装的ThreadLocal对象:
JVM内部维护了一个线程版的Map
(通过ThreadLocal对象的set方法,结果把ThreadLocal对象本身当作key,放入进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map中获取,通过这样让每个线程都用自己独立的变量,每个线程都有一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。 - 注意:要理解不是每个线程都有一个ThreadLocal实例,而是所有线程都共享一个ThreadLocal,每个线程都有一个属于自己的ThreadLocalMap集合,ThreadLocalMap并不是只能存放一个对键值对数据,而是可以存放很多键值对数据,也就是key-vlaue。但前提是,必须一个数据对应一个ThreadLocal对象实例,也就是一个value对应一个ThreadLocal对象实例
- 因为是每一个线程对应一个ThreadLocalMap所以可以理解为,JVM 内部维护了一个线程版的Map
4. ThreadLocal内存泄漏
4.1 阿里面试题
4.2 什么是内存泄漏
- 不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏
4.3 什么原因
4.3.1 源码截图
4.3.2 强引用、软引用、弱引用、虚引用
ThreadLocalMap
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的Map(其实是以它为key),不过是经过了两层包装的ThreadLocal对象:
- 第一层包装是使用WeakReference
>将ThreadLocal对象变成一个弱引用的对象 - 第二层包装是定义了一个专门的类Entry来扩展WeakReference
>
- 第一层包装是使用WeakReference
整体架构
Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
创建一个有finalize方法的对象
class MyObject{ public void init(){ } @Override protected void finalize() throws Throwable { System.out.println("MyObject==============================>>>> finalize()"); } }
强引用(默认)
public static void main(String[] args) { MyObject myObject = new MyObject(); System.out.println("gc.before " + myObject); // 将对象引用置为空,看是否回收 myObject = null; // 执行垃圾回收 System.gc(); try { // 睡眠1秒执行垃圾回收 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("gc.after " + myObject); }
当内存不足,JVM开始垃圾回收,对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不回收
强引用是最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一
对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式的将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)
软引用
软引用是一种相对于强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说
- 当系统内存充足时它 不会被回收
- 当系统内存不足时它 会被回收
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject()); //System.out.println("gc.before内存够用 " + softReference.get()); System.out.println("gc.before内存不够用 " + softReference.get()); //System.gc(); // // try { // byte[] bytes = new byte[9 * 1024 * 1024]; // // TimeUnit.SECONDS.sleep(1); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } // // System.out.println("gc.after内存够用 " + softReference.get()); try { byte[] bytes = new byte[9 * 1024 * 1024]; TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("gc.after内存不够用 " + softReference.get());
弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
WeakReference<MyObject> weakReference = new WeakReference(new MyObject()); System.out.println("gc.before " + weakReference.get()); System.gc(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("gc.after " + weakReference.get());
软引用和弱引用的使用场景
- 假如有一个应用需要读取大量的本地图片
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中又可能造成内存溢出
- 此时使用软引用可以解决这个问题
- 设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效的避免了OOM的问题。
虚引用
虚引用说明
- 虚引用需要java.lang.ref.PhantomReference类来实现
- 顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
- 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
- 其意义在于:说明一个对象已经进入finalize阶段,可以被gc回收,用来实现比finalizetion机制更灵活的回收操作。换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
构造方法
引用队列
案例演示
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>(); PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue); List<byte[]> bytesList = new ArrayList<>(); new Thread(() -> { while (true){ bytesList.add(new byte[1*1024*1024]); try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(phantomReference.get()); } }).start(); new Thread(() -> { while (true){ Reference<? extends MyObject> poll = referenceQueue.poll(); if(poll != null){ System.out.println("===============MyObject被垃圾收集器回收了"); } } }).start(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); }
GCRoots和四大引用小总结
4.3.3 关系
- 每个Thread对象维护着一个ThreadLocalMap的引用
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值value是传递进来的对象
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以THreadLocal能够实现“数据隔离”,获取当前线程的局部变量,不受其它线程影响
4.4 ThreadLocalMap中的Entry为什么使用弱引用,不使用会发生什么?
4.4.1 案例演示
public void function01(){
ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); // 1
threadLocal.set(1); // 2
threadLocal.get(); // 3
}
- 第一行新建了一个ThreadLocal对象,threadLocal是强引用指向这个对象
- 第二行调用set()方法新建一个Entry,通过源码可知Entry对象里的k是弱引用指向对象
4.4.2 为什么源代码用弱引用
- 当function01方法执行完毕后,栈帧销毁引用threadLocal也就没有了。但是此时线程的ThreadLocalMap里面某个Entry的key引用还指向这个对象
- 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及value指向的对象不能被GC回收,造成内存泄漏
- 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的问题),使用弱引用,就可以使ThreadLocal对象在方法执行完毕之后顺利被回收且Entry的key引用指向为null
4.4.3 弱引用就不会有问题吗?
问题
当我们为ThreadLocal变量赋值,实际上就是当前的Entry(ThreadLocal实例为key,值为value)往这个ThreadLocalMap中存放。Entry中的key是弱引用,当ThreadLocal外部强引用被置为null(threadLocal=null),那么系统GC的时候,根据可达性分析,这个ThreadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread ref -> Thread -> ThreadLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
当然,如果当前ThreadLocal运行结束,ThreadLocal 、ThreadLocalMap、Entry,没有引用链可达,在垃圾回收的时候都会被系统进行回收。
但在实际使用中有时候会用线程池去维护的线程,比如在Executos.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以Thread Local内存泄漏就需要我么去关注了。
key为null的Entry,原理解析
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocal Map中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
虽然弱引用保证了key指向的ThreadLocal对象能被及时回收,但是value指向的value对象需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个Entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果不手动调用remove方法,那么后面的线程就有可能获取到上一个线程遗留下来的value值,造成bug
set、get方法回去检查所有键为null的Entry对象
set()
get()
remove()
结论
不管在什么情况下都要进行清空引用,也就是调用Threalocal的remove方法
4.5 最佳实践
阿里手册
使用完记得remove
5. 小结
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expungeStaleEntry,claenSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法
十、Java对象内存布局和对象头
1. Object object = new Object()谈谈你对这句话的理解?一般情况而言JDK8按照默认情况下,new一个对象占多少内存
- 位置所在
- JVM的堆 ->新生区 -> 伊甸园区
- 构成布局
- 对象头
- 实例数据
- 对齐填充
2. 对象在堆内存中的布局
2.1 定义
周志明JVM第三版
对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
2.2 对象再堆内存中的存储布局
- 对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)
- 对象分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址
2.2.1 对象头
对象标记Mark Word
保存什么
默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定值的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
类元信息(又叫类型指针)
示意图
对象指向它的类元数据的指针,虚拟及通过这个指针来确定这个对象是哪个类的实例。
对象头的大小
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一个16个字节。
2.2.2 实例数据
- 存放类的属性(Field)数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
2.2.3 对齐填充
- 虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在,仅仅是为了字节对齐这部分内存按8字节补充对齐。
2.3 官网理论
HotSpot术语表官网:https://openjdk.org/groups/hotspot/docs/HotSpotGlossary.html
底层源码论证-oop.hpp,位置为:\jdk8u-ri-master\hotspot\src\share\vm\oops
mark字段是mark word,metadata是类指针klass pointer,对象头(object header)即是由这两个字段组成,这些术语可以参考Hotspot术语表。
3. 再说对象头的MarkWord
3.1 oop.hpp
markOop.hpp
hash:保存对象的哈希码
age:保存对象的分代年龄
biased_lock:偏向锁标识
lock:锁状态标识位
JavaThread*:保持持有偏向锁的线程ID
epoch:保存偏向时间戳
3.2 mark word(64位)分布图,对象布局、GC回收和后面的锁升级就是对象标记Mark Word里面标志位的变化
4. 聊一聊Object object = new Object()
4.1 JOL证明
JOL官网:https://openjdk.org/projects/code-tools/jol/
maven仓库地址:
<!-- 官网:https://openjdk.org/projects/code-tools/jol/ 定位:分析对象在JVM的大小和分布 --> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
演示
package com.leon.juc; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM; /**
ClassName:ObjectHeader
Package:com.leon.juc
Description:
*@Author: leon
@Version: 1.0
*/
public class ObjectHeader {public static void main(String[] args) {
// VM的细节详细情况 System.out.println(VM.current().details()); // 所有的对象分配的字节都是8的整数倍 System.out.println(VM.current().objectAlignment()); Object object = new Object(); System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
4.2 案例演示-说明
| OFFSET | 偏移量,也就是到这个字段位置所占用的byte数 |
| ————— | —————————————— |
| SIZE | 后面类型的字节大小 |
| TYPE | 是Class中定义的类型 |
| DESCRIPTION | DESCRIPTION是类型的描述 |
| VALUE | VALUE是TYPE在内存中的值 |
4.3 GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认就是15
首先设置一下JVM参数:-XX:MaxTenuringThreshold=16
报错如下
4.4 尾巴参数说明
命令:java -XX:+PrintCommandLineFlags -version
查看JVM是携带哪些命令启动的
默认开启类型指针压缩说明
指令: -XX:+UseCompressedClassPointers
结果
关闭类型指针压缩
指令:-XX:-UseCompressedClassPointers
结果
5.其它演示
package com.leon.juc;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
/**
* ClassName:ObjectHeader
* Package:com.leon.juc
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class ObjectHeader {
public static void main(String[] args) {
MyObject myObject = new MyObject();
System.out.println(ClassLayout.parseInstance(myObject).toPrintable());
}
}
class MyObject{
private int id ;
private long version;
private double salary;
}
运行结果
十一、Synchronized与锁升级
1. 面试题
- 谈谈对synchronized的理解
- synchronized的锁升级聊聊
- synchronized的性能是不是一定弱于Lock
2. 总纲
2.1 说明
- synchronized锁优化的背景
- 用锁能够实现数据的安全性,但是会带来性能下降
- 无锁能够基于线程并行提升程序性能,但是会带来安全性下降
2.2 synchronized锁
- 由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略
3. synchronized的性能变化
3.1 java 5以前,只有synchronized,这个操作系统级别的重量级操作
重量级锁,假如锁的竞争比较激烈的话,性能下降
Java5之前,用户态和内核态之间的切换
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线就需要操作系统介入,需要在用户态和内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递许多变量、参数个内核,内核也需要保护好用户态在切换时的一些寄存值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态区完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。
Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
3.2 为什么每一个对象都可以成为一个锁
markOop.hpp,位置:jdk8u-ri-master\hotspot\src\share\vm\oops
Monitior可以理解为一种同步工具,也可以理解为一种同步机制,常常被描述为一个Java对象。Java对象是天生的Monitor,每个Java对象都有成为Monitor的潜质,因为在Java的设计中,每个Java对象都可以理解成拥有着一把看不见的锁,它叫做内部锁或者Monitor锁
package com.leon.juc.synchronizedupgrade; /** * ClassName:SynchronizedUpgradeTest * Package:com.leon.juc.synchronizedupgrade * Description: * * @Author: leon * @Version: 1.0 */ public class SynchronizedUpgradeTest { public static void main(String[] args) { Object object = new Object(); new Thread(() -> { synchronized (object){ System.out.println(Thread.currentThread().getName() + ": Synchronized upgrade test"); } }).start(); } }
Monitor的本质是依赖与底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。
Monitor(监视器锁)
Mutex Lock
- Monitor是在JVM底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间,成本非常高。所以synchronized是Java语言中的一个重量级操作。
Monitor与Java对象以及线程是如何关联?
- 如果一个Java对象被某个线程锁住,则该Java对象的Mark Word字段中LockWord指向Monitor的起始地址
- Monitor的Owner字段会存放拥有相关联对象的线程id
Mutex Lock切换需要从用户态转换到内核态中,因此状态转换需要耗费很多的处理器时间
3.3 java 6开始,优化synchronized
- java6之后,为了减少获取锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。因而需要有个逐步升级的过程,别一开始就是重量级锁
4. synchronized锁种类及升级步骤
4.1 多线程访问情况,3种
- 只有一个线程来访问,有且唯一
- 有两个线程A、B来交替访问
- 竞争激烈,多个线程来访问
4.2 升级流程
- synchronized用的锁是存在Java对象头里的Mark Word中锁升级功能主要依赖Mark Word中锁标志位和释放偏向锁标志位
- 64位标记图
4.3 无锁
案例演示
package com.leon.juc.synchronizedupgrade; import org.openjdk.jol.info.ClassLayout; /**
ClassName:NotSynchronizedTest
Package:com.leon.juc.synchronizedupgrade
Description:
*@Author: leon
@Version: 1.0
*/
public class NotSynchronizedTest {public static void main(String[] args) {
Object object = new Object(); System.out.println(object.hashCode()); System.out.println(Integer.toHexString(object.hashCode())); // 16进制 System.out.println(Integer.toBinaryString(object.hashCode())); // 2 进制 // 10011010111111011101010100100 // 00010011010111111011101010100100 // 通过JOL工具,打印对象的数据 // 默认情况是没有hash值的,只有在获取的时候才会看到,也就是懒加载 System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
运行图
64位图
4.4 偏向锁
4.4.1 主要作用
- 当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获取锁
- 小结
- Hotspot的作者经过研究发现,大多数情况下:
- 多线程的情况下,锁不仅存在多线程竞争,还存在锁由同一个线程多次获得的情况。
- 偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能
4.4.2 64位标记图
- 通过CAS方式修改Mark Word中的线程ID
4.4.3 偏向锁的持有
说明
理论落地:
- 在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个线程占用它的线程拥有,这个线程就是锁的偏向线程。
- 那么只需要在锁第一次被拥有的时候,记录下偏向线程的ID。这样偏向线程就一直持有着锁。后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
- 假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候可能需要升级变为轻量级锁,才能保证线程间公平竞争。偏向锁只有遇到其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
技术实现
- 一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若线程再次访问同一个synchronized方法时,该线程只需要去对象头的Mark Word中去判断一下是否偏向锁指向本身的ID,无需在进入Monitor去竞争对象了
细化案例Account对象举例说明
偏向锁的操作不用直接到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,以一个Account对象的“对象头”为例
假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改偏向标识,标识当前线程就获得该锁。随对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于对一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。
线程获得了锁,可以执行同步代码块。当线程第二次到达同步代码块时,会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过Accout对象的Mark Word判断,当前线程ID还在,说明还持有着这个对象的锁,就可以进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标识线程获取到了当前锁,不用操作系统接入。
上述就是偏向锁:在没有其它线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行
4.4.4 偏向锁JVM指令
指令:java -XX:+PrintFlagsInitial | grep BiasedLock*
重要参数说明
实际上偏向锁在JDK1.6之后默认开启的,但是启动时间有延时,所以需要添加参数:
-XX:BiasedLockingStartupDelay=0
开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入 —-> 轻量级锁状态
-XX:-UseBiasedLocking
4.4.5 案例演示
默认情况
package com.leon.juc.synchronizedupgrade; import org.openjdk.jol.info.ClassLayout; /**
ClassName:SynchronizedUpgradeTest
Package:com.leon.juc.synchronizedupgrade
Description:
*@Author: leon
@Version: 1.0
*/
public class SynchronizedUpgradeTest {public static void main(String[] args) {
Object object = new Object(); new Thread(() -> { synchronized (object){ System.out.println(ClassLayout.parseInstance(object).toPrintable()); } }).start();
}
}
没有任何效果
系统参数默认值
-XX:+UseBiasedLocking 开启偏向锁
-XX:-UseBiasedLocking 关闭偏向锁
-XX:BiasedLockingStartupDelay=0 关闭延迟(演示偏向锁时需要开启)
参数说明
- 偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活,可以使用JVM参数来关闭延时 -XX:BiasedLockingStartupDelay=0如果确定锁通常处于竞争状态则可以通过过JVM参数-XX:-UseBiasedLocking关闭偏向锁,那么默认会进入轻量级锁。
关闭延时参数,直接启用偏向锁
指令:-XX:BiasedLockingStartupDelay=0
4.4.6 偏向锁的撤销
当另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级位轻量级锁,竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁
撤销
偏向锁的撤销
偏向锁使用一种等到竞争出现释放锁的机制,只有当其它线程竞争锁时,持有偏向锁的原来线程才会被撤销。
撤销需要等待全局安全点(改时间上没有字节码正在执行),同时检查有偏向锁的线程是否还在执行:
第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获取该轻量级锁。
第一个线程执行完成synchronized方法(退出同步块),则对象头设置成无锁状态并撤销偏向锁,重新偏向。
4.4.7 总体步骤流程图示
4.5 轻量锁
4.5.1 主要作用
- 有线程来参与锁的竞争,但是获取锁的冲突时间很短
- 本质就是自旋锁
4.5.2 64标记图
4.5.3 轻量级锁的获取
轻量级锁是为了再线程近乎交替执行同步代码块时提高性能。
主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁时用操作系统互斥量产生性能消耗,也就是先自旋再阻塞。
升级时机:当关闭偏向锁功能或多线程竞争偏向锁和会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该所已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那么线程B就会进行CAS操作希望能获取到锁。此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A ->B),重新偏向于其它线程(即偏向锁交给其它线程,相当于当前线程释放了锁),该锁会保持偏向锁状态,也就是A现场执行完毕,B线程继续执行。
如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而在竞争的线程B会进入自旋等待获取该轻量级锁。
4.5.4 演示
关闭偏向锁,就可以直接进入轻量级锁:-XX:-UseBiasedLocking
4.5.5 步骤流程图示
4.5.6 自旋达到一定次数和程度
Java6之前
默认启用,默认情况下自旋的次数是10次
-XX:PreBlockSpin=10
自旋线程数超过CPU核数一半
Java6之后
自适应
- 自适应意味着自旋的次数不是固定不变的
- 而是根据
- 同一个锁上一次自旋的时间
- 拥有锁线程的状态来决定
4.5.7 轻量锁与偏向锁的区别和不同
- 争夺轻量级锁失败时,自旋尝试抢占锁
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
4.6 重锁
4.6.1 条件
- 有大量的线程参与锁的竞争,冲突性很高
4.6.2 锁标志位
4.6.3 演示
package com.leon.juc.synchronizedupgrade;
import org.openjdk.jol.info.ClassLayout;
/**
* ClassName:SynchronizedUpgradeTest
* Package:com.leon.juc.synchronizedupgrade
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class SynchronizedUpgradeTest {
public static void main(String[] args) {
Object object = new Object();
new Thread(() -> {
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}).start();
new Thread(() -> {
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}).start();
new Thread(() -> {
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}).start();
new Thread(() -> {
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}).start();
}
}
4.7 小总结
4.7.1 各种锁的优缺点、synchronized锁升级和实现原理
- synchronized锁升级过程总结:先自旋,不行再阻塞。实际上是把之前的悲观锁(重量级锁)变成再一定条件下使用偏向锁以及使用轻量级(自旋CAS)的形式
- synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的Mark Word来实现的。
- JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁的升级过程,而不是无论什么情况都使用重量级锁。
- 偏向锁:适用于单线程的情况,在不存在竞争的时候进入同步方法/代码块则使用偏向锁
- 轻量级锁:适用于竞争不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的时自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用CPU资源但是相比较使用重量级锁还是更高效。
- 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
5. JIT编译器对锁的优化
5.1 JIT
- Just In Time Compiler,一般翻译为即时编译器
5.2 锁消除
- 从JIT角度看相当于无视它,synchronized(o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,极端的角度说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
package com.leon.juc.synchronizedupgrade;
/**
* ClassName:EliminateSynchronized
* Package:com.leon.juc.synchronizedupgrade
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class EliminateSynchronized {
static Object o = new Object();
public static void main(String[] args) {
new Thread(() -> {
Object o = new Object(); // 锁消除
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
}).start();
}
}
5.3 锁粗化
- 假如方法中首尾相接,前后相邻的都是同一个锁对象,那么JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请使用即可,避免次次的申请和释放锁,提升了性能。
package com.leon.juc.synchronizedupgrade;
/**
* ClassName:EliminateSynchronized
* Package:com.leon.juc.synchronizedupgrade
* Description:
*
* @Author: leon
* @Version: 1.0
*/
public class EliminateSynchronized {
static Object o = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
// 编译后变成这样
synchronized (o){
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
System.out.println(Thread.currentThread().getName()+"======================>锁消除");
}
}).start();
}
}
十二、AbstractQueueSynchronized之AQS
1. 前置知识
- 公平和非公平锁
- 可重入锁
- 自旋锁
- LockSupport
- 数据结构之链表
- 设计模式之模板设计模式
2. 是什么
2.1 字面意思
抽象的队列同步器
源码层面
AbstractQueueSynchronized通常被简称为AQS
2.2 技术理解
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取以及线程的排队工作,并通过一个int类变量表示持有锁的状态。
3. AQS为什么是JUC内容中最重要的基石
3.1 和AQS有关的类
ReetrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
3.2 进一步理解锁和同步器的关系
- 锁,面向锁的使用者
- 定义了程序员和锁交互的使用层API,隐藏了实现细节,调用即可
- 同步器,面向锁的实现者
- 比如Java并发大神DougLee,提出统一规范简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
4. 能干嘛?
4.1 加锁会导致阻塞
- 有阻塞就需要排队,实现排队必然需要队列
4.2 说明
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等待),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(侯客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现,它将请求共享资源的线程封装成队列的节点(Node),通过CAS、自旋以及LockSupport.park()的方式维护state变量的状态,使并发达到同步的效果。
5.AQS初步
5.1 AQS初识
官网解释
有阻塞就需要排队,实现排队必然需要队列
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
5.2 AQS内部体系架构
5.2.1 AQS自身
AQS的int变量
AQS的同步状态State成员变量
助理解:银行办理业务的受理窗口状态
- 零就是没人,自由状态可以办理。
- 大于等于1,有人占用窗口,需要等待
AQS的CLH队列
CLH队列,为一个双向队列
小总结
有阻塞就需要等待,实现排队必然需要队列
state变量+CLH双端队列
5.2.2 内部类Node(Node类在AQS类内部)
Node的int变量
Node的等待状态waitState成员变量
说明
- 等候区其它线程的等待状态
- 队列中没个排队的个体就是一个Node
Node此类的讲解
内部结构
属性说明
5.3 AQS同步队列的基本结构
6. 从ReentrantLock开始解读AQS
- Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
6.1 ReentarntLock的原理
6.2 从最简单的lock方法开始看看公平锁和非公平锁
6.3 非公平锁,方法lock()
说明
对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差别就在于非公平锁获取时比公平锁中少了一个判断!hsaQueuePredecessors()。
hasQueuePredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
- 公平锁:公平锁讲究先来后到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中
- 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有所对象。也就是锁队列的第一个排队的线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
6.3.1 源码解读
lock
acquire()
源码和3大流程走向
tryAcquire(arg)
本次走非公平锁
nonfailTryAcquire(acquires)
return false
- 继续推进条件,走下一个方法
return true
- 结束
addWaiter(Node.EXCLUSIVE)
addWaiter(Node mode)
enq(node)
双向链表中,第一个节点为虚节点(也交哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。
假如3号线程Thread-C线程进来
- prev
- compareAndSetTail
- next
acquireQueued(addWaiter(Node.EXCLUSIVE),arg)
acquireQueued
假如再抢,抢失败就会进入
shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法中
shouldParkAfterFailedAcquire
- 如股前驱节点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire方法会返回true程序员会继续向前执行parkAndCheckInterrupt方法,用于将当前线程挂起
parkAndCheckInterrupt
6.4 unlock
- sync.release(1)
- tryRelease(arg)
- unparkSuccessor
十三、 ReentrantLock、ReentrantReadWriteLock、StampedLock
1. 本章总纲
- 无锁 -> 独占锁 -> 读写锁 -> 邮戳锁
2. 面试题
- Java里面有哪些锁?
- 读写锁中,锁饥饿问题是什么?
- 有没有比读写锁更快的锁?
- StampedLock知道吗?(邮戳锁/票据锁)
- ReentrantReadWriteLock有锁降级机制策略是否知道?
3. 聊聊ReentrantReadWriteLock
3.1 是什么
3.1.2 读写说明
- 一个资源能够被多个线程访问,或者被一个写线程访问,但是不能同时存在读写线程
3.1.2 读写锁的意义和特点
- [读写锁ReentrantReadWriteLock],并不是真正意义上的读写分离,它只允许读读共享,而读写和写写依然是互斥的,大多实际场景“读/读”线程间并不存在互斥关系,只有“读/写”线程或“写/写”线程间的操作需要互斥。因此引入RentarntReadWriteLock。
- 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但是不能同时存在写锁和读锁,也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
- 只有在读多写少情境之下,读写锁才具有较高的性能体现。
3.2 特点
可重入
读写分离
无锁无序 -> 加锁 -> 读写锁演变
案例演示
package com.leon.juc; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * ClassName:ReadWriteLock * Package:com.leon.juc * Description: * * @Author: leon * @Version: 1.0 */ public class ReadWriteLock { public static void main(String[] args) { ReadWrite readWrite = new ReadWrite(); for (int i = 1; i <= 10; i++) { int finalI = i; new Thread(() -> { readWrite.write(String.valueOf(finalI),String.valueOf(finalI)); },String.valueOf(i)).start(); } for (int i = 1; i <= 10; i++) { int finalI = i; new Thread(() -> { readWrite.read(String.valueOf(finalI)); },String.valueOf(i)).start(); } } } class ReadWrite{ private Map<String,String> map = new HashMap<>(); private Lock lock = new ReentrantLock(); private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void write(String key ,String value){ //lock.lock(); readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"进行写操作"); map.put(key,value); TimeUnit.MILLISECONDS.sleep(500); System.out.println(Thread.currentThread().getName() + "写完成"); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //lock.unlock(); readWriteLock.writeLock().unlock(); } } public void read(String key){ try { //lock.lock(); readWriteLock.readLock().lock(); System.out.println(Thread.currentThread().getName()+"进行读操作"); String string = map.get(key); System.out.println(Thread.currentThread().getName()+"读完成,结果为 " + string); } finally { //lock.unlock(); readWriteLock.readLock().unlock(); } } }
从写锁->读锁,ReentrantReadWriteLock可以降级
《Java并发编程的艺术》中关于锁降价的说明
简单点说
- 锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)
读写锁降级演示
可以降级
锁降级:遵循获取写锁 -> 再获取读锁 -> 再释放写锁的次序,写锁能够降级成为读锁
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
java8官网说明
- 重入还允许通过获取写入锁定,然后读取锁然后释放写锁从写锁到读锁。但是从读锁升级到写锁是不可能的。
锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性
案例演示
package com.leon.juc.readwrite; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /**
ClassName:ReadWriteTest2
Package:com.leon.juc.readwrite
Description:
*@Author: leon
@Version: 1.0
*/
public class ReadWriteTest2 {public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); new Thread(() -> { readLock.lock(); System.out.println("=======读锁"); writeLock.lock(); System.out.println("=======写锁"); }).start(); //ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // // //new Thread(() -> { // // readWriteLock.readLock().lock(); // // readWriteLock.writeLock().lock(); // //}).start();
}
}如果线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
不可以锁升级
线程获取读锁是不能直接升级为写锁的
在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。
写锁和读锁是互斥的
- 写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其它读线程无法感知到当前写线程的操作。
- 因此分析读写锁ReentrantReadWorkLock,会发现它有个潜在的问题:读锁全完,写锁有望;写锁独占,读写全堵。如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须要等待,这是一种悲观锁
读写锁之读写规矩,降级
Oracle公司ReentrantReadWriteLock源码总结
- ReentrantReadWriteLock支持锁降级,遵循按照获取写锁,获取读锁在释放写锁的次序,写能够降级成为读锁,支持锁升级
- 声明了一个volatile类型的cacheValid变量,保证其可见性
- 首先是先获取读锁,然后判断cache是否可用,如果不可用则,释放读锁然后再获取写锁,再次进行判断擦车是否可用,如果不可用则进行数据操作,然后再将cache设置为欸true,再获取读锁,然后再释放写锁,其次进行数据的读取操作,最后再释放读锁。
- 如果不遵循锁降级原则:
- 在释放写锁之后,线程C想去读取最新的数据,这时线程D又去修改了数据,那么线程C读取的则是线程D修改之后的数据,并不是实时的数据,也就是在数据更新之后不能立马获取修改的数据
- 如果遵循锁降级原则:
- 在释放写锁之前,获取读锁,然后在释放写锁,此时线程C则可以读取到最新的数据,而线程D去获取写锁则会被阻塞,等线程获取完数据之后,释放读锁之后,线程D则再去进行修改,保证了数据的可见性和实时性,该设计是专门为缓存设计的。
4. 邮戳锁StampedLock
- 无锁 -> 独占锁 -> 读写锁 -> 邮戳锁
4.1 是什么
- StampedLock是JDK1.8中新增的一个读写锁也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
- 邮戳锁,也可以叫票据锁
- stamp(戳记,long类型)
- 代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
4.2 由锁饥饿问题引出
- 锁饥饿问题
- 假如有99个读,1个写。那么很有大概率是一直在读,而写一直在阻塞,这样便造成只有读没有写,这就是所谓的锁饥饿。
- 如何缓解锁饥饿问题
- 使用“公平锁”能够解决问题 —-> new ReentranrReadWriteLock(true),但是使用公平锁系统的吞吐量则会降级
- StampedLock类的乐观锁
- ReentrantReadWriteLock
- 允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其它线程读操作和写操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因在于ReentrantReadWriteLock支持读并发。
- ReentrantReadWriteLock的读锁被占用的时候,其它线程尝试获取写锁的时候会被阻塞。但是StampedLock采取乐观获取锁后,其它线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以获取读锁后,还需要对结果进行校验。
4.3 StampedLock的特点
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余表示成功。
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp有一致。
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)。
StampedLock有三种访问模式
Reading(读模式):功能与ReentrantReadWriteLock的读锁类似
Writing(写模式):功能与ReentrantReadWriteLock的写锁类似
Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观的认为读取时没人修改,假如被修改再实现升级为悲观读模式。
package com.leon.juc.readwrite; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; /** * ClassName:StampedLockTest * Package:com.leon.juc.readwrite * Description: * * @Author: leon * @Version: 1.0 */ public class StampedLockTest { public static void main(String[] args) { LockObject lockObject = new LockObject(); // 悲观读 //new Thread(() -> { // lockObject.readLock(); //},"bread").start(); // 乐观读 //new Thread(() -> { // lockObject.optimisticReadLock(); //}, "lread").start(); // //try { // TimeUnit.SECONDS.sleep(6); //} catch (InterruptedException e) { // throw new RuntimeException(e); //} // 乐观读 -> 悲观读 new Thread(() -> { lockObject.optimisticReadLock(); }, "lread").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(() -> { lockObject.writeLock(); }, "write").start(); } } class LockObject { private int number = 13; private StampedLock lock = new StampedLock(); // 写锁 public void writeLock() { // 加锁 long writeLock = lock.writeLock(); try { System.out.println(Thread.currentThread().getName() + " 正在写操作"); // 进行写操作 number += 14; } finally { // 释放锁 lock.unlockWrite(writeLock); } System.out.println(Thread.currentThread().getName() + "写操作完成"); } // 悲观读 public void readLock() { // 加锁 long readLock = lock.readLock(); System.out.println(Thread.currentThread().getName() + "进行读操作"); try { for (int i = 0; i < 4; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "正在读取================="); } int result = number; System.out.println(Thread.currentThread().getName() + "读取成功 =====" + result); } catch (RuntimeException e) { throw new RuntimeException(e); } finally { // 释放锁 lock.unlockRead(readLock); } } public void optimisticReadLock() { // 加锁 long read = lock.tryOptimisticRead(); System.out.println(Thread.currentThread().getName() + "正在进行读取"); System.out.println(Thread.currentThread().getName() + " 验证是否修改 " + lock.validate(read)); for (int i = 0; i < 4; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + " 查看数据是否被修改 " + lock.validate(read)); } // 判断是否修改数据 if (!lock.validate(read)) { // 开启悲观读 long readLock = lock.readLock(); System.out.println(Thread.currentThread().getName() + " 悲观读 "); int result = number; System.out.println(Thread.currentThread().getName() + " 悲观锁 读取成功 " + result); // 释放锁 lock.unlockRead(readLock); } else { int result = number; System.out.println(Thread.currentThread().getName() + " 乐观锁 读取成功 ============" + result); } } }
- 读的过程中也允许写进入
4.4 StampedLock的缺点
- StampedLock不支持重入,没有Reentarnt开头
- StampedLock的悲观读锁和写锁都不支持条件变量(Confition)
- 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法
- 如果需要支持中断功能,一定使用可中断的悲观读锁readLockInterruptibly()和写锁writeLockInterruptibly()