1 线程基础知识复习
1把锁:synchronized(后面细讲)
2个并:
- 并发(concurrent):是在同一实体上的多个事件,是在一台机器 上"同时 "处理多个任务,同一时刻,其实是只有一个事情再发生。
- 并行(parallel):是在不同实体上的多个事件,是在多台处理器上同时处理多个任务,同一时刻,大家真的都在做事情,你做你的,我做我的,各干各的。
3个程:
- 进程:在系统中运行的一个应用程序,每个进程都有它自己的内存空间和系统资源
- 线程:也被称为轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元。
- 管程:Monitor(锁),也就是我们平时所说的锁。Monitor其实是一种同步机制,它的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码,JVM中同步是基于进入和退出监视器(Monitor管程对象)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象和Java对象一同创建并销毁,底层由C++语言实现。
线程分类(一般不做特别说明配置,默认都是用户线程):
- 用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作。
- 守护线程:是一种特殊的线程为其他线程服务的,在后台默默地完成一些系统性的任务,比如垃圾回收线程就是最典型的例子。守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以假如当系统只剩下守护线程的时候,守护线程伴随着JVM一同结束工作。
守护线程代码实操
java
public class DaemonDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 开始运行," + (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
while (true) {
}
}, "t1");
t1.setDaemon(true);//通过设置属性Daemon来设置当前线程是否为守护线程
t1.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 主线程结束");
}
}
输出:t1 开始运行,守护线程
main 主线程结束--->在main主线程结束后,守护线程会伴随着JVM一同结束工作,即使还有循环没有结束
2 CompletableFuture
2.1 Future接口理论知识复习
Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
举例:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙完其他事情或者先执行完,过了一会再去获取子任务的执行结果或变更的任务状态(老师上课时间想喝水,他继续讲课不结束上课这个主线程,让学生去小卖部帮老师买水完成这个耗时和费力的任务)。
一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务
2.2 Future接口常用实现类FutureTask异步任务
2.2.1 Future接口能干什么
Future是Java5新加的一个接口,它提供一种异步并行计算的功能。
如果主线程需要执行一个很耗时的计算任务,我们会就可以通过Future把这个任务放进异步线程中执行。主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
2.2.2 Future接口相关架构
- 目的:异步多线程任务执行且返回有结果,三个特点:多线程、有返回、异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
- 代码实现:Runnable接口+Callable接口+Future接口和FutureTask实现类。
FutureTask开启异步任务
java
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask(new MyThread());
Thread t1 = new Thread(futureTask); //开启一个异步线程
t1.start();
System.out.println(futureTask.get()); //有返回hello Callable
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("--------come in");
return "hello Callable";
}
}
2.2.3 Future编码实战和优缺点分析
优点:Future+线程池异步多线程任务配合,能显著提高程序的运行效率。
缺点:
- get()阻塞---一旦调用get()方法求结果,一旦调用不见不散,非要等到结果才会离开,不管你是否计算完成,如果没有计算完成容易程序堵塞。一般建议放在程序后面
- isDone()轮询---轮询的方式会耗费无谓的cpu资源,而且也不见得能及时得到计算结果。如果想要异步获取结果,通常会以轮询的方式去获取结果,尽量不要阻塞。
结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果。
Future获取结果get()和轮询
java
package com.bilibili.juc.cf;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class FutureAPIDemo
{
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException
{
FutureTask<String> futureTask = new FutureTask<String>( () -> {
System.out.println(Thread.currentThread().getName()+"\t -----come in");
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
return "task over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(Thread.currentThread().getName()+"\t ----忙其它任务了");
//System.out.println(futureTask.get());
//System.out.println(futureTask.get(3,TimeUnit.SECONDS));
while(true)
{
if(futureTask.isDone())
{
System.out.println(futureTask.get());
break;
}else{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("正在处理中,不要再催了,越催越慢 ,再催熄火");
}
}
}
}
/**
*1 get容易导致阻塞,一般建议放在程序后面,一旦调用不见不散,非要等到结果才会离开,不管你是否计算完成,容易程序堵塞。
*2 假如我不愿意等待很长时间,我希望过时不候,可以自动离开.
*/
2.2.4 想完成一些复杂的任务
对于简单的业务场景使用Future完全ok
回调通知:
- 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
- 通过轮询的方式去判断任务是否完成这样非常占cpu并且代码也不优雅
创建异步任务:Future+线程池组合
多个任务前后依赖可以组合处理(水煮鱼--->买鱼--->调料--->下锅):
- 想将多个异步任务的结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值
- 想将两个或多个异步计算合并成为一个异步计算,这几个异步计算互相独立,同时后面这个又依赖前一个处理的结果
对计算速度选最快的:
- 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果
结论:
使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture以声明式的方式优雅的处理这些需求。
从i到i++
Future能干的,CompletableFuture都能干
2.3 CompletableFuture对Future的改进
2.3.1 CompletableFuture为什么会出现
- get()方法在Future计算完成之前会一直处在阻塞状态下,阻塞的方式和异步编程的设计理念相违背。
- isDone()方法容易耗费cpu资源(cpu空转),
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果
jdk8设计出CompletableFuture,CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
2.3.2 CompletableFuture和CompletionStage介绍
类架构说明:
接口CompletionStage
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段。
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
类CompletableFuture
- 提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法
- 它可能代表一个明确完成的Future,也可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作
2.3.3 核心的四个静态方法,来创建一个异步任务
四个静态构造方法
对于上述Executor参数说明:若没有指定,则使用默认的ForkJoinPoolcommonPool()作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
四个静态方法演示
java
public class CompletableFutureBuildDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CompletableFuture<Void> 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()); //null
CompletableFuture<String> objectCompletableFuture = 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(objectCompletableFuture.get());//hello supplyAsync
executorService.shutdown();
}
}
Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
CompletableFuture使用演示
java
package com.bilibili.juc.cf;
import java.util.concurrent.*;
public class CompletableFutureUseDemo
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try
{
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(); }
System.out.println("-----1秒钟后出结果:" + result);
if(result > 2)
{
int i=10/0;
}
return result;
},threadPool).whenComplete((v,e) -> {
if (e == null) {
System.out.println("-----计算完成,更新系统UpdateValue:"+v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况:"+e.getCause()+"\t"+e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName()+"线程先去忙其它任务");
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
//主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
private static void future1() throws InterruptedException, ExecutionException
{
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();
}
System.out.println("-----1秒钟后出结果:" + result);
return result;
});
System.out.println(Thread.currentThread().getName()+"线程先去忙其它任务");
System.out.println(completableFuture.get());
}
}
CompletableFuture优点:
- 异步任务结束 时,会自动回调某个对象的方法
- 主线程设置好回调后,不用关心异步任务的执行,异步任务之间可以顺序执行
- 异步任务出错 时,会自动回调某个对象的方法
2.4 案例精讲-从电商网站的比价需求展开
2.4.1 函数式编程已成为主流
Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程
函数式接口:
Runnable:无参数、无返回值
Function(功能型函数式接口):接受一个参数,并且有返回值
Consumer(消费型函数式接口):接受一个参数,没有返回值
BiConsumer:接受两个参数,没有返回值
Supplier(供给型函数式接口):没有参数,有返回值
小结:
chain链式调用:
Chain链式调用演示
java
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;
}
2.4.2 大厂业务需求说明
切记:功能--->性能(完成--->完美)
电商网站比价需求分析:
需求说明:
同一款产品,同时搜索出同款产品在各大电商平台的售价
同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
输出返回:
出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List<String>
例如:
《Mysql》 in jd price is 88.05
《Mysql》 in taobao price is 90.43
解决方案,对比同一个产品在各个平台上的价格,要求获得一个清单列表
step by step,按部就班,查完淘宝查京东,查完京东查天猫....
all in,万箭齐发,一口气多线程异步任务同时查询
2.4.3 一波流Java8函数式编程带走-比价案例实战Case
比价实战Case
java
package com.bilibili.juc.cf;
import lombok.*;
import lombok.experimental.Accessors;
import java.awt.print.Book;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
*
* 案例说明:电商比价需求,模拟如下情况:
*
* 1需求:
* 1.1 同一款产品,同时搜索出同款产品在各大电商平台的售价;
* 1.2 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
*
* 2输出:出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List<String>
* 《mysql》 in jd price is 88.05
* 《mysql》 in dangdang price is 86.11
* 《mysql》 in taobao price is 90.43
*
* 3 技术要求
* 3.1 函数式编程
* 3.2 链式编程
* 3.3 Stream流式计算
*/
/**
* 这里面需要注意一下Stream流方法的使用
* 这种异步查询的方法大大节省了时间消耗,可以融入简历项目中,和面试官有所探讨
*/
public class CompletableFutureMallDemo
{
static List<NetMall> list = Arrays.asList(
new NetMall("jd"),
new NetMall("dangdang"),
new NetMall("taobao"),
new NetMall("pdd"),
new NetMall("tmall")
);
/**
* step by step 一家家搜查
* List<NetMall> ----->map------> List<String>
* @param list
* @param productName
* @return
*/
public static List<String> getPrice(List<NetMall> list,String productName)
{
//《mysql》 in taobao price is 90.43
return list
.stream()
.map(netMall ->
String.format(productName + " in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))
.collect(Collectors.toList());
}
/**
* List<NetMall> ----->List<CompletableFuture<String>>------> List<String>
* @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))))
.collect(Collectors.toList())
.stream()
.map(s -> s.join())
.collect(Collectors.toList());
}
public static void main(String[] args)
{
long startTime = System.currentTimeMillis();
List<String> list1 = getPrice(list, "mysql");
for (String element : list1) {
System.out.println(element);
}
long endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒");
System.out.println("--------------------");
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) +" 毫秒");
}
}
class NetMall
{
@Getter
private String netMallName;
public NetMall(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);
}
}
2.4.4 CompletableFuture常用方法
1 获得结果和触发计算
获取结果
public T get() 不见不散
public T get(long timeout,TimeUnit unit) 过时抛出异常
public T join() --->和get一样的作用,只是不需要抛出异常
public T getNow(T valuelfAbsent) --->立即获取结果不阻塞:计算完成就返回正常值,否则返回备胎值 valuelfAbsent,
主动触发计算
public boolean complete(T value) ---->是否打断get方法立即返回括号值
com.bilibili.juc.cf.CompletableFutureAPIDemo
java
package com.bilibili.juc.cf;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class CompletableFutureAPIDemo
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
}
/**
* 获得结果和触发计算
* @throws InterruptedException
* @throws ExecutionException
*/
private static void group1() throws InterruptedException, ExecutionException
{
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "abc";
});
//System.out.println(completableFuture.get());
//System.out.println(completableFuture.get(2L,TimeUnit.SECONDS));
//System.out.println(completableFuture.join());
//暂停几秒钟线程
//try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
//System.out.println(completableFuture.getNow("xxx"));
System.out.println(completableFuture.complete("completeValue")+"\t"+completableFuture.get());
}
}
2 对计算结果进行处理
thenApply --->计算结果存在依赖关系,这两个线程串行化---->由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停
handle --->计算结果存在依赖关系,这两个线程串行化---->有异常也可以往下走一步
对计算结果进行处理演示:
com.bilibili.juc.cf.CompletableFutureAPI2Demo
java
package com.bilibili.juc.cf;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CompletableFutureAPI2Demo
{
public static void main(String[] args)
{
//自定义线程池,避免主线程立刻结束导致默认使用的线程池关闭
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture.supplyAsync(() ->{
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("111");
return 1;
},threadPool).handle((f,e) -> {
int i=10/0;
System.out.println("222");
return f + 2;
}).handle((f,e) -> {
System.out.println("333");
return f + 3;
}).whenComplete((v,e) -> {
if (e == null) {
System.out.println("----计算结果: "+v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName()+"----主线程先去忙其它任务");
threadPool.shutdown();
}
}
3 对计算结果进行消费
接受任务的处理结果,并消费处理,无返回结果
thenAccept
thenAccetp演示:
java
package com.bilibili.juc.cf;
import java.util.concurrent.CompletableFuture;
/**
* @auther zzyy
* @create 2022-01-17 17:18
*/
public class CompletableFutureAPI3Demo
{
public static void main(String[] args)
{
CompletableFuture.supplyAsync(() -> {
return 1;
}).thenApply(f ->{
return f + 2;
}).thenApply(f ->{
return f + 3;
}).thenAccept(System.out::println);
}
对比补充
thenRun(Runnable runnable) :任务A执行完执行B,并且不需要A的结果,且无返回值
thenAccept(Consumer action): 任务A执行完执行B,B需要A的结果,但是任务B没有返回值
thenApply(Function fn): 任务A执行完执行B,B需要A的结果,同时任务B有返回值
java
package com.bilibili.juc.cf;
import java.util.concurrent.CompletableFuture;
public class CompletableFutureAPI3Demo
{
public static void main(String[] args)
{
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(r -> System.out.println(r)).join());
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(r -> r + "resultB").join());
}
}
CompletableFuture和线程池说明
1 如果没有传入自定义线程池,都用默认线程池ForkJoinPool
2 传入一个自定义线程池,
如果你执行第一个任务时,传入了一个自定义线程池:
调用thenRun方法执行第二个任务时,则第二个任务和第一个任务时共用同一个线程池
调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自定义的线程池,第二个任务使用的是ForkJoin线程池
3 备注:可能是线程处理太快,系统优化切换原则, 直接使用main线程处理,thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,之间的区别同理。
4 对计算速度进行选用
谁快用谁
applyToEither
applyToEither演示:
java
package com.bilibili.juc.cf;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureFastDemo
{
public static void main(String[] args)
{
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
System.out.println("A come in");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
return "playA";
});
CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
System.out.println("B come in");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return "playB";
});
CompletableFuture<String> result = playA.applyToEither(playB, f -> {
return f + " is winer";
});
System.out.println(Thread.currentThread().getName()+"\t"+"-----: "+result.join());
}
}
5 对计算结果进行合并
两个CompletableStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理
先完成的先等着,等待其他分支任务
thenCombine演示:
java
/**
* 可以合并写在一起,不必拆分
*/
public class CompletableFutureApi3Demo {
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> finalResult = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println("----------开始两个结果合并");
return x + y;
});
System.out.println(finalResult.join());
}
}
3 说说Java"锁"事
3.1 从轻松的乐观锁和悲观锁开讲
悲观锁:
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源
一句话:狼性锁
乐观锁:
认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁。
Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。
判断规则:
1 版本号机制Version,
2 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再努力就是
一句话:佛系锁
乐观锁一般有两种实现方式:
1 采用Version版本号机制
CAS(Compare-and-Swap,即比较并替换)算法实现
3.2 通过8种情况演示锁运行案例,看看锁到底是什么
3.2.1 锁相关的8种案例演示code
java
package com.bilibili.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public static 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");
}
}
/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程 操作 资源类
* 8锁案例说明:
* 1 标准访问有ab两个线程,请问先打印邮件还是短信
* 2 sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
* 3 添加一个普通的hello方法,请问先打印邮件还是hello
* 4 有两部手机,请问先打印邮件还是短信
* 5 有两个静态同步方法,有1部手机,请问先打印邮件还是短信
* 6 有两个静态同步方法,有2部手机,请问先打印邮件还是短信
* 7 有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
* 8 有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
*
* 笔记总结:
* 1-2
* * * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁------>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* * 7-8
* * 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* * *
* * * 所有的普通同步方法用的都是同一把锁------实例对象本身,就是new出来的具体实例对象本身,本类this
* * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* * *
* * * 所有的静态同步方法用的也是同一把锁------类对象本身,就是我们说过的唯一模板Class
* * * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
*/
public class Lock8Demo
{
public static void main(String[] args)//一切程序的入口
{
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
},"a").start();
//暂停毫秒,保证a线程先启动
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
},"b").start();
}
}
/**
*
* ============================================
* 1-2
* * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
*
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁------>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* 7-8
* 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* *
* * 所有的普通同步方法用的都是同一把锁------实例对象本身,就是new出来的具体实例对象本身,本类this
* * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* *
* * 所有的静态同步方法用的也是同一把锁------类对象本身,就是我们说过的唯一模板Class
* * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
**/
结论:
对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁--->实例对象本身
对于静态同步方法,锁的是当前类的Class对象
对于同步方法块,锁的是synchronized括号内的对象
3.2.2 synchronized有三种应用方式
作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁;
作用于代码块,对括号里配置的对象加锁
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
3.2.3 从字节码角度分析synchronized实现
javap -c(v附加信息) ***.class 文件反编译
synchronized同步代码块
实现使用的是monitorenter和monitorexit指令
synchronized普通同步方法
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor
synchronized静态同步方法
ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
3.2.4 反编译synchronized锁的是什么
面试题:为什么任何一个对象都可以成为一个锁?
什么是管程monitor
C++源码解读:ObjectMonitor.java--->ObjectMonitor.cpp--->ObjectMonitor.hpp(头文件)
每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来
总结:指针指向Monitor对象(也称为管程或监视器)的真实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由OnjectMonitor实现的,其主要的数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现):
3.2.5 对于Synchronized关键字
后面章节详说
3.3 公平锁和非公平锁
3.3.1 何为公平锁/非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人在队尾排着,这是公平的
Lock lock = new ReentrantLock(true); //true 表示公平锁,先来先得
非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
Lock lock = new ReentrantLock(false); //false 表示非公平锁,后来的也可能先获得锁
Lock lock = new ReentrantLock(); //默认为非公平锁。
面试题:
为什么会有公平锁/非公平锁的设计?为什么默认非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。
3.3.2 预埋伏AQS
后续深入分析
3.4 可重入锁(递归锁)
3.4.1 概念说明
是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。
如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
"可重入锁"这四个字分开来解释:
3.4.2 可重入锁种类
隐式锁(即synchronized关键字使用的锁),默认是可重入锁
指的是可重复递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,遮掩的锁就叫做可重入锁。
简单来说:在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
显式锁(即Lock)也有ReentrantLock这样的可重入锁
java
package com.bilibili.juc.locks;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReEntryLockDemo
{
public synchronized void m1()
{
//指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
System.out.println(Thread.currentThread().getName()+"\t ----come in");
m2();
System.out.println(Thread.currentThread().getName()+"\t ----end m1");
}
public synchronized void m2()
{
System.out.println(Thread.currentThread().getName()+"\t ----come in");
m3();
}
public synchronized void m3()
{
System.out.println(Thread.currentThread().getName()+"\t ----come in");
}
static Lock lock = new ReentrantLock();
public static void main(String[] args)
{
/*ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
new Thread(() -> {
reEntryLockDemo.m1();
},"t1").start();*/
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");
}finally {
lock.unlock();
}
}finally {
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
//lock.unlock();// 正常情况,加锁几次就要解锁几次
}
},"t1").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
}finally {
lock.unlock();
}
},"t2").start();
}
private static void reEntryM1()
{
final Object object = new Object();
new Thread(() -> {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t ----外层调用");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t ----中层调用");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t ----内层调用");
}
}
}
},"t1").start();
}
}
Synchronized的重入的实现原理
3.5 死锁及排查
3.5.1 概念
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。
产生原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 系统资源分配不当
3.5.2 写一个死锁代码case
java
package com.bilibili.juc.locks;
import java.util.concurrent.TimeUnit;
public class DeadLockDemo
{
public static void main(String[] args)
{
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(() -> {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 自己持有A锁,希望获得B锁");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 成功获得B锁");
}
}
},"A").start();
new Thread(() -> {
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 自己持有B锁,希望获得A锁");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 成功获得A锁");
}
}
},"B").start();
}
}
3.5.3 如何排查死锁
纯命令
- jps -l (相当于java版的 ps -ef)
- jstack 进程编号
图形化
- jconsole
3.6 写锁(独占锁)/读锁(共享锁)
深度源码分析见后面
3.7 自旋锁spinLock
深度源码分析见后面
3.8 无锁->独占锁->读写锁->邮戳锁
深度源码分析见后面
3.9 无锁->偏向锁->轻量锁->重量锁
深度源码分析见后面
3.10 本章总结
指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
4 LockSupport与线程中断
4.1 线程中断机制
4.1.1 从阿里蚂蚁金服面试题讲起
Java.lang.Thread下的三个方法:
如何中断一个运行中的线程?
如何停止一个运行中的线程?
4.1.2 什么是中断机制
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运,所以,Thread.stop,Thread.suspend,Thread.resume都已经被废弃了
其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制----中断,也即中断标识协商机制
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自行实现。若要中断一个线程,你需要手动调用该线程interrupt方法,该方法也仅仅是将该线程对象的中断标识设置为true,接着你需要自己写代码不断检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟应该做什么需要你自己写代码实现。
每个线程对象都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设置为true;可以在别的线程中调用,也可以在自己的线程中调用。
4.1.3 中断的相关API方法之三大方法说明
public void interrupt()
实例方法, Just to set the interrupt flag
实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
public static boolean interrupted()
静态方法 Thread.interrupted();
判断线程是否被中断并清除当前中断状态(做了两件事情):
1 返回当前线程的中断状态,测试当前线程是否已被中断
2 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
这个方法有点不好理解在于如果连续两次调用此方法,则第二次返回false,因为连续调用两次的结果可能不一样
public boolean isInterrupted()
实例方法
判断当前线程是否被中断(通过检查中断标志位)
4.1.4 大厂面试题中断机制考点
如何停止中断运行中的线程?
1 通过一个volatile变量实现
volatile中断线程演示
java
/**
* 使用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();
}
}
/**
* -----------hello volatile
* -----------hello volatile
* -----------hello volatile
* -----------hello volatile
* -----------hello volatile
* -----------hello volatile
* t1 isStop的值被改为true,t1程序停止
*/
2 通过AutomicBoolean
AutomicBoolean中断线程演示
java
/**
* 使用AtomicBoolean
*/
public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (atomicBoolean.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(() -> {
atomicBoolean.set(true);
}, "t2").start();
}
}
/**
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* t1 atomicBoolean的值被改为true,t1程序停止
*/
3 通过Thread类自带的中断API实例方法实现
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。
interrupt() 和isInterrupted()组合使用来中断某个线程演示
java
/**
* 使用interrupt() 和isInterrupted()组合使用来中断某个线程
*/
public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
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();
}
}
/**
* -----------hello isInterrupted()
* -----------hello isInterrupted()
* -----------hello isInterrupted()
* -----------hello isInterrupted()
* t1 isInterrupted()的值被改为true,t1程序停止
*/
当前线程的中断标识为true,是不是线程就立刻停止?
答案是不立刻停止,具体来说,当对一个线程,调用interrupt时:
如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响,所以interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行,对于不活动的线程没有任何影响。
如果线程处于阻塞状态(例如sleep,wait,join状态等),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(interrupt状态也将被清除),并抛出一个InterruptedException异常。
第一种情况正常活动状态演示
java
package com.bilibili.juc.interrupt;
import java.util.concurrent.TimeUnit;
/**
* 执行interrupt方法将t1标志位设置为true后,t1没有中断,仍然完成了任务后再结束
* 在2000毫秒后,t1已经结束称为不活动线程,设置状态为没有任何影响
*/
public class InterruptDemo2 {
public static void main(String[] args) {
//实例方法interrupt()仅仅是设置线程的中断状态位为true,不会停止线程
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 300; i++) {
System.out.println("------: " + i);
}
/**
* ------: 298
* ------: 299
* ------: 300
* t1线程调用interrupt()后的中断标志位02:true
*/
System.out.println("t1线程调用interrupt()后的中断标志位02:" + Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
System.out.println("t1线程默认的中断标志位:" + t1.isInterrupted());//false
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();//true
/**
* ------: 251
* ------: 252
* ------: 253
* t1线程调用interrupt()后的中断标志位01:true
*/
System.out.println("t1线程调用interrupt()后的中断标志位01:" + t1.isInterrupted());//true
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2000毫秒后,t1线程已经不活动了,不会产生任何影响
System.out.println("t1线程调用interrupt()后的中断标志位03:" + t1.isInterrupted());//false
}
}
第二种情况线程处于阻塞状态演示
java
package com.bilibili.juc.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo3
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
while (true)
{
if(Thread.currentThread().isInterrupted())
{
System.out.println(Thread.currentThread().getName()+"\t " +
"中断标志位:"+Thread.currentThread().isInterrupted()+" 程序停止");
break;
}
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();
}
}
/**
* 1 中断标志位,默认false
* 2 t2 ----> t1发出了中断协商,t2调用t1.interrupt(),中断标志位true
* 3 中断标志位true,正常情况,程序停止,^_^
* 4 中断标志位true,异常情况,InterruptedException,将会把中断状态将被清除,并且将收到InterruptedException 。中断标志位false
* 导致无限循环
*
* 5 在catch块中,需要再次给中断标志位设置为true,2次调用停止程序才OK
*/
对于第二种情况的源码分析如下:
总之,需要记住的是中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断
静态方法Thread.interrupted(),谈谈你的理解?
静态方法Thread.interrupted()演示
java
package com.bilibili.juc.interrupt;
import java.util.concurrent.locks.LockSupport;
public class InterruptDemo4
{
public static void main(String[] args)
{
//测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
// 第二次再调用时中断状态已经被清除,将返回一个false。
/**
* main false
* main false
* -----------1
* -----------2
* main true
* main false
*/
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println("----1");
Thread.currentThread().interrupt();// 中断标志位设置为true
System.out.println("----2");
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
LockSupport.park();
Thread.interrupted();//静态方法
Thread.currentThread().isInterrupted();//实例方法
}
}
对于静态方法Thread.interrupted()和实例方法isInterrupted()区别在于:
- 静态方法interrupted将会清除中断状态(传入的参数ClearInterrupted为true)
- 实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)
4.1.5 总结
public void interrupt() 是一个实例方法
它通知目标线程中断,也仅仅是设置目标线程的中断标志位为true
public boolean isInterrupted() 是一个实例方法
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
public static boolean interrupted() 是一个静态方法
返回当前线程的中断真实状态(boolean类型)后会将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置为false。
4.2 LockSupport是什么
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,其中park()和unpack()的作用分别是阻塞线程和解除阻塞线程.
4.3 线程等待唤醒机制
4.3.1 三种让线程等待和唤醒的方法
方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
4.3.2 Object类中的wait和notify方法实现线程等待和唤醒
wait和notify方法必须要在同步代码块或者方法里面,且成对出现使用
先wait再notify才ok
Object类中的wait和notify方法实现线程等待和唤醒演示
java
public class LockSupportDemo {
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();
}
}
4.3.3 Condition接口中的await和signal方法实现线程的等待和唤醒
Condition中的线程等待和唤醒方法,需要先获取锁
一定要先await后signal,不要反了
Condition接口中的await和signal方法实现线程的等待和唤醒演示
java
public class LockSupportDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
/**
* t1 -----------come in
* t2 -----------发出通知
* t1 -----------被唤醒
*/
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();
}
}
4.3.4 上述两个对象Object和Condition使用的限制条件
线程需要先获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒
4.3.5 LockSupport类中的park等待和unpark唤醒
是什么
- LockSupport 是用于创建锁和其他同步类的基本线程阻塞原语
- LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只能有一个,累加上限是1。
主要方法
- 阻塞: Peimit许可证默认没有不能放行,所以一开始调用park()方法当前线程会阻塞,直到别的线程给当前线程发放peimit,park方法才会被唤醒。
- park/park(Object blocker)-------阻塞当前线程/阻塞传入的具体线程
- 唤醒: 调用unpack(thread)方法后 就会将thread线程的许可证peimit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。
- unpark(Thread thread)------唤醒处于阻塞状态的指定线程
代码
LockSupport类中的park等待和unpark唤醒演示
java
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");
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);
System.out.println(Thread.currentThread().getName() + "\t ----------发出通知");
}, "t2").start();
}
}
重点说明(重要)
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
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却需要消费两个凭证,证不够,不能放行。
5 Java内存模型之JMM
5.1 先从大厂面试开始
你知道什么是Java内存模型JMM吗?
JMM和volatile他们两个之间的关系?
JMM有哪些特征或者它的三大特征是什么?
为什么要有JMM,它为什么出现?作用和功能是什么?
happens-before先行并发原则你有了解过吗?
5.2 计算机硬件存储体系
计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算
因为有这么多级的缓存(cpu和物理主内存的速度不一致的)
CPU的运行并不是直接操作内存而是先把内存里面的数据读到缓存,而内存的读和写操作的时候会造成不一致的问题。
JVM规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序再各种平台下都能达到一致性的内存访问效果。
5.3 Java内存模型Java Memory Model
JMM(Java内存模型Java Memory Model)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
能干嘛?
1 通过JMM来实现线程和主内存之间的抽象关系
2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致性的内存访问效果。
5.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的执行特性,最大限度的发挥机器性能。
但是指令重排可以保证串行语义一致,但没有义务保证多线程的语义也一致(即可能产生"脏读"),简单而言,就是两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
从源码到最终执行示例图:
单线程环境里确实能够保证程序最终执行结果和代码顺序执行的结果一致
处理器在进行重排序时必须考虑到指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,可能出现乱序现象,两个线程使用的变量能否保证一致性是无法确定的,结果无法预测。
5.5 JMM规范下多线程对变量的读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读写赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
JMM定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存中(从硬件角度讲就是内存条)
- 每个线程都有一个自己的本地工作内存,本地工作内存中存储了该线程用来读写共享变量的副本(从硬件角度来说就是CPU的缓存)
小总结:
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保证该线程使用到的共享变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能互相访问)。
5.6 JMM规范下多线程先行发生原则之happens-before
在JVM中,如果一个操作执行的结果需要对另一个操作可见或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则,逻辑上的先后关系。
5.6.1 x,y案例说明
|-----------|-------|
| x=5 | 线程A执行 |
| y=x | 线程B执行 |
| 上述称之为:写后读 | |
问题?
|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| y是否等于5呢? 如果线程A的操作(x=5)happens-before(先行发生)线程B的操作(y=x),那么可以确定线程B执行y=5一定成立; 如果他们不存在happens-before原则,那么y=5不一定成立 这就是happens-before原则的为例----------->包含可见性和有序性的约束 |
5.6.2 先行并发原则说明
如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点。
我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下,有一个"先行发生"(happens-before)的原则限制和规矩,给你理好了规矩!
这个原则非常重要:它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型晦涩难懂的底层编译原理之中。
5.6.3 happens-before总原则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
如果两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
5.6.4 happens-before之8条
从JDK 5开始,Java使用新的JSR-133内存模型,提供了 happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:
1 次序规则:一个线程内,按照代码的顺序,写在前面的操作先行发生于写在后面的操作。也就是说前一个操作的结果可以被后续的操作获取(保证语义串行性,按照代码顺序执行)。比如前一个操作把变量x赋值为1,那后面一个操作肯定能知道x已经变成了1
2 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作(后面指时间上的先后)。
3 volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的后面同样指时间上的先后
4 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
5 线程启动规则(Thread start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
6 线程中断规则(Thread Interruption Rule):
a)对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
b)可以通过Thread.interrupted()检测到是否发生中断
c)也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发生
7 线程终止规则(Thread Termination Rule):线程中的所有操作都优先发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
8 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始------->对象没有完成初始化之前,是不能调用finalized()方法的
5.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规范结合操作系统给我们搞定,我们只写好代码即可。
5.6.6 案例说明
初始案例演示:
java
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原则推导出来,那么就不能保证他们的有序性,虚拟机可以随意对他们进行重排序 |
如何修复?
修复方法1:把getter/setter方法都定义为synchronized方法------->不好,重量锁,并发性下降
把getter/setter方法都定义为synchronized方法
java
private int value =0;
public synchronized int getValue(){
return value;
}
public synchronized int setValue(){
return ++value;
}
修复方法2:把Value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
java
/**
* 利用volatile保证读取操作的可见性,
* 利用synchronized保证复合操作的原子性结合使用锁和volatile变量来减少同步的开销
*/
private volatile int value =0;
public int getValue(){
return value; //利用volatile保证读取操作的可见性
}
public synchronized int setValue(){
return ++value; //利用synchronized保证复合操作的原子性
}
6 volatile与JMM
6.1 被volatile修饰的变量有两大特点
特点:
- 可见性
- 有序性:有排序要求,有时需要禁重排
内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量的值
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
volatile凭什么可以保证可见性和有序性?
- 内存屏障Memory Barrier
6.2 内存屏障(面试重点必须拿下)
6.2.1 生活case
没有管控,顺序难保
设定规则,禁止乱序---->上海南京路武警当红灯
再说vilatile两大特性:
可见 :写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面所有线程可见
有序性(禁重排):
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序。
若不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序;
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
6.2.2 是什么
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性
内存屏障之前的所有写操作都要回写到主内存
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store buffers)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前的所有写入指令执行完毕才能继续往下执行。 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。 |
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。
6.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及其后的读操作才能执行 |
6.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屏障;
6.3 volatile特性
6.3.1 保证可见性
保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见
Code
- 不加volatile,没有可见性,程序无法停止
- 加了volatile,保证可见性,程序可以停止
加了volatile保证可见性:
java
package com.bilibili.juc.volatiles;
import java.util.concurrent.TimeUnit;
public class VolatileSeeDemo
{
//static boolean flag = true;
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 = false;
System.out.println(Thread.currentThread().getName()+"\t 修改完成flag: "+flag);
}
}
/**
* t1 -------come in
* main 修改完成
* t1 -------flag被设置为false,程序停止
*/
线程t1中为何看不到被主线程main修改为false的flag的值?
问题可能:
1.主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
2.主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。
我们的诉求:
-
线程中修改了自己工作内存的副本之后,立即将其刷新到主内存;
-
工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
解决:
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
-
线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
-
线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
volatile变量的读写过程(了解即可)
6.3.2 没有原子性
volatile变量的复合操作不具有原子性,比如number++
java
package com.bilibili.juc.volatiles;
import java.util.concurrent.TimeUnit;
class MyNumber
{
volatile int number;
public void addPlusPlus()
{
number++;
}
}
public class VolatileNoAtomicDemo
{
public static void main(String[] args)
{
MyNumber myNumber = new MyNumber();
for (int i = 1; i <=10; i++) {
new Thread(() -> {
for (int j = 1; j <=1000; j++) {
myNumber.addPlusPlus();
}
},String.valueOf(i)).start();
}
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(myNumber.number);
}
}
- 对于voaltile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅仅是数据加载时是最新的。但是多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须加锁同步。
- 至于怎么去理解这个写丢失的问题,就是再将数据读取到本地内存到写回主内存中有三个步骤:数据加载---->数据计算---->数据赋值,如果第二个线程在第一个线程读取旧值与写回新值期间读取共享变量的值,那么第二个线程将会与第一个线程一起看到同一个值,并执行自己的操作,一旦其中一个线程对volatile修饰的变量先行完成操作刷回主内存后,另一个线程会作废自己的操作,然后重新去读取最新的值再进行操作,这样的话,它自身的那一次操作就丢失了,这就造成了 线程安全失败,因此,这个问题需要使用synchronized修饰以保证线程安全性。
结论:volatile变量不适合参与到依赖当前值的运算,如i++,i=i+1之类的
那么依靠可见性的特点volatile可以用在哪些地方呢?通常用来保存某个状态的boolean值或者int值。
也正是由于volatile变量只能保证可见性,在不符合以下规则的运算场景中,我们仍然要通过加锁来保证原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
面试回答为什么不具备原子性:举例i++的例子,在字节码文件中,i++分为三部,间隙期间不同步非原子操作
- 对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的;如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,也就造成了线程安全问题。
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
6.3.3 指令禁重排
内存屏障
|-----------------------------------------------------------------------------|
| 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序,这个操作保证了volatile读之后的操作不会被重排到volatile读之前。 |
| 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序,这个操作保证了volatile写之前的操作不会被重排到volatile写之后 |
| 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排 |
在每一个volatile写操作前面插入一个StoreStore屏障--->StoreStore屏障可以保证在volatile写之前,其前面所有的普通写操作都已经刷新到主内存中。
在每一个volatile写操作后面插入一个StoreLoad屏障--->StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
在每一个volatile读操作后面插入一个LoadLoad屏障--->LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
在每一个volatile读操作后面插入一个LoadStore屏障--->LoadTore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序
案例说明(volatile读写前或后加了屏障保证有序性):
6.4 如何正确使用volatile
单一赋值可以,但是含复合运算赋值不可以(i++之类的)
volatile int a = 10;
volatile boolean flag = true;
状态标志,判断业务是否结束
作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
开销较低的读,写锁策略
当读远多于写,结合使用内部锁和volatile变量来减少同步的开销
原理是:利用volatile保证读操作的可见性,利用synchronized保证符合操作的原子性
DCL双端锁的发布
问题描述:首先设定一个加锁的单例模式场景
在单线程环境下(或者说正常情况下),在"问题代码处",会执行以下操作,保证能获取到已完成初始化的实例:
隐患:在多线程环境下,在"问题代码处",会执行以下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象
其中第3步中实例化分多步执行(分配内存空间、初始化对象、将对象指向分配的内存空间),某些编译器为了性能原因,会将第二步和第三步重排序,这样某个线程肯能会获得一个未完全初始化的实例:
这种场景在著名的双重检查锁定(double-checked-locking)中会出现:
其中第3步中实例化Singleton分多步执行(分配内存空间、初始化对象、将对象指向分配的内存空间),某些编译器为了性能原因,会将第二步和第三步进行重排序(分配内存空间、将对象指向分配的内存空间、初始化对象)。这样某个线程可能会获得一个未完全初始化的实例。
多线程下的解决方案:加volatile修饰
6.5 本章最后的小总结
6.5.1 volatile可见性
|-------------------|-------------------------------------------------------------------|
| volatile关键字保证可见性: | 对一个被volatile关键字修饰的变量 |
| 1 | 写操作的话,这个变量的最新值会立即刷新回到主内存中 |
| 2 | 读操作的话,总是能够读取到这个变量的最新值,也就是这个变量最后被修改的值 |
| 3 | 当某个线程收到通知,去读取volatile修饰的变量的值的时候,线程私有工作内存的数据失效,需要重新回到主内存中去读取最新的数据。 |
6.5.2 volatile没有原子性
6.5.3 volatile禁重排
凭什么我们java写了一个volatile关键字,系统底层就加入了内存屏障?两者关系怎么建立的?
字节码层面:
6.5.5 内存屏障是什么?
是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也称为内存栅栏或栅栏指令。
6.5.6 内存屏障能干吗?
阻止屏障两边的指令重排序
写操作时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
读操作时加入屏障,线程私有工作内存的数据失效,重新回到主物理内存中获取最新值
6.5.7 内存屏障四大指令
6.5.8 3句话总结
volatile写之前的操作,都禁止重排序到volatile之后
volatile读之后的操作,都禁止重排序到volatile之前
volatile写之后volatile读,禁止重排序
7 CAS
7.1 原子类
Java.util.concurrent.atomic
7.2 没有CAS之前
多线程环境中不使用原子类保证线程安全i++(基本数据类型)
java
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
7.3 使用CAS之后
多线程环境中使用原子类保证线程安全i++(基本数据类型)---------->类似于乐观锁
java
class Test2 {
AtomicInteger atomicInteger = new AtomicInteger();
public int getAtomicInteger() {
return atomicInteger.get();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public void setAtomicInteger() {
atomicInteger.getAndIncrement();
}
}
7.4 是什么?
CAS(compare and swap),中文翻译为比较并交换,实现并发算法时常用到的一种技术,用于保证共享变量的原子性更新,它包含三个操作数---内存位置、预期原值与更新值。
执行CAS操作的时候,将内存位置的值与预期原值进行比较:
如果相匹配,那么处理器会自动将该位置更新为新值
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
CASDemo演示
java
package com.bilibili.juc.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo
{
public static void main(String[] args)
{
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2022)+"\t"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2022)+"\t"+atomicInteger.get());
atomicInteger.getAndIncrement();
}
}
硬件级别保证
compareAndSet()源码
7.5 CAS底层原理?谈谈对Unsafe类的理解?
7.5.1 Unsafe
1 Unsafe
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应任务。
2 变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
3 变量value用volatile修饰,保证了多线程之间的内存可见性
问题:我们知道i++是线程不安全的,那atomicInteger.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的原子指令,不会造成所谓的数据不一致问题。
7.5.2 源码分析
new AtomicInteger().getAndIncrement();
7.5.3 底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中
JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用compxchg指令比较并更新变量值(原子性)
(Atomic::cmpxchg(x,addr,e))== e;
总结:
CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
实现方式是基于硬件平台的汇编指令,在inter的CPU中(X86机器上),使用的是汇编指令compxchg指令
核心思想就是比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap),如果不相等自旋再来
7.6 原子引用
原子引用演示
java
package com.bilibili.juc.cas;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.util.concurrent.atomic.AtomicReference;
@Getter
@ToString
@AllArgsConstructor
class User
{
String userName;
int age;
}
public class AtomicReferenceDemo
{
public static void main(String[] args)
{
AtomicReference<User> atomicReference = new AtomicReference<>();
User z3 = new User("z3",22);
User li4 = new User("li4",28);
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
}
}
7.7 CAS与自旋锁,借鉴CAS思想
7.7.1 是什么?
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋锁---字面意思自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
7.7.2 自己实现一个自旋锁spinLockDemo
题目:实现一个自旋锁,借鉴CAS思想
通过CAS完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
借鉴CAS思想实现自旋锁
java
package com.bilibili.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 题目:实现一个自旋锁,复习CAS思想
* 自旋锁好处:循环比较获取没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
* 当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
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();
//暂停500毫秒,线程A先于B启动
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
},"B").start();
}
}
7.8 CAS缺点
7.8.1 循环时间长开销很大
getAndAddInt方法有一个do while
如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大开销
7.8.2 引出来ABA问题?
ABA问题怎么产生的?
CAS算法实现一个重要前提需要提取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期ok,然后线程1操作成功
尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
版本号时间戳原子引用
AtomicStampedReference带戳记流水的简单演示(单线程)
java
package com.bilibili.juc.cas;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicStampedReference;
@NoArgsConstructor
@AllArgsConstructor
@Data
class Book
{
private int id;
private String bookName;
}
public class AtomicStampedDemo
{
public static void main(String[] args)
{
Book javaBook = new Book(1,"javaBook");
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);
System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
Book mysqlBook = new Book(2,"mysqlBook");
boolean b;
b = stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
b = stampedReference.compareAndSet(mysqlBook, javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
}
}
多线程情况下演示AtomicStampedReference解决ABA问题
java
package com.bilibili.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo
{
static AtomicInteger atomicInteger = new AtomicInteger(100);
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args)
{
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t"+"首次版本号:"+stamp);
//暂停500毫秒,保证后面的t4线程初始化拿到的版本号和我一样
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"2次流水号:"+stampedReference.getStamp());
stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"3次流水号:"+stampedReference.getStamp());
},"t3").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t"+"首次版本号:"+stamp);
//暂停1秒钟线程,等待上面的t3线程,发生了ABA问题
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
boolean b = stampedReference.compareAndSet(100, 2022, stamp, stamp + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.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, 2022)+"\t"+atomicInteger.get());
},"t2").start();
}
}
一句话:比较加版本号一起上