JUC并发编程
1、什么是juc
JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题!
2、线程基础知识复习
-
1把锁:synchronized(后面细讲)
-
2个并:
-
并发:(concurrent):是在同一实体上的多个事件,是在一台机器上“同时”处理多个任务,同一时刻,其实是只有一个事情再发生。
-
并行(parallel):是在不同实体上的多个事件,是在多台处理器上同时处理多个任务,同一时刻,大家都在做事情,你做你的,我做我的,各干各的。
-
-
3个程
- 进程:在系统中运行的一个应用程序,每个进程都有它自己的内存空间和系统资源
- 线程:也被称为轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元。
- 管程序:Monitor(锁),也就是我们平常说的锁。Montior其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码,JVM中同步是基于进入和退出监视器(Monitor)来实现的,每一个实例对象都自带一个Monitor对象,Monitor对象和java对象一样一同创建并销毁,底层由C++语言实现
-
线程分类
-
用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作
-
守护线程:是一种特殊的线程为其他线程服务的,在后台默默地完成一些系统性的任务,比如垃圾回收线程就是最典型的例子。守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以假如当系统只剩下守护线程的时候,守护线程伴随着JVM一同结束工作。
注意:在没有特别配置的情况下,创建的线程都是用户线程
//守护线程案例 package juc; /** * @Author:xiaoqi * @creat 2023/8/5 15:30 */ public class DaemonDemo { public static void main(String[] args) { Thread thread = new Thread(()->{ System.out.println("我是一个守护线程"); for (;;) { } },"daemon"); //设置该线程为守护线程 thread.setDaemon(true); thread.start(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主线程结束》》》》》》》》》》》》》》》"); } } 输出结果: 我是一个守护线程 主线程结束》》》》》》》》》》》》》》》 Process finished with exit code 0
-
3、CompletableFuture
3.1 Future接口理论知识
Future接口(FutureTask实现类)定义了异步任务执行的一些方法,如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等
举例:主线程让一个子线程去执行任务,子线程比较耗时,启动子线程开始执行任务后,主线程就去完成其他业务,等到主线程其他业务执行完毕,过了一会再去向子线程获取执行结果或任务状态(老师上课时间想喝水,但是又不能结束课程去买水,然后可以让一个学生替他买水,自己接着讲课,这样就不耽误时间)。
Future接口结构
3.2 Future接口常用实现类FutureTask异步任务
3.2.1 Future接口能干什么
Future是Java5新加的一个接口,它提供一种异步并行计算的功能,如果主线程需要执行一个很耗时的计算任务,我们会就可以通过Future把这个任务放进异步线程中执行,主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
3.2.2 Future 接口相关框架
-
**目的:**异步多线程执行,并且有返回结果。三大特点:多线程、返回结果、异步执行。
- 单独使用Runnable时:无法获得返回结果
- 单独使用Callable时:无法在新线程中(new Thread(Runnable r))使用,只能使用ExecutorService
Thread类只支持Runnable接口
- FutureTask:实现了Fure和Runnable,兼顾二者优点,既可以使用ExecutorService也可以使用Thread
-
代码实现:Runnable接口+Callable接口+Future接口和FutureTask实现类。
//FutureTask开启异步任务代码案例
package juc;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
/**
* @Author:xiaoqi
* @creat 2023/8/5 16:23
*/
public class FutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//FutureTask + Callable
FutureTask<String> callableFutureTask = new FutureTask<>(()->{
System.out.println("callableFutureTask come in.......");
return "hello word";
});
Thread thread = new Thread(callableFutureTask);
thread.start();
System.out.println(callableFutureTask.get());
}
}
3.2.3 Future 编码实战和相关优缺点分析
-
优点:Future+线程池异步多线程任务配合,能显著提高程序的运行效率。
-
缺点:
-
- get()阻塞—一旦调用get()方法求结果,一旦调用不见不散,非要等到结果才会离开,不管你是否计算完成,如果没有计算完成容易程序堵塞。
- isDone()轮询—轮询的方式会耗费无谓的cpu资源,而且也不见得能及时得到计算结果,如果想要异步获取结果,通常会以轮询的方式去获取结果,尽量不要阻塞。
-
结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果。
//Future获取结果get()和轮询
package juc;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author:xiaoqi
* @creat 2023/8/5 16:50
* Future获取结果get()和轮询
*/
public class FutureDrawBack {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(()->{
System.out.println("futureTask 开始处理任务-------");
//处理三秒的业务
Thread.sleep(3000);
return "task over";
});
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(Thread.currentThread().getName() +":开始执行其他任务---------");
while (true){
//轮询判断futureTask业务是否处理完成
if (futureTask.isDone()){
System.out.println(futureTask.get());
break;
}else {
System.out.println("futureTask任务暂时未完成,请等待............");
}
}
}
}
/**
* 执行结果
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* task over
*/
3.2.4 Future的总结及演化
-
对于简单的业务需求Future目前的api及功能完全够用
-
回调通知
-
- 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
-
- 通过轮询的方式去判断任务是否完成这样非常占cpu并且代码也不优雅
-
创建异步任务,可以使用 线程池+Future的方式
package juc;
import java.util.concurrent.*;
/**
* @Author:xiaoqi
* @creat 2023/8/5 16:50
* 线程池+Future
*/
public class FutureDrawBack {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
FutureTask<String> futureTask = new FutureTask<>(()->{
System.out.println("futureTask 开始处理任务-------");
//处理三秒的业务
Thread.sleep(3000);
return "task over";
});
// Thread thread = new Thread(futureTask);
// thread.start();
executorService.submit(futureTask);
System.out.println(Thread.currentThread().getName() +":开始执行其他任务---------");
while (true){
//轮询判断futureTask业务是否处理完成
if (futureTask.isDone()){
System.out.println(futureTask.get());
break;
}else {
System.out.println("futureTask任务暂时未完成,请等待............");
}
}
}
}
/**
* 执行结果
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* futureTask任务暂时未完成,请等待............
* task over
*/
Future对以下业务无能为力
-
多个任务前后依赖可以组合处理(水煮鱼—>买鱼—>调料—>下锅):
-
- 想将多个异步任务的结果组合起来,后一个异步任务的计算结果需要钱一个异步任务的值
- 想将两个或多个异步计算合并成为一个异步计算,这几个异步计算互相独立,同时后面这个又依赖前一个处理的结果
-
对计算速度选最快的:
-
- 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果
-
结论:
-
- 使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture以声明式的方式优雅的处理这些需求。
- 从i到i++
- Future能干的,CompletableFuture都能干
3.3 CompletableFuture出场
3.3.1 CompletableFuture为什么会出现
- get()方法在Future计算完成之前会一直处在阻塞状态下,阻塞的方式和异步编程的设计理念相违背。
- isDene()方法容易耗费cpu资源(cpu空转)
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果
jdk8设计出CompletableFuture,CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
3.3.2 CompletableFuture和CompletionStage介绍
类架构图
-
接口CompletionStage
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段。
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
-
类CompletableFuture
-
提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法
-
它可能代表一个明确完成的Future,也可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。
-
3.3.3 核心的四个静态方法
四个静态构造方法
对于上述Executor参数说明:若没有指定,则使用默认的ForkJoinPoolcommonPool()作为它的线程池执行异步代码**(此时若主线程运行结束,异步线程也会关闭)**
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码(这个时候当主线程停止,异步线程CompletableFuture不会停止,还会继续保持运行)
package juc;
import java.util.concurrent.*;
/**
* @Author:xiaoqi
* @creat 2023/8/5 17:14
* 四个静态方法的演示
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//使用CompletableFuture.runAsync创建completableFuture(没有返回值)
CompletableFuture completableFuture = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},executorService);
System.out.println(completableFuture.get());
//使用CompletableFuture.supplyAsync创建completableFuture(没有返回值)
CompletableFuture supplyCompletableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello supplyAsync";
},executorService);
System.out.println(supplyCompletableFuture.get());
}
}
/**
* 执行结果
* pool-1-thread-1
* null
* pool-1-thread-2
* hello supplyAsync
*/
CompletableFuture减少阻塞和轮询,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
package juc;
import java.util.concurrent.*;
/**
* @Author:xiaoqi
* @creat 2023/8/5 17:53
* CompletableFuture使用演示
*/
public class CompletableFutureUseDemo {
public static void main(String[] args) {
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "---come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (result > 5) { //模拟产生异常情况
int i = 10 / 0;
}
System.out.println("----------1秒钟后出结果" + result);
return result;
},executorService).whenComplete((result,throwable)->{
if (throwable == null){//当没有出现异常情况时
System.out.println("得到异步任务的执行结果:"+result);
}
}).exceptionally((e)->{//当出现异常情况时
e.printStackTrace();
System.out.println("异常情况:" + e.getCause() + " " + e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName() + "先去完成其他任务");
}
}
/**
* 异常情况运行结果:
* pool-1-thread-1---come in
* main先去完成其他任务
* java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
* 异常情况:java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
*
* 正常情况:
* pool-1-thread-1---come in
* main先去完成其他任务
* ----------1秒钟后出结果4
* 得到异步任务的执行结果:4
*/
CompletableFuture优点:
- 异步任务结束时,会自动回调某个对象的方法
- 主线程设置好回调后,不用关心异步任务的执行,异步任务之间可以顺序执行
- 异步任务出错时,会自动回调某个对象的方法
3.4 案例精讲——从电商网站的比价需求展开
3.4.1 函数式编程
Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程
函数式接口
-
Runnable(无参数,无返回值)
-
Function(一个参数,有返回值)
-
Consumer(一个参数,无返回值)
-
BiConsumer(两个参数,无返回值)
-
Supplier(无参数,有返回值)
小结
函数式接口名称 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无参数 | 无返回值 |
Function | apply | 1个参数 | 有返回值 |
Consumer | accept | 1个参数 | 无返回值 |
BiConsumer | accept | 2个参数 | 无返回值 |
Supplier | get | 无参数 | 有返回值 |
Chain链式调用
/**
* @Author:xiaoqi
* @creat 2023/8/5 17:53
*/
public class CompletableFutureMallDemo {
public static void main(String[] args) {
Student student = new Student();
student.setId(1).setStudentName("z3").setMajor("english"); //链式调用
}
}
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)//开启链式调用
class Student {
private Integer id;
private String studentName;
private String major;
}
3.4.2 业务需求
切记:功能—>性能(完成—>完美)
电商网站比价需求分析:
-
需求说明:
-
- 同一款产品,同时搜索出同款产品在各大电商平台的售价
- 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
-
输出返回:
-
- 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List
例如:《Mysql》 in jd price is 88.05 《Mysql》 in taobao price is 90.43
-
解决方案,对比同一个产品在各个平台上的价格,要求获得一个清单列表
-
- step by step,按部就班,查完淘宝查京东,查完京东查天猫…
- all in,万箭齐发,一口气多线程异步任务同时查询
package juc;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @Author:xiaoqi
* @creat 2023/8/6 10:51
*/
public class CompletableFutureMallDemo {
static List<NetMall> list = Arrays.asList(new NetMall("jd"), new NetMall("taobao"), new NetMall("dangdang"));
/**
* step by step
*
* @param list
* @param productName
* @return
*/
public static List<String> getPrice(List<NetMall> list, String productName) {
//《Mysql》 in jd price is 88.05
return list
.stream()
.map(netMall ->
String.format("《" + productName + "》" + "in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))
.collect(Collectors.toList());
}
/**
* all in 万箭齐发
*
* @param list
* @param productName
* @return
*/
public static List<String> getPriceByCompletableFuture(List<NetMall> list, String productName) {
return list.stream().map(netMall ->
CompletableFuture.supplyAsync(() ->
String.format("《" + productName + "》" + "in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))) //Stream<CompletableFuture<String>>
.collect(Collectors.toList()) //List<CompletableFuture<String>>
.stream()//Stream<String>
.map(s -> s.join()).collect(Collectors.toList()); //List<String>
}
public static void main(String[] args) {
/**
* 采用step by setp方式查询
*/
long StartTime = System.currentTimeMillis();
List<String> list1 = getPrice(list, "masql");
for (String element : list1) {
System.out.println(element);
}
long endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒");
/**
* 采用 all in三个异步线程方式查询
*/
long StartTime2 = System.currentTimeMillis();
List<String> list2 = getPriceByCompletableFuture(list, "mysql");
for (String element : list2) {
System.out.println(element);
}
long endTime2 = System.currentTimeMillis();
System.out.println("------costTime" + (endTime2 - StartTime2) + " 毫秒");
/**
* 运行结果
* 《masql》in jd price is 110.57
* 《masql》in taobao price is 109.49
* 《masql》in dangdang price is 109.53
* ------costTime: 3156 毫秒
* 《mysql》in jd price is 109.46
* 《mysql》in taobao price is 110.08
* 《mysql》in dangdang price is 109.80
* ------costTime1020 毫秒
*
* Process finished with exit code 0
*/
}
}
class NetMall {
private String netMallName;
public NetMall() {
}
public NetMall(String netMallName) {
this.netMallName = netMallName;
}
public String getNetMallName() {
return netMallName;
}
public void setNetMallName(String netMallName) {
this.netMallName = netMallName;
}
public double calcPrice(String productName) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
}
3.5 CompletableFuture常用方法
-
获得结果和触发计算
-
- 获取结果
-
-
- public T get() -->立即获取结果,如果未完成,则会一直阻塞直到完成任务
- public T get(long timeout,TimeUnit unit) -->设置等待超时时间,如果超时则抛出异常
- public T join() —>和get一样的作用,只是不需要抛出异常
- public T getNow(T valuelfAbsent) —>计算完成就返回正常值,否则返回备胎值(传入的参数),立即获取结果不阻塞
-
-
- 主动触发计算
-
-
- public boolean complete(T value) ---->是否打断get方法立即返回括号值
-
package juc; import java.sql.Time; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * @Author:xiaoqi * @creat 2023/8/6 11:15 */ public class CompletableFutureApiDemo { public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException { CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(()->{ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return "ok"; }); boolean b = completableFuture.complete("打断"); System.out.println(b); System.out.println(completableFuture.get(20,TimeUnit.MILLISECONDS)); System.out.println("不干了"); } }
-
-
-
对计算结果进行处理
-
- thenApply —>计算结果存在依赖关系,这两个线程串行化---->由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停
- handle —>计算结果存在依赖关系,这两个线程串行化---->有异常也可以往下走一步
-
package juc; import java.sql.Time; import java.util.concurrent.*; /** * @Author:xiaoqi * @creat 2023/8/6 11:15 */ public class CompletableFutureApiDemo { public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException { ExecutorService threadPool = Executors.newFixedThreadPool(3); CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return 1; }, threadPool).thenApply(f -> { System.out.println("222"); return f + 2; }).handle((f, e) -> { System.out.println("3333"); int i=10/0; return f + 2; // thenApply(f -> { // System.out.println("3333"); // return f + 2; }).whenComplete((v, e) -> { if (e == null) { System.out.println("----计算结果" + v); } }).exceptionally(e -> { e.printStackTrace(); System.out.println(e.getCause()); return null; }); System.out.println(Thread.currentThread().getName() + "------主线程先去做其他事情"); System.out.println(completableFuture.join()); } }
-
对计算结果进行消费
- thenAccept --> 接受任务的处理结果,并无返回值
package juc; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Author:xiaoqi * @creat 2023/8/6 11:29 */ public class CompletableFutureApi2Demo { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(3); CompletableFuture.supplyAsync(() -> { return 1; }, threadPool).thenAccept(r -> { System.out.println(r);//5 }); } }
-
thenAccept(Consumer action): 任务A执行完执行B,B需要A的结果,但是任务B没有返回值
-
thenRun(Runnable runnable) :任务A执行完执行B,并且不需要A的结果
-
thenApply(Function fn): 任务A执行完执行B,B需要A的结果,同时任务B有返回值
package juc; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Author:xiaoqi * @creat 2023/8/6 11:29 */ public class CompletableFutureApi2Demo { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(3); CompletableFuture.supplyAsync(() -> { return 1; }, threadPool).thenApply(r -> { System.out.println(r);//5 return r; }).thenRun(()->{ System.out.println("hello"); }); } }
-
- CompletableFuture和线程池说明
-
-
- 如果没有传入自定义线程池,都用默认线程池ForkJoinPool
- 传入一个线程池,如果你执行第一个任务时,传入了一个自定义线程池
-
-
-
-
- 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务时共用同一个线程池
- 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自定义的线程池,第二个任务使用的是ForkJoin线程池
-
-
-
-
- 备注:可能是线程处理太快,系统优化切换原则, 直接使用main线程处理,thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,之间的区别同理。
-
-
对计算速度进行选用
-
applyToEither–>两个异步任务,谁运行的快,就使用谁的运行结果
package juc; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @Author:xiaoqi * @creat 2023/8/6 11:40 */ public class CompletableFutureApi3Demo { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(3); CompletableFuture<String> completableFuture1 = CompletableFuture.supplyAsync(()->{ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return "completableFuture1 quickly"; },threadPool); CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(()->{ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return "completableFuture2 quickly"; },threadPool); CompletableFuture<String> future = completableFuture1.applyToEither(completableFuture2, r -> { return r + " is win"; }); System.out.println(future.join()); } } /** * 运行结果 :completableFuture1 quickly is win */
-
-
对计算结果进行合并
-
-
两个CompletableStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理
-
先完成的先等着,等待其他分支任务
package juc; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** * @Author:xiaoqi * @creat 2023/8/6 11:53 */ public class CompletableFutureApi4Demo { public static void main(String[] args) { CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + " 启动"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return 10; }); CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + " 启动"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } return 20; }); CompletableFuture<Integer> future = completableFuture1.thenCombine(completableFuture2, (a, b) -> { System.out.println("开始合并"); return a+b; }); System.out.println(future.join()); } } /** * 运行结果: * ForkJoinPool.commonPool-worker-1 启动 * ForkJoinPool.commonPool-worker-2 启动 * 开始合并 * 30 */
-
4、Java ”锁“ 事
4.1 悲观锁与乐观锁
-
乐观锁: 认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命—佛系锁
-
悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源-----狼性锁
4.2 八种情况演示锁运行案例
4.2.1 八锁案例演示代码
package juc;
import java.util.concurrent.TimeUnit;
/**
* @Author:xiaoqi
* @creat 2023/8/6 14:03
*/
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
/**
* 现象描述:
* 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁
* 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信 共用一个对象锁
* 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件
* 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件 资源没有争抢,不是同一个对象锁
* 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信 共用一个类锁
* 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁
* 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件 一个用类锁一个用对象锁
* 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMS();
}, "b").start();
}
}
结论:
-
对于普通同步方法,锁是当前对象(通常也就是this),所有的同步方法用的是同一把锁–>实例对象本身(对象锁)
-
对于静态同步方法,锁时当前类的Class对象(类锁)
-
对于同步代码块,锁是synchronized括号内的对象
4.2.2 synchronized的三种应用方式
- 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁
- 作用于代码块,对括号里配置的对象加锁
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
4.2.3 从字节码角度分析synchronized实现
-
javap -c(v附加信息) ***.class 文件反编译
-
synchronized同步代码块
-
-
实现使用的是monitorenter和monitorexit指令
-
-
synchronized普通同步方法
-
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor
-
-
synchronized静态同步方法
-
-
ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
-
4.2.4 反编译synchronized锁的是什么
面试题:为什么任何一个对象都可以成为一个锁?
C++源码:ObjectMonitor.java—>ObjectMonitor.cpp—>ObjectMonitor.hpp
每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来
总结:指针指向Monitor对象(也称为管程或监视器)的真实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要的数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现):
4.2.5 对于Synchronized关键字
后面章节详细探讨
4.3 公平锁与非公平锁
4.3.1 何为公平锁/非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的----- Lock lock = new ReentrantLock(true)—表示公平锁,先来先得。
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)---- Lock lock = new ReentrantLock(false)—表示非公平锁,后来的也可能先获得锁,默认为非公平锁。
面试题:
-
为什么会有公平锁/非公平锁的设计?为什么默认非公平?
-
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
-
什么时候用公平?什么时候用非公平?
-
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。
4.3.2 重要概念之AQS
后续章节详细探讨
4.4 可重入锁
4.4.1 概念
是指同一线程在外层方法获取到锁的时侯,再进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。
4.4.2 可重入锁的种类
-
隐式锁(即synchronized关键字使用的锁),默认是可重入锁
- 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
-
显式锁(即Lock)也有ReentrantLock这样的可重入锁
package juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author:xiaoqi
* @creat 2023/8/6 14:32
* 重入锁代码演示
*/
public class ReEntryLockDemo {
public static void main(String[] args) {
final Object o = new Object();
/**
* ---------------外层调用
* ---------------中层调用
* ---------------内层调用
*/
new Thread(() -> {
synchronized (o) {
System.out.println("---------------外层调用");
synchronized (o) {
System.out.println("---------------中层调用");
new ReEntryLockDemo().reEntryLock();
}
}
}, "t1").start();
/**
* 注意:加锁几次就需要解锁几次
* ---------------外层调用
* ---------------中层调用
* ---------------内层调用
*/
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("---------------外层调用");
lock.lock();
try {
System.out.println("---------------中层调用");
lock.lock();
try {
System.out.println("---------------内层调用");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t2").start();
}
public synchronized void reEntryLock(){
System.out.println("重入其他synchronized修饰的方法");
}
}
4.5 死锁
4.5.1 概念
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。
产生原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 系统资源分配不当
4.5.2 死锁代码案例
package juc;
import java.util.concurrent.TimeUnit;
/**
* @Author:xiaoqi
* @creat 2023/8/6 14:39
*/
public class DeadLockDemo {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a){
System.out.println("t1线程持有a锁,试图获取b锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("t1线程获取到b锁");
}
}
},"t1").start();
new Thread(() -> {
synchronized (b){
System.out.println("t2线程持有b锁,试图获取a锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("t2线程获取到a锁");
}
}
},"t2").start();
}
}
4.5.3 排查死锁
-
纯命令
-
- jps -l
- jstack 进程编号
-
图形化
-
- jconsole
4.6 写锁(独占锁)/读锁(共享锁)
见最后一章节详讲
4.7 自旋锁spinLock
见CAS(自旋锁原理)详讲
4.8 无锁->独占锁->读写锁->邮戳锁
见最后一章节详讲
4.9 无锁->偏向锁->轻量锁->重量锁
见Synchronized与锁升级详讲
5、LockSupport与线程中断
5.1 线程中断机制
5.1.1 从阿里蚂蚁金服面试题讲起
Java.lang.Thread下的三个方法:
- 如何中断一个运行中的线程?
- 如何停止一个运行中的线程?
5.1.2 什么是中断机制
-
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运,所以,Thread.stop,Thread.suspend,Thread.resume都已经被废弃了
-
其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制----中断,也即中断标识协商机制
-
- 中断只是一种协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自行实现。若要中断一个线程,你需要手动调用该线程interrupt()方法,该方法也仅仅是将该线程对象的中断标识设置为true,接着你需要自己写代码不断检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟应该做什么需要自己写代码实现。
- 每个线程对象都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设置为true;可以在别的线程中调用,也可以在自己的线程中调用。
5.1.3 中断的相关API方法之三大方法说明
-
public void interrupt()
- 实例方法讲中断标识设置为ture( Just to set the interrupt flag)
- 该方法仅仅是设置中断标识位为ture,发起一个协商而不会立刻停止线程
-
public static boolean interrupted()
-
- 静态方法 Thread.interrupted();
- 判断线程是否被中断并清除当前中断状态(做了两件事情)
-
-
- 1.返回当前线程的中断状态,测试当前线程是否已被中断
- 2.将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
- 3.这个方法有点不好理解在于如果连续两次调用此方法,则第二次返回false,因为连续调用两次的结果可能不一样
-
-
public boolean isInterrupted()
-
- 实例方法
- 判断当前线程是否被中断(通过检查中断标志位)
5.1.4 面试题中断机制考点
-
如何中断一个运行中线程
-
通过一个volatile变量实现
package juc; import java.util.concurrent.TimeUnit; /** * @Author:xiaoqi * @creat 2023/8/6 15:00 * 使用volatile修饰一个标识符来决定是否结束线程 */ public class InterruptDemo { static volatile boolean isStop = false; //volatile表示的变量具有可见性 public static void main(String[] args) { new Thread(() -> { while (true) { if (isStop) { System.out.println(Thread.currentThread().getName() + " isStop的值被改为true,t1程序停止"); break; } System.out.println("-----------hello volatile"); } }, "t1").start(); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { isStop = true; }, "t2").start(); } }
-
通过原子类AutomicBoolean实现
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * @Author:xiaoqi * @creat 2023/8/6 15:06 */ public class InterruptDemoByAtomicBoolean { static AtomicBoolean isStop = new AtomicBoolean(false); public static void main(String[] args) { new Thread(() -> { while (true) { if (isStop.get()) { System.out.println(Thread.currentThread().getName() + " atomicBoolean的值被改为true,t1程序停止"); break; } System.out.println("-----------hello atomicBoolean"); } }, "t1").start(); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { isStop.set(true); }, "t2").start(); } }
-
通过Thread类自带的中断API实例方法实现----在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。
package juc; import java.util.concurrent.TimeUnit; /** * @Author:xiaoqi * @creat 2023/8/6 15:11 * 使用interrupt() 和isInterrupted()组合使用来中断某个线程 */ public class InterruptDemoByApi { public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " isInterrupted()的值被改为true,t1程序停止"); break; } System.out.println("-----------hello isInterrupted()"); } }, "t1"); t1.start(); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //t2向t1放出协商,将t1中的中断标识位设为true,希望t1停下来 new Thread(() -> t1.interrupt(), "t2").start(); //当然,也可以t1自行设置 t1.interrupt(); } }
-
-
当前线程的中断标识为true,是不是线程就立刻停止?
答案是不立刻停止,具体来说,当对一个线程,调用interrupt时:
-
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响,所以interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行,对于不活动的线程没有任何影响。
-
-
如果线程处于阻塞状态(例如sleep,wait,join状态等),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(interrupt状态也将被清除),并抛出一个InterruptedException异常。
package juc; import java.util.concurrent.TimeUnit; /** * @Author:xiaoqi * @creat 2023/8/6 15:20 * 1. 中断标志位默认为false * 2.t2对t1发出中断协商 t1.interrupt(); * 3. 中断标志位为true: 正常情况 程序停止 * * 中断标志位为true 异常情况,.InterruptedException ,将会把中断状态清楚,中断标志位为false * 4。需要在catch块中,再次调用interrupt()方法将中断标志位设置为false; */ public class InterruptDemo2 { public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " 中断标志位为:" + Thread.currentThread().isInterrupted() + " 程序停止"); break; } //sleep方法抛出InterruptedException后,中断标识也被清空置为false,如果没有在 //catch方法中调用interrupt方法再次将中断标识置为true,这将导致无限循环了 try { Thread.sleep(200); } catch (InterruptedException e) { //Thread.currentThread().interrupt(); e.printStackTrace(); } System.out.println("-------------hello InterruptDemo3"); } }, "t1"); t1.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { t1.interrupt(); }, "t2").start(); } } /** * 运行结果: * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 * java.lang.InterruptedException: sleep interrupted * at java.lang.Thread.sleep(Native Method) * at juc.InterruptDemo2.lambda$main$0(InterruptDemo2.java:25) * at java.lang.Thread.run(Thread.java:748) * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 * -------------hello InterruptDemo3 *结论:在阻塞状态下调用当前线程对象的interrupt方法,会立刻将线程唤醒并抛出异常同时中断标识也被清空置为false,线程继续 *运行 */
-
通过源码分析原因
-
-
总之需要记住的是中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断
-
静态方法Thread.interrupted(),谈谈你的理解?
-
查看源码方法介绍
-
代码演示
/** * @Author:xiaoqi * @creat 2023/8/6 15:20 */ public class InterruptDemo4 { public static void main(String[] args) { /** * main false * main false * -----------1 * -----------2 * main true * main false */ System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());//false System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());//false System.out.println("-----------1"); Thread.currentThread().interrupt(); System.out.println("-----------2"); System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());//true System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());//false } }
-
对于静态方法Thread.interrupted()和实例方法isInterrupted()区别在于:
-
静态方法interrupted将会清除中断状态(传入的参数ClearInterrupted为true)
-
实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)
-
-
5.1.5 总结
- public void interrupt() 是一个实例方法,它通知目标线程中断,也仅仅是设置目标线程的中断标志位为true
- public boolean isInterrupted() 是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
- public static boolean interrupted() 是一个静态方法,返回当前线程的中断真实状态(boolean类型)后会将当前线程的中断状态设为false,此方法调用之后会清楚当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置为false。
5.2 LockSupport概念
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,其中park()和unpack()的作用分别是阻塞线程和解除阻塞线程.
5.3 线程等待唤醒机制
5.3.1 三种让线程等待和唤醒的方法
- 方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
- 方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
5.3.2 前两种方式实现线程等待和唤醒
-
使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
-
wait和notify方法必须要在同步代码块或者方法里面,且成对出现使用
-
先wait再notify才ok
package juc; import java.util.concurrent.TimeUnit; /** * @Author:xiaoqi * @creat 2023/8/6 16:18 * 使用**Object中的wait()**方法让线程等待,使用**Object中的notify()**方法唤醒线程 */ public class LockSupportDemo1 { public static void main(String[] args) { Object objectLock = new Object(); /** * t1 -----------come in * t2 -----------发出通知 * t1 -------被唤醒 */ new Thread(() -> { synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t -----------come in"); try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t -------被唤醒"); } }, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { synchronized (objectLock) { objectLock.notify(); System.out.println(Thread.currentThread().getName() + "\t -----------发出通知"); } }, "t2").start(); } }
-
-
使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
-
Condition中的线程等待和唤醒方法,需要先获取锁
-
一定要先await后signal,不要反了
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @Author:xiaoqi * @creat 2023/8/6 16:20 * 使用**JUC包中的Condition的await()**方法让线程等待,使用**signal()**方法唤醒线程 */ public class LockSupportDemo2 { public static void main(String[] args) { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t -----------come in"); condition.await(); System.out.println(Thread.currentThread().getName() + "\t -----------被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t -----------发出通知"); } finally { lock.unlock(); } }, "t2").start(); } }
-
5.3.3 上述两种方式的使用限制条件
- 线程需要先获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
5.3.4 LockSupport类中的park等待和unpark唤醒
-
是什么
-
- LockSupport 是用于创建锁和其他同步类的基本线程阻塞原语
- LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只能有一个,累加上限是1。
-
主要方法
-
- 阻塞: Permit许可证默认没有不能放行,所以一开始调用park()方法当前线程会阻塞,直到别的线程给当前线程发放peimit,park方法才会被唤醒。
-
-
- park/park(Object blocker)-------阻塞当前线程/阻塞传入的具体线程
-
-
- 唤醒: 调用unpack(thread)方法后 就会将thread线程的许可证peimit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。
-
-
- unpark(Thread thread)------唤醒处于阻塞状态的指定线程
-
package juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @Author:xiaoqi
* @creat 2023/8/6 16:26
* LockSupport类中的park等待和unpark唤醒
*/
public class LockSupportDemo {
public static void main(String[] args) {
/**
* t1 -----------come in
* t2 ----------发出通知
* t1 ----------被唤醒
*/
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t -----------come in");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒");
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
LockSupport.unpark(t1);//指定需要唤醒的线程,可以先给t1发放许可证,t1再被锁定,此时t1可以立马被唤醒
System.out.println(Thread.currentThread().getName() + "\t ----------发出通知");
}, "t2").start();
}
}
-
重点说明(重要)
-
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,所有的方法都是静态方法,可以让线程再任意位置阻塞,阻塞后也有对应的唤醒方法。归根结底,LockSupport时调用Unsafe中的native代码
- LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程,LockSupport和每个使用它的线程都有一个许可(Peimit)关联,每个线程都有一个相关的permit,peimit最多只有一个,重复调用unpark也不会积累凭证。
- 形象理解:线程阻塞需要消耗凭证(Permit),这个凭证最多只有一个
-
-
- 当调用park时,如果有凭证,则会直接消耗掉这个凭证然后正常退出。如果没有凭证,则必须阻塞等待凭证可用;
- 当调用unpark时,它会增加一个凭证,但凭证最多只能有1各,累加无效。
-
-
面试题
-
- 为什么LockSupport可以突破wait/notify的原有调用顺序?
-
-
- 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞,先发放了凭证后续可以畅通无阻。
-
-
- 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
-
-
- 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证,而调用两次park却需要消费两个凭证,证不够,不能放行。
-
6、Java内存模型之JMM
6.1 先从面试题开始
- 你知道什么是Java内存模型JMM吗?
- JMM和volatile他们两个之间的关系?
- JMM没有那些特征或者它的三大特征是什么?
- 为什么要有JMM,它为什么出现?作用和功能是什么?
- happens-before先行并发原则你有了解过吗?
6.2 计算机硬件存储体系
CPU的运行并不是直接操作内存而是先把内存里面的数据读到缓存,而内存的读和写操作的时候会造成不一致的问题。JVM规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序再各种平台下都能达到一致性的内存访问效果。
6.3 JMM
JMM(Java内存模型Java Memory Model)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
能干吗?
- 通过JMM来实现线程和主内存之间的抽象关系
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致性的内存访问效果。
6.4 JMM规范下三大特性
-
可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。
系统中主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现**“脏读”,所以每个线程都有自己的工作内存**,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在线程自己的工作内存中进行,而不能够直接写入主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
线程脏读
主内存中有变量X,初始值为0 |
---|
线程A要将X加1,先将X=0拷贝到自己的私有内存中,然后更新X的值 |
线程A将更新后的X值回刷到主内存的时间是不固定的 |
刚好在线程A没有回刷x到主内存时,线程B同样从主内存中读取X,此时为0,和线程A一样的操作,最后期盼的X=2就会变成X=1 |
-
原子性:指一个操作是不可被打断的,即多线程环境下,操作不能被其他线程干扰
-
有序性:对于一个线程的执行代码而言,我们总是习惯性地认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序话执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
-
- 优缺点:
- JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令更符合CPU的执行特性,最大限度的发挥机器性能。
- 但是指令重排可以保证串行语义一致,但没有义务保证多线程的语义也一致(即可能产生“脏读”),简单而言就是两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
- 优缺点:
-
从源码到最终执行示例图:
-
-
单线程环境里确实能够保证程序最终执行结果和代码顺序执行的结果一致
-
处理器在进行重排序时必须考虑到指令之间的数据依赖性
-
多线程环境中线程交替执行,由于编译器优化重排的存在,可能出现乱序现象,两个线程使用的变量能否保证一致性是无法确定的,结果无法预测。
6.5 JMM规范下多线程对变量的读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读写赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
JMM定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存中(从硬件角度讲就是内存条)
- 每个线程都有一个自己的本地工作内存,本地工作内存中存储了该线程用来读写共享变量的副本(从硬件角度来说就是CPU的缓存)
小总结:
-
我们定义的所有共享变量都储存在物理主内存中
-
每个线程都有自己独立的工作内存,里面保证该线程使用到的共享变量的副本(主内存中该变量的一份拷贝)
-
线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存在读写(不能越级)
-
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能互相访问)。
6.6 JMM规范下多线程先行发生原则之happens-before
在JVM中,如果一个操作执行的结果需要对另一个操作可见或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则,逻辑上的先后关系。
6.6.1 案例说明
x=5 |
---|
y=x |
上述称之为:写后读 |
问题?
y是否等于5呢?如果线程A的操作(x=5)happens-before(先行发生)线程B的操作(y=x),那么可以确定线程B执行y=5一定成立;如果他们不存在happens-before原则,那么y=5不一定成立这就是happens-before原则的为例----------->包含可见性和有序性的约束
6.6.2 先行并发原则说明
如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点。
我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下,有一个“先行发生”(happens-before)的原则限制和规矩,给你理好了规矩!
这个原则非常重要:它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型晦涩难懂的底层编译原理之中。
6.6.3 happens-before总原则
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 如果两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
6.6.4 happens-before之8条
从JDK 5开始,Java使用新的JSR-133内存模型,提供了 happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:
-
次序规则:一个线程内,按照代码的顺序,写在前面的操作先行发生于写在后面的操作,也就是说前一个操作的结果可以被后续的操作获取(保证语义串行性,按照代码顺序执行)。比如前一个操作把变量x赋值为1,那后面一个操作肯定能知道x已经变成了1
-
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作(后面指时间上的先后)。
-
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的后面同样指时间上的先后
-
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
-
线程启动规则(Thread start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
-
线程中断规则(Thread Interruption Rule):
-
- 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 可以通过Thread.interrupted()检测到是否发生中断
- 也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发生
7.线程终止规则(Thread Termination Rule):线程中的所有操作都优先发生于对此线程的终止检测,我们可以通过isAlive()等手段检 测线程是否已经终止执行。
8.对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始------->对象没有完成初始化之前,是不能调用finalized()方法的
6.6.5 happens-before小总结
-
在Java语言里面,Happens-before的语义本质上是一种可见性
-
A happens-before B ,意味着A发生过的事情对B而言是可见的,无论A事件和B事件是否发生在同一线程里
-
JVM的设计分为两部分:
-
- 一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了
- 另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提升性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序,我们只要关注前者就好了,也就是理解happens-before规则即可,其他繁杂的内容由JMM规范结合操作系统给我们搞定,我们只写好代码即可。
6.6.6 代码案例
private int value =0;
public int getValue(){
return value;
}
public int setValue(){
return ++value;
}
问题描述:假设存在线程A和B,线程A先(时间上的先后)调用了setValue()方法,然后线程B调用了同一个对象的getValue()方法,那么线程B收到的返回值是什么?
答案:不一定
分析happens-before规则(规则5,6,7,8可以忽略,和代码无关)
1 由于两个方法由不同线程调用,不满足一个线程的条件,不满足程序次序规则
2 两个方法都没有用锁,不满足锁定规则
3 变量没有使用volatile修饰,所以不满足volatile变量规则
4 传递规则肯定不满足
综上:无法通过happens-before原则推导出线程A happens-before 线程B,虽然可以确定时间上线程A优于线程B,但就是无法确定线程B获得的结果是什么,所以这段代码不是线程安全的
注意:
- 如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证他们的有序性,虚拟机可以随意对他们进行重排序
解决方案
-
把getter/setter方法都定义为synchronized方法------->不好,重量锁,并发性下降
private int value =0; public synchronized int getValue(){ return value; } public synchronized int setValue(){ return ++value; }
-
把Value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
/** * 利用volatile保证读取操作的可见性, * 利用synchronized保证符合操作的原子性结合使用锁和volatile变量来减少同步的开销 */ private volatile int value =0; public int getValue(){ return value; } public synchronized int setValue(){ return ++value; }
7、volatile与JMM
7.1 被volatile修饰的变量有两大特点
-
特点:
-
- 可见性
- 有序性:有排序要求,有时需要禁重排
-
内存语义:
-
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量的值
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
-
volatile凭什么可以保证可见性和有序性?
-
- 内存屏障Memory Barrier
7.2 内存屏障(重点)
7.2.1 生活案例
-
没有管控,顺序难保
-
设定规则,禁止乱序---->上海南京路武警当红灯
-
再说vilatile两大特性:
-
- 可见:写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面所有线程可见
- 有序性(禁重排):
-
-
- 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序,若不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序;但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
-
7.2.2 是什么
内存屏障(也称内存栅栏,屏障指令等)是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性
- 内存屏障之前的所有写操作都要回写到主内存
- 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store buffers)中的数据同步到主内存,也就是说当看到Store屏障指令,就必须把该指令之前的所有写入指令执行完毕才能继续往下执行读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。
7.2.3 内存屏障分类
-
粗分为两种
- 读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存 当中的缓存数据失效,重新回到主内存中获取最新数据。
- 写屏障(Store Barrier):在写指令之后插入写屏障,强制把缓冲区的数据刷回到主内存中。
-
细分为四种
屏障类型 指令类型 说明 LoadLoad Load1;LoadLoad;Load2 保证Load1的读取操作在Load2及后续读取操作之前执行 StoreStore Store1;StoreStore;Store2 在store2及其后的写操作执行前,保证Store1的写操作已经刷新到主内存 LoadStore Load1;LoadStore;Store2 在Store2及其后的写操作执行前,保证Load1的读操作已经结束 StoreLoad Store1;StoreLoad;Load2 保证Store1的写操作已经刷新到主内存后,Load2及其后的读操作才能执行
7.2.4 困难内容
-
什么叫保证有序性?----->通过内存屏障禁重排
-
- 重排序有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM别自动重排序,我这里不需要重排序,一切听我的。
- 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序
- 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。
-
happens-before之volatile变量规则
第一个操作 | 第二个操作:普通读写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
---|---|---|---|
普通读写 | 可以重排 | 可以重排 | 不可以重排 |
volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序,这个操作保证了volatile读之后的操作不会被重排到volatile读之前。 |
---|
当第一个操作为volatile写时,第二个操作为volatile读时,不能重排 |
当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序,这个操作保证了volatile写之前的操作不会被重排到volatile写之后 |
-
JMM就将内存屏障插入策略分为4种规则
-
读屏障:在每个volatile读操作的后面插入一个LoadLoad屏障或者LoadStore屏障
-
-
-
写屏障:在每个volatile写操作的前面插入StoreStore屏障;在每个volatile写操作的后面插入StoreLoad屏障
-
7.3 volatile特性
7.3.1 保证可见性
保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见
-
Code
-
- 不加volatile,没有可见性,程序无法停止
- 加了volatile,保证可见性,程序可以停止
/**
* @Author:xiaoqi
* @creat 2023/8/6 16:26
*/
public class VolatileSeeDemo {
/**
* t1 -------come in
* main 修改完成
* t1 -------flag被设置为false,程序停止
*/
static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t-------come in");
while (flag) {
}
System.out.println(Thread.currentThread().getName() + "\t-------flag被设置为false,程序停止");
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新flag值
flag = false;
System.out.println(Thread.currentThread().getName() + "\t 修改完成");
}
}
-
volatile变量的读写过程(了解即可)
7.3.2 volatile不具有原子性
volatile变量的符合操作不具有原子性
-
对于voaltile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅仅是数据加载时是最新的。但是多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须加锁同步。
-
至于怎么去理解这个写丢失的问题,就是再将数据读取到本地内存到写回主内存中有三个步骤:数据加载---->数据计算---->数据赋值,如果第二个线程在第一个线程读取旧值与写回新值期间读取共享变量的值,那么第二个线程将会与第一个线程一起看到同一个值,并执行自己的操作,一旦其中一个线程对volatile修饰的变量先行完成操作刷回主内存后,另一个线程会作废自己的操作,然后重新去读取最新的值再进行操作,这样的话,它自身的那一次操作就丢失了,这就造成了 线程安全失败,因此,这个问题需要使用synchronized修饰以保证线程安全性。
-
结论:volatile变量不适合参与到依赖当前值的运算,如i++,i=i+1之类的,通常用来保存某个状态的boolean值或者int值,也正是由于volatile变量只能保证可见性,在不符合以下规则的运算场景中,我们仍然要通过加锁来保证原子性:
-
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
-
面试回答为什么不具备原子性:举例i++的例子,在字节码文件中,i++分为三部,间隙期间不同步非原子操作
-
- 对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,也就造成了线程安全问题。
7.3.3 指令禁重排
- 在每一个volatile写操作前面插入一个StoreStore屏障—>StoreStore屏障可以保证在volatile写之前,其前面所有的普通写操作都已经刷新到主内存中。
- 在每一个volatile写操作后面插入一个StoreLoad屏障—>StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
- 在每一个volatile读操作后面插入一个LoadLoad屏障—>LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
- 在每一个volatile读操作后面插入一个LoadStore屏障—>LoadTore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序
- 案例说明(volatile读写前或后加了屏障保证有序性):
7.4 正确的使用volatile
-
单一赋值可以,但是含复合运算赋值不可以(i++之类的)
- volatile int a = 10;
- volatile boolean flag = true;
-
状态标志,判断业务是否结束
-
作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
-
-
开销较低的读写锁策略
-
当读多于写,结合使用内部锁和volatile变量来减少同步的开销
-
原理是:利用volatile保证读操作的可见性,利用synchronized保证符合操作的原子性
-
-
DCL双端锁的发布
-
问题描述:首先设定一个加锁的单例模式场景
package juc; /** * @Author:xiaoqi * @creat 2023/8/6 18:59 * 双端检索的单例模式(懒汉式) */ public class SafeDoubleCheckSingleton { private static SafeDoubleCheckSingleton safeDoubleCheckSingleton; private SafeDoubleCheckSingleton(){ } public SafeDoubleCheckSingleton getInstance(){ if (safeDoubleCheckSingleton == null){ synchronized (SafeDoubleCheckSingleton.class){ if (safeDoubleCheckSingleton == null){ //不加volatile会出现隐患, // 在多线程环境下,由于指令重排序,可能该对象还未完成初始化,就被其他对象读取 safeDoubleCheckSingleton = new SafeDoubleCheckSingleton(); } } } return safeDoubleCheckSingleton; } }
-
单线程环境下(或者说正常情况下),在“问题代码处”,会执行以下操作,保证能获取到已完成初始化的实例:
-
多线程环境下,在“问题代码处”,会执行以下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象,其中第3步中实例化分多步执行(分配内存空间、初始化对象、将对象指向分配的内存空间),某些编译器为了性能原因,会将第二步和第三步重排序,这样某个线程肯能会获得一个未完全初始化的实例:
-
线程下的解决方案:加volatile修饰
package juc; /** * @Author:xiaoqi * @creat 2023/8/6 18:59 * 双端检索的单例模式(懒汉式) */ public class SafeDoubleCheckSingleton { //用volatile修饰,实现线程安全的延迟初始化 private volatile static SafeDoubleCheckSingleton safeDoubleCheckSingleton; private SafeDoubleCheckSingleton(){ } public SafeDoubleCheckSingleton getInstance(){ if (safeDoubleCheckSingleton == null){ synchronized (SafeDoubleCheckSingleton.class){ if (safeDoubleCheckSingleton == null){ //加volatile后, //由于禁止指令重排序,所以会先完成初始化,再为对象分配内存内存地址 safeDoubleCheckSingleton = new SafeDoubleCheckSingleton(); } } } return safeDoubleCheckSingleton; } }
-
7.5 本章总结
7.5.1 volatile可见性
volatile关键字保证可见性: | 对一个被volatile关键字修饰的变量 |
---|---|
1 | 写操作的话,这个变量的最新值会立即刷新回到主内存中 |
2 | 读操作的话,总是能够读取到这个变量的最新值,也就是这个变量最后被修改的值 |
3 | 当某个线程收到通知,去读取volatile修饰的变量的值的时候,线程私有工作内存的数据失效,需要重新回到主内存中去读取最新的数据。 |
7.5.2 volatile没有原子性
7.5.3 volatile禁重排
7.5.4 凭什么我们Java写了一个volatile关键字,系统底层加入内存屏障?两者的关系如何勾搭?
7.5.5 内存屏障是什么?
是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也称为内存栅栏或栅栏指令。
7.5.6 内存屏障能干吗?
- 阻止屏障两边的指令重排序
- 写操作时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
- 读操作时加入屏障,线程私有工作内存的数据失效,重新回到主物理内存中获取最新值
7.5.7 内存屏障四大指令
7.5.8 3句话总结
- volatile写之前的操作,都禁止重排序到volatile之后
- volatile读之后的操作,都禁止重排序到volatile之前
- volatile写之后volatile读,禁止重排序
8、CAS
8.1 原子类
Java.util.concurrent.atomic
8.2 在没有CAS之前
多线程环境中不使用原子类保证线程安全i++(基本数据类型),写操作都得加锁
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
8.3 使用CAS之后
多线程环境中使用原子类保证线程安全i++(基本数据类型)---------->类似于乐观锁
package juc;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author:xiaoqi
* @creat 2023/8/7 22:20
*/
public class AtomicDemo {
AtomicInteger atomicInteger = new AtomicInteger(10);
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public void increment() {
atomicInteger.incrementAndGet();
}
public int getCount() {
return atomicInteger.get();
}
}
8.4 CAS是什么
CAS(compare and swap),中文翻译为比较并交换,实现并发算法时常用到的一种技术,用于保证共享变量的原子性更新,它包含三个操作数—内存位置、预期原值与更新值。
执行CAS操作的时候,将内存位置的值与预期原值进行比较:
- 如果相匹配,那么处理器会自动将该位置更新为新值
- 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
代码演示
package juc;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author:xiaoqi
* @creat 2023/8/7 22:25
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,6)?"更新成功":"更新失败");
System.out.println(atomicInteger.compareAndSet(5,6)?"更新成功":"更新失败");
}
}
compareAndSet 源码
8.5 CAS底层原理?谈谈对Unsafe类的理解?
8.5.1 Unsafe
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应任务。
问题:我们知道i++是线程不安全的,那AtomicInteger.getAndIncrement()如何保证原子性?
AtomicInteger类主要利用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升:
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
8.5.2 源码分析
8.5.3 底层汇编
JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用compxchg指令比较并更新变量值(原子性)
总结:
- CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
- 实现方式是基于硬件平台的汇编指令,在inter的CPU中,使用的是汇编指令compxchg指令
- 核心思想就是比较要更新变量V的值和预期值E,相等才会将V的值设为新值N,如果不相等自旋再来
8.6 原子引用
代码案例
package juc;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author:xiaoqi
* @creat 2023/8/9 22:20
*/
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<User> userAtomicReference = new AtomicReference<>();
User z3 = new User("z3", 22);
User li4 = new User("li4", 25);
userAtomicReference.set(z3);
System.out.println(userAtomicReference.compareAndSet(z3, li4) + "\t" + userAtomicReference.get().toString());//true User(userName=li4, age=25)
System.out.println(userAtomicReference.compareAndSet(z3, li4) + "\t" + userAtomicReference.get().toString());//false User(userName=li4, age=25)
}
}
class User {
String userName;
int age;
public User() {
}
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
8.7 CAS使用场景(自旋锁)
8.7.1 自旋锁
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋锁—字面意思自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
8.7.2 手写一个自旋锁
需求:利用CAS思想,手动实现一个自旋锁
通过CAS完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
package juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author:xiaoqi
* @creat 2023/8/9 22:28
* 简易自旋锁实现
*/
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t --------come in");
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void unLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName() + "\t --------task over,unLock.........");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unLock();
}, "A").start();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "B").start();
}
}
8.8 CAS的缺点
8.8.1 循环时间长,开销大
- 如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大开销
8.8.2 ABA问题
-
ABA问题怎么产生的?
-
- CAS算法实现一个重要前提需要提取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
- 比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期ok,然后线程1操作成功--------尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
-
版本号时间戳原子引用
-
-
单线程情况下代码案例
package juc; import java.util.concurrent.atomic.AtomicStampedReference; /** * @Author:xiaoqi * @creat 2023/8/9 22:40 */ class Book { private int id; private String bookName; public Book() { } public Book(int id, String bookName) { this.id = id; this.bookName = bookName; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getBookName() { return bookName; } public void setBookName(String bookName) { this.bookName = bookName; } } public class AtomicStampedReferenceDemo { public static void main(String[] args) { Book javaBook = new Book(1, "javaBook"); AtomicStampedReference<Book> atomicStampedReference = new AtomicStampedReference<>(javaBook, 1); System.out.println(atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp()); Book mysqlBook = new Book(2, "mysqlBook"); boolean b = atomicStampedReference.compareAndSet(javaBook, mysqlBook, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); b = atomicStampedReference.compareAndSet(mysqlBook, javaBook, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); System.out.println(b + "\t" + atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp()); } }
-
多线程情况下代码案例
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; /** * @Author:xiaoqi * @creat 2023/8/9 22:46 */ public class ABADemo { static AtomicInteger atomicInteger = new AtomicInteger(100); static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); public static void main(String[] args) { // abaHappen();//true 2023 /** * t3 首次版本号: 1 * t4 首次版本号: 1 * t3 2次版本号: 2 * t3 3次版本号: 3 * false 100 3 */ abaNoHappen(); } private static void abaNoHappen() { new Thread(() -> { //先得到最初版本号,在比较的时候判断是否更改的版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp); try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100, 101, stamp, atomicStampedReference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t" + "2次版本号: " + atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101, 100, stamp, atomicStampedReference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t" + "3次版本号: " + atomicStampedReference.getStamp()); }, "t3").start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1); System.out.println(b + "\t" + atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp()); }, "t4").start(); } private static void abaHappen() { new Thread(() -> { atomicInteger.compareAndSet(100, 101); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } atomicInteger.compareAndSet(101, 100); }, "t1").start(); new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicInteger.compareAndSet(100, 2023) + "\t" + atomicInteger.get());//true 2023 }, "t2").start(); } }
使用AtomicStampedReference带上版本号,解决ABA问题!!!
9、原子操作类
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
9.1 基本类型原子类
AtomicInteger
:整型原子类AtomicBoolean
:布尔型原子类AtomicLong
:长整型原子类
9.1.1 常用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) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
9.1.2 案例
package juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author:xiaoqi
* @creat 2023/8/12 10:25
* AtomicInteger案例演示
*/
class Number{
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus() {
atomicInteger.getAndIncrement();
}
}
public class AtomicIntegerDemo {
public static final int SIZE = 50;
public static void main(String[] args) throws InterruptedException {
Number number = new Number();
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
for (int i = 1; i <= SIZE; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 10; j++) {
number.addPlusPlus();
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + "result: " + number.atomicInteger.get());//main result: 500
}
}
9.2 数组型原子类
AtomicIntegerArray
:整型数组原子类AtomicLongrArray
:长整型数组原子类AtomicReferenceArray
:用类型数组原子类
9.2.1 常用API
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
9.2.2 案例
package juc;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* @Author:xiaoqi
* @creat 2023/8/12 10:35
* AtomicIntegerArray案例演示
*/
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
for (int i = 0; i < atomicIntegerArray.length(); i++) {
System.out.println(atomicIntegerArray.get(i));
}
System.out.println();
int tempInt = 0;
tempInt = atomicIntegerArray.getAndSet(0, 1122);
System.out.println(tempInt + "\t" + atomicIntegerArray.get(0));
tempInt = atomicIntegerArray.getAndIncrement(0);
System.out.println(tempInt + "\t" + atomicIntegerArray.get(0));
}
}
9.3 引用类型原子类
-
AtomicReference
:引用类型原子类 -
AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -
- 解决修改过几次
-
AtomicMarkableReference
:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来 -
-
解决是否修改过,它的定义就是将标记戳简化为true/false,类似于一次性筷子
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicMarkableReference; /** * @Author:xiaoqi * @creat 2023/8/12 10:40 * AtomicMarkableReference案例演示 */ public class AtomicMarkableReferenceDemo { static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference<>(100,false); public static void main(String[] args) { new Thread(()->{ boolean marked = atomicMarkableReference.isMarked(); System.out.println(Thread.currentThread().getName() + "\t" + "默认标识: " + marked);//t1 默 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicMarkableReference.compareAndSet(100,1000,marked,!marked); },"t1").start(); new Thread(()->{ boolean marked = atomicMarkableReference.isMarked(); System.out.println(Thread.currentThread().getName() + "\t" + "默认标识: " + marked); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } atomicMarkableReference.compareAndSet(100,2000,marked,!marked); boolean b = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked); System.out.println(Thread.currentThread().getName() + "\t" + "t2线程CASResult:" + b); System.out.println(Thread.currentThread().getName() + "\t" + atomicMarkableReference.isMarked());//t2 true System.out.println(Thread.currentThread().getName() + "\t" + atomicMarkableReference.getReference());//t2 1000 },"t2").start(); } } /** * 运行结果: * t1 默认标识: false * t2 默认标识: false * t2 t2线程CASResult:false * t2 true * t2 1000 */
-
9.4 对象属性修改原子类
AtomicIntegerFieldUpdater
:原子更新对象中int类型字段的值AtomicLongFieldUpdater
:原子更新对象中Long类型字段的值AtomicReferenceFieldUpdater
:原子更新对象中引用类型字段的值
9.4.1 使用目的
希望以一种线程安全的方式操作非线程安全的某些对象
9.4.2 使用要求
- 更新的对象属性必须使用public volatile修饰符
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
9.4.3 案例
package juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* @Author:xiaoqi
* @creat 2023/8/12 10:57
* AtomicIntegerFieldUpdater使用案例
* 需求:10个线程各自转账1000
*/
class BankAccount {
public volatile int money = 0;
AtomicIntegerFieldUpdater<BankAccount> atomicIntegerFieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
public void transferMoney(BankAccount bankAccount) {
atomicIntegerFieldUpdater.getAndIncrement(bankAccount);
}
}
public class AtomicIntegerFieldUpdaterDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 1000; j++) {
bankAccount.transferMoney(bankAccount);
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + '\t' + "result: " + bankAccount.money);
}
}
/**
* 运行结果
* main result: 10000
*/
package juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @Author:xiaoqi
* @creat 2023/8/12 11:02
* AtomicReferenceFieldUpdater案例演示
* 需求:
* 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作
* 要求只能被初始化一次,只有一个线程操作成功
*/
class MyVar {
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
public void init(MyVar myVar) {
if (referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)) {
System.out.println(Thread.currentThread().getName() + "\t" + "--------------start init ,need 2 secondes");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "--------------over init");
} else {
System.out.println(Thread.currentThread().getName() + "\t" + "--------------已经有线程进行初始化工作了。。。。。");
}
}
}
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
myVar.init(myVar);
}, String.valueOf(i)).start();
}
}
}
/**
* 运行结果
* 1 --------------start init ,need 2 secondes
* 5 --------------已经有线程进行初始化工作了。。。。。
* 4 --------------已经有线程进行初始化工作了。。。。。
* 3 --------------已经有线程进行初始化工作了。。。。。
* 2 --------------已经有线程进行初始化工作了。。。。。
* 1 --------------over init
*/
9.5 原子操作增强类原理解析
DoubleAccumulator
:一个或多个变量,它们一起保持运行double使用所提供的功能更新值DoubleAdder
:一个或多个变量一起保持初始为零double总和LongAccumulator
:一个或多个变量,一起保持使用提供的功能更新运行的值long ,提供了自定义的函数操作LongAdder
:一个或多个变量一起维持初始为零long总和(重点),只能用来计算加法,且从0开始计算
9.5.1 常用API
9.5.2 面试题
- 热点商品点赞计算器,点赞数加加统计,不要求实时精确
- 一个很大的list,里面都是int类型,如何实现加加,思路?
9.5.3 点赞计数器
/**
* @author Guanghao Wei
* @create 2023-04-13 12:19
* 需求:50个线程,每个线程100w此,总点赞数出来
*/
class ClickNumber {
int number = 0;
public synchronized void clickBySynchronized() {
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong() {
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
public void clickByLongAdder() {
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
public void clickByLongAccumulator() {
longAccumulator.accumulate(1);
}
}
public class AccumulatorCompareDemo {
public static final int _1W = 10000;
public static final int THREAD_NUMBER = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long StartTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_NUMBER);
CountDownLatch countDownLatch2 = new CountDownLatch(THREAD_NUMBER);
CountDownLatch countDownLatch3 = new CountDownLatch(THREAD_NUMBER);
CountDownLatch countDownLatch4 = new CountDownLatch(THREAD_NUMBER);
StartTime = System.currentTimeMillis();
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.clickBySynchronized();
}
} finally {
countDownLatch1.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒" + "\t clickBySynchronized: " + clickNumber.number);
StartTime = System.currentTimeMillis();
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.clickByAtomicLong();
}
} finally {
countDownLatch2.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒" + "\t clickByAtomicLong: " + clickNumber.atomicLong.get());
StartTime = System.currentTimeMillis();
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.clickByLongAdder();
}
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒" + "\t clickByLongAdder: " + clickNumber.longAdder.sum());
StartTime = System.currentTimeMillis();
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.clickByLongAccumulator();
}
} finally {
countDownLatch4.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒" + "\t clickByLongAccumulator: " + clickNumber.longAccumulator.get());
}
}
/**
* ------costTime: 1313 毫秒 clickBySynchronized: 50000000
* ------costTime: 825 毫秒 clickByAtomicLong: 50000000
* ------costTime: 92 毫秒 clickByLongAdder: 50000000
* ------costTime: 61 毫秒 clickByLongAccumulator: 50000000
*/
9.5.4 源码、原理分析
-
架构
-
-
原理(LongAdder为什么这么快)
-
- 如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
- LongAdder是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]中
-
-
-
源码解读深度分析
-
- LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value值拆分进这个数组cells。多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果
- add(1L)
-
-
- 1 如果Cells表为空,尝试用CAS更新base字段,成功则退出
- 2 如果Cells表为空,CAS更新base字段失败,出现竞争,uncontended为true,调用longAccumulate(新建数组)
- 3 如果Cells表非空,但当前线程映射的槽为空,uncontended为true,调用longAccumulate(初始化)
- 4 如果Cells表非空,且当前线程映射的槽非空,CAS更新Cell的值,成功则返回,否则,uncontended设为false,调用longAccumulate(扩容)
-
-
- longAccumulate
-
-
-
- sum
-
-
- sum()会将所有Cell数组中的value和base累加作为返回值。核心思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
- sum执行时,并没有限制对base和cells的更新,所以LongAdder不是强一致性的,它是最终一致性的,对cell的读取无法保证是最后一次写入的值,所以在没有并发的场景下,可以获得正确的结果。
-
-
使用总结
-
- AtomicLong线程安全,可允许一些性能损耗,要求高精度时可使用,保证精度,多个线程对单个热点值value进行了原子操作-----保证精度,性能代码
- LongAdder当需要在高并发场景下有较好的性能表现,且对值得精确度要求不高时,可以使用,LongAdder时每个线程拥有自己得槽,各个线程一般只对自己槽中得那个值进行CAS操作—保证性能,精度代价
9.5.5 总结
-
AtomicLong
-
- 原理:CAS+自旋
- 场景:低并发下的全局计算,AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性问题
- 缺陷:高并发后性能急剧下降----AtomicLong的自旋会成为瓶颈(N个线程CAS操作修改线程的值,每次只有一个成功过,其他N-1失败,失败的不停自旋直至成功,这样大量失败自旋的情况,一下子cpu就打高了)
-
LongAdder
-
- 原理:CAS+Base+Cell数组分散-----空间换时间并分散了热点数据
- 场景:高并发下的全局计算
- 缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确
10、ThreadLocal
10.1 ThreadLocal简介
10.1.1 面试题
- ThreadLocal中ThreadLocalMap的数据结构和关系
- ThreadLocal的key是弱引用,这是为什么?
- ThreadLocal内存泄漏问题你知道吗?
- ThreadLocal中最后为什么要加remove方法?
10.1.2 是什么?
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事物ID)与线程关联起来。
10.1.3 能干嘛?
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不用麻烦别人,不和其他人共享,人人有份,人各一份)。主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其改为当前线程所存的副本的值从而避免了线程安全问题。比如8锁案例中,资源类是使用同一部手机,多个线程抢夺同一部手机,假如人手一份不是天下太平?
10.1.4 API介绍
10.1.5 从HelloWord入门
- 问题描述:5个销售买房子,集团只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金--------群雄逐鹿起纷争------为了数据安全只能加锁
/**
* @author xiaoqi
* @create 2023/8/12 11:02
* 需求:5个销售卖房子,集团只关心销售总量的精确统计数
*/
class House {
int saleCount = 0;
public synchronized void saleHouse() {
saleCount++;
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
System.out.println(size);
for (int j = 1; j <= size; j++) {
house.saleHouse();
}
}, String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
}
}
/**
* 3
* 4
* 2
* 4
* 2
* main 共计卖出多少套: 15
*/
- 需求变更:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计-------比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和。----人手一份天下安
package juc;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @Author:xiaoqi
* @creat 2023/8/12 11:27
*/
class House {
int saleCount = 0;
public synchronized void saleHouse() {
saleCount++;
}
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void saleVolumeByThreadLocal() {
saleVolume.set(1 + saleVolume.get());
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
try {
for (int j = 1; j <= size; j++) {
house.saleHouse();
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
} finally {
house.saleVolume.remove();
}
}, String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
}
}
/**
* 运行结果
* 2 号销售卖出:3
* 4 号销售卖出:5
* 1 号销售卖出:1
* 5 号销售卖出:5
* 3 号销售卖出:2
* main 共计卖出多少套: 16
*/
10.1.6 总结
-
因为每个Thread内有自己的实例副本且该副本只有当前线程自己使用
-
既然其他ThreadLocal不可访问,那就不存在多线程间共享问题
-
统一设置初始值,但是每个线程对这个值得修改都是各自线程互相独立得
-
如何才能不争抢
-
- 加入synchronized或者Lock控制资源的访问顺序
- 人手一份,大家各自安好,没有必要争抢
10.2 ThreadLocal源码分析
10.2.1 Thread、ThreadLocal、ThreadLocalMap关系
- Thread和ThreadLocal,人手一份
-
- ThreadLocal和ThreadLocalMap
-
- 三者总概括
-
-
- ThreadLocalMap实际上就是一个以ThreadLocal实例为Key,任意对象为value的Entry对象
- 当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为Key,值为value的Entry往这个ThreadLocalMap中存放
10.2.2 总结
-
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
-
-
JVM内部维护了一个线程版的Map<ThreadLocal, Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当作Key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
10.3 ThreadLocal内存泄漏问题
10.3.1 什么是内存泄漏
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏
10.3.2 原因
-
回看ThreadLocalMap
-
强软弱虚引用
-
- 强引用:
-
-
- 对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄露的主要原因之一。
-
-
- 软引用:
-
-
- 是一种相对强引用弱化了一些的引用,对于只有软引用的对象而言,当系统内存充足时,不会被回收,当系统内存不足时,他会被回收,软引用通常用在对内存敏感的程序中,比如高速缓存,内存够用就保留,不够用就回收。
-
-
- 弱引用:
-
-
- 比软引用的生命周期更短,对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
-
-
- 软引用和弱引用的使用场景----->假如有一个应用需要读取大量的本地图片:
-
-
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中又可能会造成内存溢出
- 此时使用软应用来解决,设计思路时:用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题
-
-
- 虚引用:
-
-
- 虚引用必须和引用队列联合使用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。
- 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制。换句话说就是在对象被GC的时候会收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。
-
10.3.3 为什么要用弱引用?不用如何
-
为什么要用弱引用:
-
- 当方法执行完毕后,栈帧销毁,强引用t1也就没有了,但此时线程的ThreadLocalMap里某个entry的Key引用还指向这个对象,若这个Key是强引用,就会导致Key指向的ThreadLocal对象即V指向的对象不能被gc回收,造成内存泄露
- 若这个引用时弱引用就大概率会减少内存泄漏的问题(当然,还得考虑key为null这个坑),使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null
-
这里有个需要注意的问题:
-
- ThreadLocalMap使用ThreadLocal的弱引用作为Key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现Key为null的Entry,就没有办法访问这些Key为null的Entry的value,如果当前线程迟迟不结束的话(好比正在使用线程池),这些key为null的Entry的value就会一直存在一条强引用链
- 虽然弱引用,保证了Key指向的ThreadLocal对象能够被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
- 清除脏Entry----key为null的entry
-
-
- set()方法
-
-
-
-
-
- get()方法
-
-
-
-
-
- remove()
-
-
-
10.3.4 最佳实践
- ThreadLocal一定要初始化,避免空指针异常。
- 建议把ThreadLocal修饰为static
- 为了避免重复创建TSO(thread specific object,即与线程相关的变量。),我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。因此,如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。显然,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将ThreadLocal使用static修饰即可。
- 用完记得手动remove
10.4 小总结
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于它自己的专属map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有他的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用。避免了ThreadLocal对象无法被回收的问题
- 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及entry对象本身从而防止内存泄漏,属于安全加固的方法
- 群雄逐鹿起纷争,人各一份天下安
11、Java对象内存布局和对象头
11.1 面试题
- 说下JUC,AQS的大致流程
- CAS自旋锁,是获取不到锁就一直自旋吗?CAS和synchronized区别在哪里,为什么CAS好,具体优势在哪里?
- sychronized底层是如何实现的,实现同步的时候用到了CAS 了吗?具体哪里用到了?
- 对象头存储那些信息?长度是多少位存储?
11.2 Object object = new Object()谈谈你对这句话的理解?
- 位置所在-------->JVM堆->新生区->伊甸园区
- 构成布局-------->对象头+实例数据+对齐填充
11.3 对象在堆内存中的定义
11.3.1 权威定义----周志明老师JVM
在HotSpot虚拟机里,对象在堆内存的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data) 和对齐填充(Padding)。
11.3.2 对象在堆内存中的存储布局
-
对象头(在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节)
-
- 对象标记(Mark Word)
-
-
- 默认存储对象的HashCode、分代年龄和锁标志等信息。
- 这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
- 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
-
-
- 类元信息(类型指针)
-
-
- 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象哪个类的实例
-
-
实例数据
-
- 存放类的属性(Field)数据信息,包括父类的属性信息
-
对齐填充(保证8个字节的倍数)
-
- 虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐。
11.4 再说对象头的MarkWord
11.5 聊聊Object obj = new Object()
11.5.1 运行结果展示
11.5.2 压缩指针
-
Java -XX:+PrintCommandLineFlags -version 查看当前虚拟机信息
-
默认开启压缩指针,开启后将上述类型指针压缩为4字节,以节约空间
-
-
手动关闭压缩指针: -XX: -UseCompressedClassPointers
-
11.6 换成其他对象试试
12、Synchronized与锁升级
12.1 面试题
- 谈谈你对Synchronized的理解
- Sychronized的锁升级你聊聊
- Synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorenter和monitorexit这两个是怎么保证同步的嘛?或者说这两个操作计算机底层是如何执行的
- 偏向锁和轻量级锁有什么区别
12.2 Synchronized性能变化
-
Java5以前,只有Synchronized,这个是操作系统级别的重量级操作
-
- 重量级锁,假如锁的竞争比较激烈的话,性能下降
- Java 5之前 用户态和内核态之间的转换
-
-
-
Java6 之后为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
12.3 Synchronized锁种类及升级步骤
12.3.1 多线程访问情况
- 只有一个线程访问 only one
- 两个线程交替访问
- 竞争激烈,多个线程同时访问
12.3.2 升级流程
-
Synchronized用的锁是存在Java对象头里的MarkWord中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
-
锁指向,请牢记
-
- 偏向锁:MarkWord存储的是偏向的线程ID
- 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
- 重量锁:MarkWord存储的是指向堆中的monitor对象(系统互斥量指针)
12.3.3 无锁
12.3.4 偏向锁
偏向锁:单线程竞争,当线程A第一次竞争到锁时,通过修改MarkWord中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
主要作用:
- 当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁
- 同一个老顾客来访,直接老规矩行方便
结论:
- HotSpot的作者经过研究发现,大多数情况下:在多线程情况下,锁不仅不存在多线程竞争,还存在由同一个线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有一个线程执行同步时提高性能。
- 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他线程访问,则持有偏向锁的线程将永远不需要出发同步。也即偏向锁在资源在没有竞争情况下消除了同步语句,懒得连CAS操作都不做了,直接提高程序性能。
理论落地:
技术实现:
偏向锁JVM命令:
案例演示:
- 偏向锁默认情况演示—只有一个线程
/**
* @Author:xiaoqi
* @creat 2023/8/12 11:27
*/
public class SynchronizedUpDemo {
public static void main(String[] args) {
/**
* 这里偏向锁在JDK6以上默认开启,开启后程序启动几秒后才会被激活,可以通过JVM参数来关闭延迟 -XX:BiasedLockingStartupDelay=0
*/
// try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
偏向锁的撤销:
-
当有另外一个线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁,使用的是等到竞争出现才释放锁的机制
-
竞争线程尝试CAS更新对象头失败,会等到全局安全点(此时不会执行任何代码)撤销偏向锁,同时检查持有偏向锁的线程是否还在执行:
-
- 第一个线程正在执行Synchronized方法(处于同步块),它还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级,此时轻量级锁由原来持有偏向锁的线程持有,继续执行同步代码块,而正在竞争的线程会自动进入自旋等待获得该轻量级锁
- 第一个线程执行完Synchronized(退出同步块),则将对象头设置为无所状态并撤销偏向锁,重新偏向。
题外话:Java15以后逐步废弃偏向锁,需要手动开启------->维护成本高
12.3.5 轻锁
概念:多线程竞争,但是任意时候最多只有一个线程竞争,即不存在锁竞争太激烈的情况,也就没有线程阻塞。
主要作用:有线程来参与锁的竞争,但是获取锁的冲突时间极短---------->本质是自旋锁CAS
轻量锁的获取:
案例演示:
自旋一定程度和次数(Java8 之后是自适应自旋锁------意味着自旋的次数不是固定不变的):
- 线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也大概率会成功
- 如果很少会自选成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转
轻量锁和偏向锁的区别:
- 争夺轻量锁失败时,自旋尝试抢占锁
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
12.3.6 重锁
有大量线程参与锁的竞争,冲突性很高
12.3.7 小总结
- 锁升级的过程
-
锁升级后,hashcode去哪儿了?
-
-
各种锁优缺点、synchronized锁升级和实现原理
-
12.4 JIT编译器对锁的优化
12.4.1 JIT
Just In Time Compiler 即时编译器
12.4.2 锁消除
/**
/**
* @Author:xiaoqi
* @creat 2023/8/12 11:27
* 锁消除
* 从JIT角度看想相当于无视他,synchronized(o)不存在了
* 这个锁对象并没有被共用扩散到其他线程使用
* 极端的说就是根本没有加锁对象的底层机器码,消除了锁的使用
*/
public class LockClearUpDemo {
static Object object = new Object();
public void m1() {
//锁消除问题,JIT会无视它,synchronized(o)每次new出来的,都不存在了,非正常的
Object o = new Object();
synchronized (o) {
System.out.println("-----------hello LockClearUpDemo" + "\t" + o.hashCode() + "\t" + object.hashCode());
}
}
public static void main(String[] args) {
LockClearUpDemo lockClearUpDemo = new LockClearUpDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lockClearUpDemo.m1();
}, String.valueOf(i)).start();
}
}
}
/**
* -----------hello LockClearUpDemo 229465744 57319765
* -----------hello LockClearUpDemo 219013680 57319765
* -----------hello LockClearUpDemo 1109337020 57319765
* -----------hello LockClearUpDemo 94808467 57319765
* -----------hello LockClearUpDemo 973369600 57319765
* -----------hello LockClearUpDemo 64667370 57319765
* -----------hello LockClearUpDemo 1201983305 57319765
* -----------hello LockClearUpDemo 573110659 57319765
* -----------hello LockClearUpDemo 1863380256 57319765
* -----------hello LockClearUpDemo 1119787251 57319765
*/
12.4.3 锁粗化
/**
* @Author:xiaoqi
* @creat 2023/8/12 12:27
* 锁粗化
* 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器会把这几个synchronized块合并为一个大块
* 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提高了性能
*/
public class LockBigDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
System.out.println("111111111111");
}
synchronized (objectLock) {
System.out.println("222222222222");
}
synchronized (objectLock) {
System.out.println("333333333333");
}
synchronized (objectLock) {
System.out.println("444444444444");
}
//底层JIT的锁粗化优化
synchronized (objectLock) {
System.out.println("111111111111");
System.out.println("222222222222");
System.out.println("333333333333");
System.out.println("444444444444");
}
}, "t1").start();
}
}
12.5 总结
- 没有锁:自由自在
- 偏向锁:唯我独尊
- 轻量锁:楚汉争霸
- 重量锁:群雄逐鹿
13、AQS重磅登场
13.1 前置知识
-
公平锁和非公平锁
-
- 公平锁:锁被释放以后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
- 非公平锁:锁被释放以后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁
-
可重入锁
-
- 也叫做递归锁,指的是线程可以再次获取自己的内部锁,比如一个线程获取到了对象锁,此时这个对象锁还没有释放,当其想再次获取这个对象锁的时候还是可以获取的,如果不可重入的话,会导致死锁。
-
自旋思想
-
- 当线程请求锁时,如果锁已经被其他线程持有,那么该线程会不断地重试获取锁,而不是被挂起等待,这种不断尝试获取锁的行为称为自旋
-
LockSupport
-
- 一个工具类,用于线程的阻塞和唤醒操作,类似于wait()和notify()方法,但是更加灵活和可控
- 提供了park()和unpark()两个静态方法用于线程阻塞和唤醒操作。
- 优点在于可以在任意时刻阻塞和唤醒线程而不需要事先获取锁或监视器对象。
-
数据结构之双向链表
-
- 双向链表(Doubly Linked List)是一种常见的数据结构,它是由一系列结点(Node)组成的,每个结点包含三个部分:数据域、前驱指针和后继指针。其中,数据域存储结点的数据,前驱指针指向前一个结点,后继指针指向后一个结点。通过这种方式,双向链表可以实现双向遍历和插入、删除操作。
-
设计模式之模板设计模式
-
- 模板设计模式是一种行为型设计模式,定义了一种算法的框架,并将某些步骤延迟到子类中实现,这种设计模式的主要目的是允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。
- 优点是能够提高代码复用性和可维护性。
13.2 AQS入门级理论知识
13.2.1 AQS是什么?
抽象的队列同步器
技术解释
- 是用来实现锁或者其他同步器组件的公共基础部分的抽象实现
- 是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给”谁“的问题。
- 整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
13.2.2 AQS为什么是JUC内容中最重要的基石
-
和AQS有关的
-
- ReentrantLock
-
-
-
- CountDownLatch
-
-
-
- ReentrantReadWriteLock
-
-
-
- Semaphore
-
-
-
- …
-
进一步理解锁和同步器的关系
-
- 锁,面向锁的使用者:定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可
- 同步器,面向锁的实现者:Java并发大神DoungLee,提出了统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的----公共基础部分
13.2.3 能干嘛?
加锁会导致阻塞------有阻塞就需要排队,实现排队必然需要队列
-
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占失败的线程继续去等待(类似于银行办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等待),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)
-
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
-
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护着state变量的状态,使其达到同步的状态。
13.2.4 小总结
AQS同步队列的基本结构
13.3 AQS源码分析前置知识
13.3.1 AQS内部体系架构图
13.3.2 AQS内部结构
- AQS的int类型变量state
-
- AQS的同步状态State成员变量
-
-
- 银行办理业务的受理窗口状态
-
-
- 零就是没人,自由状态可以去办理
- 大于等于1,有人占用窗口,等着去
-
- AQS的CLH队列
-
- CLH(三个大牛的名字组成)队列为一个双向队列
-
-
-
- 银行候客区的等待顾客
- 小总结
-
- 有阻塞就需要排队,实现排队必然需要队列
- State变量+CLH双端队列
13.3.1 AQS内部体系架构----内部类Node
-
Node的int变量
-
- Node的等待状态waitState成员变量
-
-
-
- 说人话
-
-
- 等候区其他顾客(其他线程)的等待状态
- 队列中每个排队的个体就是一个Node
-
-
Node此类的讲解
-
- 内部结构
-
-
-
- 属性说明
-
-
13.4 AQS源码分析
13.4.1 ReentrantLock的原理
Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的
13.4.2 从最简单的lock方法开始看看公平和非公平
公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()-----公平锁加锁时判断等待队列中是否存在有效节点的方法
13.4.3 以非公平锁ReentrantLock()为例作为突破走起—方法lock()
对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差异就在于非公平锁获取锁时比公平锁中少了一个判断**!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队**,导致公平锁和非公平锁的差异如下:
- 公平锁:公平锁讲究先来后到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入到等待队列中;
- 非公平锁:不管是否有等待队列,如果可以获取到锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还需要参加竞争锁(存在线程竞争的情况下),后来的线程可能不讲武德插队夺锁了。
正式开始源码解读:
-
lock()
-
-
acquire()
-
-
tryAcquire(arg)
-
- return false:继续推进条件,走下一个方法
- return true:结束
-
addwaiter(Node.EXCLUSIVE)
-
- 注意:在双向链表中,第一个节点为虚节点(也叫做哨兵节点),其实不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的
- 假如此时有线程C进入:
-
acquireQueued(addWeiter(Node.EXCLUSIVE), arg)-----坐稳队列
-
13.4.4 unlock()
13.4.5 补充
详细源码解读可参考csdn这位大佬的博客阿昌教你看懂AQS核心流程_compareandsetstate_阿昌喜欢吃黄桃的博客-CSDN博客
14、ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
14.1 面试题
- 你知道Java里面有那些锁
- 你说说你用过的锁,锁饥饿问题是什么?
- 有没有比读写锁更快的锁
- StampedLock知道吗?(邮戳锁/票据锁)
- ReentrantReadWriteLock有锁降级机制,你知道吗?
14.2 ReentrantReadWriteLock
14.2.1 介绍
-
读写锁说明
-
- 一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程
-
再说说演变
-
- 无锁无序->加锁->读写锁->邮戳锁
-
读写锁意义和特点
-
- 它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是”读/读“线程间不存在互斥关系,只有”读/写“线程或者”写/写“线程间的操作是需要互斥的,因此引入了 ReentrantReadWriteLock
- 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但是不能同时存在写锁和读锁,也即资源可以被多个读操作访问,或一个写操作访问,但两者不能同时进行。
- 只有在读多写少情景之下,读写锁才具有较高的性能体现。
-
14.2.1 特点
-
可重入
-
读写兼顾
-
结论:一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得
-
锁降级:
-
- 将写锁降级为读锁------>遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁
- 如果一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
- 如果释放了写锁,那么就完全转换为读锁
- 如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
14.3 邮戳锁StampedLock
14.3.1 介绍
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
stamp 代表了锁的状态。当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
14.3.2 由来
它是由饥饿问题引出的锁
-
锁饥饿问题:
-
- ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因此当前有可能会一直存在读锁,而无法获得写锁。
-
如何解决锁饥饿问题:
-
- 使用”公平“策略可以一定程度上缓解这个问题
- 使用”公平“策略是以牺牲系统吞吐量为代价的
- StampedLock类的乐观读锁方式—>采取乐观获取锁,其他线程尝试获取写锁时不会被阻塞,在获取乐观读锁后,还需要对结果进行校验
14.3.3 特点
-
所有获取锁的方法,都返回一个邮戳,stamp为零表示失败,其余都表示成功
-
所有释放锁的方法,都需要一个邮戳,这个stamp必须是和成功获取锁时得到的stamp一致
-
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,在去获取写锁的话会造成死锁)
-
StampedLock有三种访问模式:
-
- Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似
- Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- Optimistic reading(乐观读模式):无锁机制,类似与数据库中的乐观锁,支持读写并发,很乐观认为读时没人修改,假如被修改再实现升级为悲观读模式
-
一句话:读的过程中也允许写锁介入
14.3.4 乐观读模式下代码演示
-
传统的读写锁模式----读的时候写锁不能获取
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; /** * @Author:xiaoqi * @creat 2023/8/12 16:15 * 传统的读写锁模式案例演示 */ public class StampedLockDemo { static int number = 37; static StampedLock stampedLock = new StampedLock(); public void write() { long stamp = stampedLock.writeLock(); System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改"); try { number = number + 13; } finally { stampedLock.unlockWrite(stamp); } System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改"); } public void read() { long stamp = stampedLock.readLock(); System.out.println(Thread.currentThread().getName() + "\t" + " come in readLock codeBlock"); for (int i = 0; i < 4; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取中"); } try { int result = number; System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量值result: " + result); System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥"); } finally { stampedLock.unlockRead(stamp); } } public static void main(String[] args) { StampedLockDemo resource = new StampedLockDemo(); new Thread(() -> { resource.read(); }, "readThread").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t"+" come in"); resource.write(); }, "writeThread").start(); } } /** * 运行结果 * readThread come in readLock codeBlock * readThread 正在读取中 * writeThread come in * readThread 正在读取中 * readThread 正在读取中 * readThread 正在读取中 * readThread 获得成员变量值result: 37 * 写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥 * writeThread 写线程准备修改 * writeThread 写线程结束修改 */
-
乐观读模式----读的过程中也允许写锁介入
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; /** * @Author:xiaoqi * @creat 2023/8/12 16:22 * 乐观读模式途中有写操作介入案例演示 */ public class StampedLockDemo2 { static int number = 37; static StampedLock stampedLock = new StampedLock(); public void write() { long stamp = stampedLock.writeLock(); System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改"); try { number = number + 13; } finally { stampedLock.unlockWrite(stamp); } System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改"); } public void read() { long stamp = stampedLock.tryOptimisticRead(); int result = number; System.out.println("4秒前 stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp)); for (int i = 0; i < 4; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取...." + i + "秒后" + "stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp)); } if (!stampedLock.validate(stamp)) { System.out.println("有人修改----------有写操作"); stamp = stampedLock.readLock(); try { System.out.println("从乐观读升级为悲观读"); result = number; System.out.println("重新悲观读后result:" + result); } finally { stampedLock.unlockRead(stamp); } } System.out.println(Thread.currentThread().getName() + "\t" + "finally value: " + result); } public static void main(String[] args) { StampedLockDemo2 resource = new StampedLockDemo2(); new Thread(() -> { resource.read(); }, "readThread").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t" + " come in"); resource.write(); }, "writeThread").start(); } } /** * 运行结果 * 4秒前 stampedLock.validate方法值(true 无修改 false有修改) true * readThread 正在读取....0秒后stampedLock.validate方法值(true 无修改 false有修改) true * writeThread come in * writeThread 写线程准备修改 * writeThread 写线程结束修改 * readThread 正在读取....1秒后stampedLock.validate方法值(true 无修改 false有修改) false * readThread 正在读取....2秒后stampedLock.validate方法值(true 无修改 false有修改) false * readThread 正在读取....3秒后stampedLock.validate方法值(true 无修改 false有修改) false * 有人修改----------有写操作 * 从乐观读升级为悲观读 * 重新悲观读后result:50 * readThread finally value: 50 * * Process finished with exit code 0 */
14.3.5 StampedLock的缺点
-
StampedLock不支持重入,没有Re开头
-
StampedLock的悲观读锁和写锁都不支持条件变量,这个也是主要
-
使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法
} finally {
stampedLock.unlockRead(stamp);
}} public static void main(String[] args) { StampedLockDemo resource = new StampedLockDemo(); new Thread(() -> { resource.read(); }, "readThread").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t"+" come in"); resource.write(); }, "writeThread").start(); }
}
/**- 运行结果
- readThread come in readLock codeBlock
- readThread 正在读取中
- writeThread come in
- readThread 正在读取中
- readThread 正在读取中
- readThread 正在读取中
- readThread 获得成员变量值result: 37
- 写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥
- writeThread 写线程准备修改
- writeThread 写线程结束修改
*/
-
乐观读模式----读的过程中也允许写锁介入
package juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; /** * @Author:xiaoqi * @creat 2023/8/12 16:22 * 乐观读模式途中有写操作介入案例演示 */ public class StampedLockDemo2 { static int number = 37; static StampedLock stampedLock = new StampedLock(); public void write() { long stamp = stampedLock.writeLock(); System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改"); try { number = number + 13; } finally { stampedLock.unlockWrite(stamp); } System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改"); } public void read() { long stamp = stampedLock.tryOptimisticRead(); int result = number; System.out.println("4秒前 stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp)); for (int i = 0; i < 4; i++) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取...." + i + "秒后" + "stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp)); } if (!stampedLock.validate(stamp)) { System.out.println("有人修改----------有写操作"); stamp = stampedLock.readLock(); try { System.out.println("从乐观读升级为悲观读"); result = number; System.out.println("重新悲观读后result:" + result); } finally { stampedLock.unlockRead(stamp); } } System.out.println(Thread.currentThread().getName() + "\t" + "finally value: " + result); } public static void main(String[] args) { StampedLockDemo2 resource = new StampedLockDemo2(); new Thread(() -> { resource.read(); }, "readThread").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t" + " come in"); resource.write(); }, "writeThread").start(); } } /** * 运行结果 * 4秒前 stampedLock.validate方法值(true 无修改 false有修改) true * readThread 正在读取....0秒后stampedLock.validate方法值(true 无修改 false有修改) true * writeThread come in * writeThread 写线程准备修改 * writeThread 写线程结束修改 * readThread 正在读取....1秒后stampedLock.validate方法值(true 无修改 false有修改) false * readThread 正在读取....2秒后stampedLock.validate方法值(true 无修改 false有修改) false * readThread 正在读取....3秒后stampedLock.validate方法值(true 无修改 false有修改) false * 有人修改----------有写操作 * 从乐观读升级为悲观读 * 重新悲观读后result:50 * readThread finally value: 50 * * Process finished with exit code 0 */
14.3.5 StampedLock的缺点
- StampedLock不支持重入,没有Re开头
- StampedLock的悲观读锁和写锁都不支持条件变量,这个也是主要
- 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法