线程基础知识复习
先拜拜大神
Doug Lea(道格.利)
零基础就业班讲解时阳哥JUC四大口诀
- 高内聚低耦合前提下,封装思想
线程 操作 资源类
- 判断、干活、通知
- 防止虚假唤醒,wait方法要注意使用while判断
- 注意标志位flag,可能是volatile的
为什么多线程极其重要???
硬件方面
摩尔定律失效
摩尔定律:
它是由英特尔创始人之一Gordon Moore(戈登·摩尔)提出来的。其内容为:
当价格不变时,集成电路上可容纳的元器件的数目约每隔18-24个月便会增加一倍,性能也将提升一倍。
换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。
可是从2003年开始CPU主频已经不再翻倍,而是采用多核而不是更快的主频。
摩尔定律失效。
在主频不再提高且核数在不断增加的情况下,要想让程序更快就要用到并行或并发编程。
软件方面
- zhuanability
- 高并发系统,异步+回调等生产需求
从start一个线程说起
Java线程理解以及openjdk中的实现
- private native void start0();
- Java语言本身底层就是C++语言
- OpenJDK源码网址
建议下载源码到本地观看:openjdk8\hotspot\src\share\vm\runtime
更加底层的C++源码解读
- openjdk8\jdk\src\share\native\java\lang
thread.c :
java线程是通过start的方法启动执行的,主要内容在native方法start0中,
Openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c
start0其实就是JVM_StartThread。此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。
- openjdk8\hotspot\src\share\vm\prims
jvm.cpp:
- openjdk8\hotspot\src\share\vm\runtime
thread.cpp:
Java多线程相关概念
进程
是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源
线程
在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程
⼀个进程会有1个或多个线程的
面试题:何为进程和线程?
管程
Monitor(监视器),也就是我们平时所说的锁
Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,
Object o = new Object();
new Thread(() -> {
synchronized (o)
{
}
},"t1").start();
Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
用户线程和守护线程
Java线程分为用户线程和守护线程,
线程的daemon属性为true表示是守护线程,false表示是用户线程
守护线程:是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作
code:
package com.atguigu.itdachang;
/**
* @auther zzyy
* @create 2020-07-07 15:39
*/
public class DaemonDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t 开始运行,"+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
while (true) {
}
}, "t1");
//线程的daemon属性为true表示是守护线程,false表示是用户线程
t1.setDaemon(true);
t1.start();
//3秒钟后主线程再运行
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("----------main线程运行完毕");
}
}
重点:
当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出
如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出
了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出
设置守护线程,需要在start()方法之前进行
CompletableFuture
Future和Callable接口
Future接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
Callable接口中定义了需要有返回的任务需要实现的方法
比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,
主线程就去做其他事情了,过了一会才去获取子任务的执行结果。
从之前的FutureTask说开去
本源的Future接口相关架构
Code
package com.zzyy.study.test;
import java.util.concurrent.*;
/**
* @auther zzyy
* @create 2020-06-14 17:02
*/
public class CompletableFutureDemo
{
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException
{
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println("-----come in FutureTask");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
return ""+ThreadLocalRandom.current().nextInt(100);
});
Thread t1 = new Thread(futureTask,"t1");
t1.start();
//3秒钟后才出来结果,还没有计算你提前来拿(只要一调用get方法,对于结果就是不见不散,会导致阻塞)
//System.out.println(Thread.currentThread().getName()+"\t"+futureTask.get());
//3秒钟后才出来结果,我只想等待1秒钟,过时不候
System.out.println(Thread.currentThread().getName()+"\t"+futureTask.get(1L,TimeUnit.SECONDS));
System.out.println(Thread.currentThread().getName()+"\t"+" run... here");
}
}
get()阻塞 =》一旦调用get()方法,不管是否计算完成都会导致阻塞,o(╥﹏╥)o
Code2
package com.zzyy.study.test;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println("-----come in FutureTask");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
return ""+ThreadLocalRandom.current().nextInt(100);
});
new Thread(futureTask,"t1").start();
System.out.println(Thread.currentThread().getName()+"\t"+"线程完成任务");
/**
* 用于阻塞式获取结果,如果想要异步获取结果,通常都会以轮询的方式去获取结果
*/
while(true)
{
if (futureTask.isDone())
{
System.out.println(futureTask.get());
break;
}
}
}
}
isDone()轮询 :
轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果.
如果想要异步获取结果,通常都会以轮询的方式去获取结果
尽量不要阻塞
小总结
不见不散、过时不候、轮询
想完成一些复杂的任务
应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果。
当Future集合中某个任务最快结束时,返回结果。
等待Future结合中的所有任务都完成。
对Future的改进
CompletableFuture和CompletionStage源码分别介绍
类架构说明:
接口CompletionStage:
代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。
类CompletableFuture:
核心的四个静态方法,来创建一个异步操作
runAsync 无 返回值:
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
supplyAsync 有 返回值:
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
上述Executor executor参数说明:
没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
Code:
无返回值:
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"-----come in");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----task is over");
});
System.out.println(future.get());
}
}
有返回值:
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextInt(100);
});
System.out.println(completableFuture.get());
}
}
Code之通用演示,减少阻塞和轮询:
从Java8开始引入了CompletableFuture,它是Future的功能增强版,
可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
package com.atguigu.juc.senior.inner.completablefuture;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-23 12:59
*/
public class cfuture4
{
public static void main(String[] args) throws Exception
{
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
int result = ThreadLocalRandom.current().nextInt(10);
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----计算结束耗时1秒钟,result: "+result);
if(result > 6)
{
int age = 10/0;
}
return result;
}).whenComplete((v,e) ->{
if(e == null)
{
System.out.println("-----result: "+v);
}
}).exceptionally(e -> {
System.out.println("-----exception: "+e.getCause()+"\t"+e.getMessage());
return -44;
});
//主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
解释下为什么默认线程池关闭,自定义线程池记得关闭
CompletableFuture的优点:
- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
案例精讲-从电商网站的比价需求说开去
函数式编程已经主流
大厂面试题看看
Lambda +Stream+链式调用+Java8函数式编程带走
先说说join和get对比
相同点:
join()和get()方法都是用来获取CompletableFuture异步之后的返回值
区别:
1.join()方法抛出的是uncheck异常(即RuntimeException),不会强制开发者抛出,
会将异常包装成CompletionException异常 /CancellationException异常,但是本质原因还是代码内存在的真正的异常,
public static void main(String[] args) {
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
int i =1/0;
return 1;
});
CompletableFuture.allOf(f1).join();
System.out.println("CompletableFuture Test");
}
异常信息
Exception in thread "main" java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1592)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.ArithmeticException: / by zero
at com.gabriel.stage.utils.IpAddressUtil.lambda$main$0(IpAddressUtil.java:44)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
... 5 more
2.get()方法抛出的是经过检查的异常,ExecutionException, InterruptedException 需要用户手动处理(抛出或者 try catch)
public static void main(String[] args) {
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
int i =1/0;
return 1;
});
try {
f1.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("CompletableFuture Test");
}
说说你过去工作中的项目亮点?
大厂业务需求说明
切记,功能→性能
对内微服务多系统调用
对外网站比价:
经常出现在等待某条 SQL 执行完成后,再继续执行下一条 SQL ,而这两条 SQL 本身是并无关系的,可以同时进行执行的。
我们希望能够两条 SQL 同时进行处理,而不是等待其中的某一条 SQL 完成后,再继续下一条。同理,
对于分布式微服务的调用,按照实际业务,如果是无关联step by step的业务,可以尝试是否可以多箭齐发,同时调用。
我们去比同一个商品在各个平台上的价格,要求获得一个清单列表,
1 step by step,查完京东查淘宝,查完淘宝查天猫......
2 all 一口气同时查询。。。。。
一波流Java8函数式编程带走
package com.zzyy.study.day628;
import lombok.Getter;
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;
/**
* @auther zzyy
* @create 2020-06-28 10:07
*/
public class T1
{
static List<NetMall> list = Arrays.asList(
new NetMall("jd"),
new NetMall("tmall"),
new NetMall("pdd"),
new NetMall("mi")
);
public static List<String> findPriceSync(List<NetMall> list,String productName)
{
return list.stream().map(mall -> String.format(productName+" %s price is %.2f",mall.getNetMallName(),mall.getPriceByName(productName))).collect(Collectors.toList());
}
public static List<String> findPriceASync(List<NetMall> list,String productName)
{
return list.stream().map(mall -> CompletableFuture.supplyAsync(() -> String.format(productName + " %s price is %.2f", mall.getNetMallName(), mall.getPriceByName(productName)))).collect(Collectors.toList()).stream().map(CompletableFuture::join).collect(Collectors.toList());
}
public static void main(String[] args)
{
long startTime = System.currentTimeMillis();
List<String> list1 = findPriceSync(list, "thinking in java");
for (String element : list1) {
System.out.println(element);
}
long endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒");
long startTime2 = System.currentTimeMillis();
List<String> list2 = findPriceASync(list, "thinking in java");
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 getPriceByName(String productName)
{
return calcPrice(productName);
}
private double calcPrice(String productName)
{
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return ThreadLocalRandom.current().nextDouble() + productName.charAt(0);
}
}
CompletableFuture常用方法
获得结果和触发计算
获取结果:
public T get() =》不见不散
public T get(long timeout, TimeUnit unit) =》过时不候
public T getNow(T valueIfAbsent) =》没有计算完成的情况下,给我一个替代结果
立即获取结果不阻塞=》计算完,返回计算完成后的结果
=》没算完,返回设定的valueIfAbsent值
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return 533;
});
//去掉注释上面计算没有完成,返回444
//开启注释上满计算完成,返回计算结果
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(completableFuture.getNow(444));
}
}
public T join()
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
System.out.println(CompletableFuture.supplyAsync(() -> "abc").thenApply(r -> r + "123").join());
}
}
主动触发计算:
public boolean complete(T value) =》是否打断get方法立即返回括号值
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return 533;
});
//注释掉暂停线程,get还没有算完只能返回complete方法设置的444;暂停2秒钟线程,异步线程能够计算完成返回get
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
//当调用CompletableFuture.get()被阻塞的时候,complete方法就是结束阻塞并get()获取设置的complete里面的值.
System.out.println(completableFuture.complete(444)+"\t"+completableFuture.get());
}
}
对计算结果进行处理
thenApply:
计算结果存在依赖关系,这两个线程串行化
package com.zzyy.study.test;
import java.util.concurrent.*;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
//当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化,
CompletableFuture.supplyAsync(() -> {
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("111");
return 1024;
}).thenApply(f -> {
System.out.println("222");
return f + 1;
}).thenApply(f -> {
//int age = 10/0; // 异常情况:那步出错就停在那步。
System.out.println("333");
return f + 1;
}).whenCompleteAsync((v,e) -> {
System.out.println("*****v: "+v);
}).exceptionally(e -> {
e.printStackTrace();
return null;
});
System.out.println("-----主线程结束,END");
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
handle
package com.zzyy.study.test;
import lombok.Getter;
import lombok.Setter;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
//当一个线程依赖另一个线程时用 handle 方法来把这两个线程串行化,
// 异常情况:有异常也可以往下一步走,根据带的异常参数可以进一步处理
CompletableFuture.supplyAsync(() -> {
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("111");
return 1024;
}).handle((f,e) -> {
int age = 10/0;
System.out.println("222");
return f + 1;
}).handle((f,e) -> {
System.out.println("333");
return f + 1;
}).whenCompleteAsync((v,e) -> {
System.out.println("*****v: "+v);
}).exceptionally(e -> {
e.printStackTrace();
return null;
});
System.out.println("-----主线程结束,END");
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
有异常也可以往下一步走,根据带的异常参数可以进一步处理
总结
对计算结果进行消费
接收任务的处理结果,并消费处理,无返回结果
thenAccept
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture.supplyAsync(() -> {
return 1;
}).thenApply(f -> {
return f + 2;
}).thenApply(f -> {
return f + 3;
}).thenApply(f -> {
return f + 4;
}).thenAccept(r -> System.out.println(r));
}
补充:
Code之任务之间的顺序执行
thenRun:
thenRun(Runnable runnable)
任务 A 执行完执行 B,并且 B 不需要 A 的结果
thenAccept:
thenAccept(Consumer action)
任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值
thenApply:
thenApply(Function fn)
任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB").join());
对计算速度进行选用
谁快用谁
applyToEither
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
return 10;
});
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return 20;
});
CompletableFuture<Integer> thenCombineResult = completableFuture1.applyToEither(completableFuture2,f -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
return f + 1;
});
System.out.println(Thread.currentThread().getName() + "\t" + thenCombineResult.get());
}
}
对计算结果进行合并
两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine 来处理
先完成的先等着,等待其它分支任务
thenCombine
code标准版,好理解先拆分
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
return 10;
});
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
return 20;
});
CompletableFuture<Integer> thenCombineResult = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
return x + y;
});
System.out.println(thenCombineResult.get());
}
}
code表达式
package com.zzyy.study.test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* @auther zzyy
* @create 2020-06-16 20:16
*/
public class CompletableFutureDemo2
{
public static void main(String[] args) throws ExecutionException, InterruptedException
{
CompletableFuture<Integer> thenCombineResult = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in 1");
return 10;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in 2");
return 20;
}), (x,y) -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in 3");
return x + y;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in 4");
return 30;
}),(a,b) -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in 5");
return a + b;
});
System.out.println("-----主线程结束,END");
System.out.println(thenCombineResult.get());
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
说说Java“锁”事
大厂面试题复盘
从轻松的乐观锁和悲观锁开讲
悲观锁
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现类都是悲观锁
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 显式的锁定之后再操作同步资源
伪代码说明
//=============悲观锁的调用方式
public synchronized void m1()
{
//加锁后的业务逻辑......
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
// 操作同步资源
}finally {
lock.unlock();
}
}
//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
乐观锁
- 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
- 乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
- 乐观锁一般有两种实现方式:采用版本号机制、CAS(Compare-and-Swap,即比较并替换)算法实现
通过8种情况演示锁运行案例,看看我们到底锁的是什么
承前启后的复习一下
锁相关的8种案例演示:
看看JVM中对应的锁在哪里?
synchronized有三种应用方式
JDK源码(notify方法)说明举例
8种锁的案例实际体现在3个地方:
- 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
- 作用于代码块,对括号里配置的对象加锁。
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
从字节码角度分析synchronized实现
javap -c ***.class文件反编译
-c 对代码进行反汇编
假如你需要更多信息:
javap -v ***.class文件反编译
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
synchronized同步代码块
javap -c ***.class文件反编译
反编译
实现使用的是monitorenter和monitorexit指令
m1方法里面自己添加一个异常试试
synchronized普通同步方法
javap -v ***.class文件反编译
反编译
synchronized普通同步方法:
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。
如果设置了,执行线程会将先持有monitor然后再执行方法,
最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor
synchronized静态同步方法
javap -v ***.class文件反编译
反编译
synchronized静态同步方法:
ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法
反编译synchronized锁的是什么
什么是管程monitor:
大厂面试题讲解
为什么任何一个对象都可以成为一个锁
管程:
管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
上述C++源码解读:
ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
objectMonitor.hpp
ObjectMonitor中有几个关键属性
_owner | 指向持有ObjectMonitor对象的线程 |
_WaitSet | 存放处于wait状态的线程队列 |
_EntryList | 存放处于等待锁block状态的线程队列 |
_recursions | 锁的重入次数 |
_count | 用来记录该线程获取锁的次数 |
每个对象天生都带着一个对象监视器
对于synchronized关键字,我们在《Synchronized与锁升级》章节还会再深度讲解
提前剧透,混个眼熟:
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,目前为了承前启后的学习,对下图先混个眼熟即可,O(∩_∩)O
公平锁和非公平锁
从ReentrantLock卖票编码演示公平和非公平现象
package com.atguigu.juc.senior.test;
import java.util.concurrent.locks.ReentrantLock;
class Ticket
{
private int number = 30;
ReentrantLock lock = new ReentrantLock();
public void sale()
{
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
/**
* @auther zzyy
* @create 2020-05-14 17:26
*/
public class SaleTicketDemo
{
public static void main(String[] args)
{
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"c").start();
}
}
何为公平锁/非公平锁?
⽣活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平
源码解读
按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;
先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以
面试题:
为什么会有公平锁/非公平锁的设计为什么默认非公平?
1、恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
2、使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
使⽤公平锁会有什么问题?
公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。
预埋伏AQS
可重入锁(又名递归锁)
说明:
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”这四个字分开来解释:
可:可以。重:再次。入:进入。锁:同步锁。 进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码
一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁
可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块:
package com.atguigu.juc.senior.prepare;
/**
* @auther zzyy
* @create 2020-05-14 11:59
*/
public class ReEntryLockDemo
{
public static void main(String[] args)
{
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("-----外层调用");
synchronized (objectLockA)
{
System.out.println("-----中层调用");
synchronized (objectLockA)
{
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
同步方法:
package com.atguigu.juc.senior.prepare;
/**
* @auther zzyy
* @create 2020-05-14 11:59
* 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEntryLockDemo
{
public synchronized void m1()
{
System.out.println("-----m1");
m2();
}
public synchronized void m2()
{
System.out.println("-----m2");
m3();
}
public synchronized void m3()
{
System.out.println("-----m3");
}
public static void main(String[] args)
{
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
reEntryLockDemo.m1();
}
}
Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
为什么任何一个对象都可以成为一个锁
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
package com.atguigu.juc.senior.prepare;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2020-05-14 11:59
* 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEntryLockDemo
{
static Lock lock = new ReentrantLock();
public static void main(String[] args)
{
new Thread(() -> {
lock.lock();
try
{
System.out.println("----外层调用lock");
lock.lock();
try
{
System.out.println("----内层调用lock");
}finally {
// 这里故意注释,实现加锁次数和释放次数不一样
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock(); // 正常情况,加锁几次就要解锁几次
}
}finally {
lock.unlock();
}
},"a").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("b thread----外层调用lock");
}finally {
lock.unlock();
}
},"b").start();
}
}
死锁及排查
是什么
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生死锁主要原因:
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
请写一个死锁代码case
package com.atguigu.juc.senior.prepare;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-05-14 10:56
*/
public class DeadLockDemo
{
public static void main(String[] args)
{
final Object objectLockA = new Object();
final Object objectLockB = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
}
}
},"A").start();
new Thread(() -> {
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
}
}
},"B").start();
}
}
如何排查死锁
纯命令: jps -l 和 jstack 进程编号
图形化: jconsole
写锁(独占锁)/读锁(共享锁)
自旋锁SpinLock
无锁→独占锁→读写锁→邮戳锁
有没有比读写锁更快的锁?StampedLock
无锁→偏向锁→轻量锁→重量锁
其它细节
不可以String同一把锁 =》 严禁这么做
LockSupport与线程中断
线程中断机制
从阿里蚂蚁金服面试题讲起
如何停止、中断一个运行中的线程??
什么是中断?
首先
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的机制——中断。
中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
中断的相关API方法
public void interrupt() | 实例方法, 实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程 |
public static boolean interrupted() | 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态 这个方法做了两件事: 1 返回当前线程的中断状态 2 将当前线程的中断状态设为false 这个方法有点不好理解,因为连续调用两次的结果可能不一样。 |
public boolean isInterrupted() | 实例方法, 判断当前线程是否被中断(通过检查中断标志位) |
面试题:如何使用中断标识停止线程?
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑。
- 修改状态
- 停止程序的运行
方法:
- 通过一个volatile变量实现
package com.atguigu.juc.senior.test;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-05-12 14:19
*/
public class InterruptDemo
{
private static volatile boolean isStop = false;
public static void main(String[] args)
{
new Thread(() -> {
while(true)
{
if(isStop)
{
System.out.println(Thread.currentThread().getName()+"线程------isStop = true,自己退出了");
break;
}
System.out.println("-------hello interrupt");
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
isStop = true;
}
}
- 通过AtomicBoolean
package com.zzyy.study.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @auther zzyy
* @create 2020-05-26 23:24
*/
public class StopThreadDemo
{
private final static AtomicBoolean atomicBoolean = new AtomicBoolean(true);
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
while(atomicBoolean.get())
{
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----hello");
}
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
atomicBoolean.set(false);
}
}
- 通过Thread类自带的中断api方法实现
API
public void interrupt() | 实例方法, 实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程 |
public static boolean interrupted() | 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态 这个方法做了两件事: 1 返回当前线程的中断状态 2 将当前线程的中断状态设为false 这个方法有点不好理解,因为连续调用两次的结果可能不一样。 |
public boolean isInterrupted() | 实例方法, 判断当前线程是否被中断(通过检查中断标志位) |
实例方法interrupt(),没有返回值
public void interrupt() =>实例方法,调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真正立刻停止线程。
实例方法isInterrupted,返回布尔值
public boolean isInterrupted()=>实例方法,获取中断标志位的当前值是什么,判断当前线程是否被中断(通过检查中断标志位),默认是false
code:
package com.atguigu.itdachang;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-07-10 17:33
*/
public class InterruptDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
while(true)
{
if(Thread.currentThread().isInterrupted())
{
System.out.println("-----t1 线程被中断了,break,程序结束");
break;
}
System.out.println("-----hello");
}
}, "t1");
t1.start();
System.out.println("**************"+t1.isInterrupted());
//暂停5毫秒
try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
t1.interrupt();
System.out.println("**************"+t1.isInterrupted());
}
}
当前线程的中断标识为true,是不是就立刻停止?
具体来说,当对一个线程,调用 interrupt() 时:
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。
被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,
那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
code02
package com.atguigu.juc.senior.test;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-05-13 10:25
*/
public class InterruptDemo2
{
public static void main(String[] args) throws InterruptedException
{
Thread t1 = new Thread(() -> {
for (int i=0;i<300;i++) {
System.out.println("-------"+i);
}
System.out.println("after t1.interrupt()--第2次---: "+Thread.currentThread().isInterrupted());
},"t1");
t1.start();
System.out.println("before t1.interrupt()----: "+t1.isInterrupted());
//实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
t1.interrupt();
//活动状态,t1线程还在执行中
try { TimeUnit.MILLISECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("after t1.interrupt()--第1次---: "+t1.isInterrupted());
//非活动状态,t1线程不在执行中,已经结束执行了。
try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("after t1.interrupt()--第3次---: "+t1.isInterrupted());
}
}
code02后手案例(重要,面试就是它,操蛋)
结论:
小总结:中断只是一种协同机制,修改中断标识位仅此而已,不是立刻stop打断
静态方法Thread.interrupted()
code:
package com.atguigu.juc.senior.test;
/**
* @auther zzyy
* @create 2020-05-12 14:19
* 作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
* 第二次再调用时中断状态已经被清除,将返回一个false。
*/
public class InterruptDemo
{
public static void main(String[] args) throws InterruptedException
{
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println("111111");
Thread.currentThread().interrupt();
System.out.println("222222");
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
}
}
说明
public static boolean interrupted() =>静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态,类似i++这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false
这个方法有点不好理解,因为连续调用两次的结果可能不一样。
都会返回中断状态,两者对比
结论:
方法的注释也清晰的表达了“中断状态将会根据传入的ClearInterrupted参数值确定是否重置”。
所以,静态方法interrupted将 会清除中断状态(传入的参数ClearInterrupted为true),
实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)。
总结
线程中断相关的方法:
- interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。
- isInterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
- Thread类的静态方法interrupted(),返回当前线程的中断状态(boolean类型)且将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false
LockSupport是什么
是什么
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
下面这句话,后面详细说
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程
线程等待唤醒机制
3种让线程等待和唤醒的方法
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-04-13 17:12
*
* 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
*
* 1 正常程序演示
*
* 以下异常情况:
* 2 wait方法和notify方法,两个都去掉同步代码块后看运行效果
* 2.1 异常情况
* Exception in thread "t1" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method)
* Exception in thread "t2" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method)
* 2.2 结论
* Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)。
*
* 3 将notify放在wait方法前面
* 3.1 程序一直无法结束
* 3.2 结论
* 先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
*/
public class LockSupportDemo
{
public static void main(String[] args)//main方法,主线程一切程序入口
{
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
//objectLock.notify();
/*synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
},"t2").start();
}
}
正常:
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-04-13 17:12
*
* 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
*
* 1 正常程序演示
*
*/
public class LockSupportDemo
{
public static void main(String[] args)//main方法,主线程一切程序入口
{
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
},"t2").start();
}
}
异常1:
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-04-13 17:12
*
* 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
* 以下异常情况:
* 2 wait方法和notify方法,两个都去掉同步代码块后看运行效果
* 2.1 异常情况
* Exception in thread "t1" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method)
* Exception in thread "t2" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method)
* 2.2 结论
* Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)。
*/
public class LockSupportDemo
{
public static void main(String[] args)//main方法,主线程一切程序入口
{
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
objectLock.notify();
},"t2").start();
}
}
wait方法和notify方法,两个都去掉同步代码块
异常情况:
异常2:
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-04-13 17:12
*
* 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
*
* 3 将notify放在wait方法前先执行,t1先notify了,3秒钟后t2线程再执行wait方法
* 3.1 程序一直无法结束
* 3.2 结论
* 先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
*/
public class LockSupportDemo
{
public static void main(String[] args)//main方法,主线程一切程序入口
{
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
},"t1").start();
//t1先notify了,3秒钟后t2线程再执行wait方法
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
},"t2").start();
}
}
将notify放在wait方法前面
程序无法执行,无法唤醒
小总结:wait和notify方法必须要在同步块或者方法里面,且成对出现使用;先wait后notify才OK
Condition接口中的await后signal方法实现线程的等待和唤醒
正常:
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2020-04-13 17:55
*/
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"+"start");
condition.await();
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
lock.lock();
try
{
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
},"t2").start();
}
}
异常1
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2020-04-13 17:55
* 异常:
* condition.await();和condition.signal();都触发了IllegalMonitorStateException异常
*
* 原因:调用condition中线程等待和唤醒的方法的前提是,要在lock和unlock方法中,要有锁才能调用
*/
public class LockSupportDemo2
{
public static void main(String[] args)
{
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"start");
condition.await();
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
try
{
condition.signal();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
},"t2").start();
}
}
去掉lock/unlock
condition.await();和 condition.signal();都触发了 IllegalMonitorStateException异常。
结论:
lock、unlock对里面才能正确调用调用condition中线程等待和唤醒的方法
异常2
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2020-04-13 17:55
* 异常:
* 程序无法运行
*
* 原因:先await()后signal才OK,否则线程无法被唤醒
*/
public class LockSupportDemo2
{
public static void main(String[] args)
{
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try
{
condition.signal();
System.out.println(Thread.currentThread().getName()+"\t"+"signal");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"等待被唤醒");
condition.await();
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"t2").start();
}
}
先signal后await
小总结:Condtion中的线程等待和唤醒方法之前,需要先获取锁;一定要先await后signal,不要反了
Object和Condition使用的限制条件
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
是什么:
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
官网解释:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
permit只有两个值1和零,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
主要方法
阻塞=》park() /park(Object blocker) 调用LockSupport.park()时
permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,
然后会将permit再次设置为零并返回。
阻塞当前线程/阻塞传入的具体线程
唤醒 =》unpark(Thread thread) ,LockSupport.unpark(thread);
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
唤醒处于阻塞状态的指定线程
代码
正常+无锁块要求
package com.atguigu.juc.prepare;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @auther zzyy
* @create 2020-04-13 20:30
*/
public class LockSupportDemo3
{
public static void main(String[] args)
{
//正常使用+不需要锁块
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+" "+"1111111111111");
LockSupport.park();
System.out.println(Thread.currentThread().getName()+" "+"2222222222222------end被唤醒");
},"t1");
t1.start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+" -----LockSupport.unparrk() invoked over");
}
}
之前错误的先唤醒后等待,LockSupport照样支持
package com.zzyy.study.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @auther zzyy
* @create 2020-05-06 17:07
*/
public class T1
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---被叫醒");
},"t1");
t1.start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---unpark over");
}
}
成双成对要牢记
Java内存模型之JMM
先从大厂面试题开始
- 你知道什么是Java内存模型JMM吗?
- JMM与volatile它们两个之间的关系?(下一章详细讲解)
- JMM有哪些特性or它的三大特性是什么?
- 为什么要有JMM,它为什么出现?作用和功能是什么?
- happens-before先行发生原则你有了解过吗?
计算机硬件存储体系
计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。
一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算
问题?和推导出我们需要知道JMM
因为有这么多级的缓存(cpu和物理主内存的速度不一致的),
CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题
Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异,
以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出我们需要知道JMM
Java内存模型Java Memory Model
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则:
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
1 通过JMM来实现线程和主内存之间的抽象关系。
2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM规范下,三大特性
可见性
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。
Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
线程脏读:如果没有可见性保证
主内存中有变量 x,初始值为 0 |
线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值 |
线程 A 将更新后的 x 值回刷到主内存的时间是不固定的 |
刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会变成 x=1 |
原子性
指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
有序性
public void mySort()
{
int x = 11; //语句1
int y = 12; //语句2
x = x + 5; //语句3
y = x * x; //语句4
}
1234
2134
1324
问题:请问语句4可以重排后变成第一个条吗?
JMM规范下,多线程对变量的读写过程
读取过程:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
JMM定义了线程和主内存之间的抽象关系
1 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
2 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
小总结 :
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
JMM规范下,多线程先行发生原则之happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者 代码重排序,那么这两个操作之间必须存在happens-before关系。
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原则的威力。-------------------》包含可见性和有序性的约束
先行发生原则说明 :
如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,
但是我们在编写Java并发代码的时候并没有察觉到这一点。
我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下
有一个“先行发生”(Happens-Before)的原则限制和规矩
这个原则非常重要:
它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操
作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
happens-before总原则
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
值日=》周一张三周二李四,假如有事情调换班可以的
1+2+3 = 3+2+1
happens-before之8条
次序规则: 一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;加深说明,前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1。
锁定规则:一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;
package com.zzyy.study.test;
/**
* @auther zzyy
* @create 2020-06-11 16:01
*/
public class HappenBeforeDemo
{
static Object objectLock = new Object();
public static void main(String[] args) throws InterruptedException
{
//对于同一把锁objectLock,threadA一定先unlock同一把锁后B才能获得该锁, A 先行发生于B
synchronized (objectLock)
{
}
}
}
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检
测,我们可以通过Thread::join()方法是否结束、
Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始,说人话=》对象没有完成初始化之前,是不能调用finalized()方法的
案例说明
假设存在线程A和B,
线程A先(时间上的先后)调用了setValue(1),
然后线程B调用了同一个对象的getValue(),
那么线程B收到的返回值是什么?
解释:
我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 可以忽略,因为他们和这段代码毫无关系):
1 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足程序次序规则;
2 两个方法都没有使用锁,所以不满足锁定规则;
3 变量不是用volatile修饰的,所以volatile变量规则不满足;
4 传递规则肯定不满足;
所以我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,
但就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?
修复:把getter/setter方法都定义为synchronized方法,把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
volatile与Java内存模型
被volatile修改的变量有2大特点
特点
可见性
有序性=》排序要求
volatile的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
内存屏障(面试重点必须拿下)
先说生活case
没有管控,顺序难保
设定规则,禁止乱序(上海南京路步行街武警“人墙”当红灯)
是什么
内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。
volatile凭什么可以保证可见性和有序性???
内存屏障 (Memory Barriers / Fences)
JVM中提供了四类内存屏障指令
一句话:上一章讲解过happens-before先行发生原则,类似接口规范,落地?
落地靠什么?你凭什么可以保证?你管用吗?
C++源码分析:IDEA工具里面找Unsafe.class
Unsafe.java:
Unsafe.cpp
OrderAccess.hpp
orderAccess_linux_x86.inline.hpp
四大屏障分别是什么意思
orderAccess_linux_x86.inline.hpp
如下内容困难,可能会导致学生懵逼,课堂要讲解细致分2次讲解+复习
happens-before 之 volatile 变量规则
- 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
- 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
- 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
JMM 就将内存屏障插⼊策略分为 4 种:
写
1. 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
2. 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障
对比图
读
3. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
4. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障
volatile特性
保证可见性
说明:保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见
Code:
package com.zzyy.study.juc;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2020-06-30 11:29
*/
public class VolatileSeeDemo
{
static boolean flag = true; //不加volatile,没有可见性
//static volatile boolean flag = true; //加了volatile,保证可见性
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();
//暂停2秒钟后让main线程修改flag值
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
flag = false;
System.out.println("main线程修改完成");
}
}
不加volatile,没有可见性,程序无法停止
加了volatile,保证可见性,程序可以停止
上述代码原理解释
线程t1中为何看不到被主线程main修改为false的flag的值?
问题可能:
1. 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
2. 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。
我们的诉求:
1.线程中修改了工作内存中的副本之后,立即将其刷新到主内存;
2.工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
解决:
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
volatile变量的读写过程
Java内存模型中定义的8种工作内存与主内存之间的原子操作
read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)
read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
没有原子性
volatile变量的复合操作(如i++)不具有原子性
Code:
package com.zzyy.study.juc;
import java.util.concurrent.TimeUnit;
class MyNumber
{
volatile int number = 0;
public void addPlusPlus()
{
number++;
}
}
public class VolatileNoAtomicDemo
{
public static void main(String[] args) throws InterruptedException
{
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(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);
}
}
从i++的字节码角度说明
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
public void add()
{
i++; //不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分3步完成
}
如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,
并执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全.
不保证原子性:
多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致
对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。
由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步
读取赋值一个普通变量的情况: 当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套操作