并发编程-共享模型之管程三(No5)
5.共享模型之内存
第4章讲解的Monitor主要关注的是访问共享变量时,保证临界区代码的原子性
本章深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题
5.1Java内存模型
JMM 即 Java Memory Model ,它定义了主存(所有线程都共享的数据)、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
原子性-保证指令不会受到线程上下文切换的影响
可见性-保证指令不会受CPU缓存的影响
有序性-保证指令不会受CPU指令并行优化的影响
5.2可见性
退不出的循环
@Slf4j
public class Test16 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (run){
//
}
});
t.start();
Thread.sleep(1);
run =false; //线程t不会停下来
}
}
原因:线程获取的是工作内存中的值,
![](https://i-blog.csdnimg.cn/blog_migrate/096b30f151d3aab80eac15b7fd0abbcd.png)
解决方法
volatile(容变关键字)
可以用来修饰成员变量和静态成员变量(不能修饰局部变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
@Slf4j
public class Test16 {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (run){
//
}
});
t.start();
Thread.sleep(1);
run =false; //静态成员变量加volatile,线程t会停下来
}
}
可以用synchronized的保证(不推荐,重量级)
@Slf4j
public class Test16 {
static boolean run = true;
final static Object obj=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (run){
synchronized (obj){
if (!run){
break; }
}}
});
t.start();
Thread.sleep(1);
synchronized (obj){
run =false;//使用synchronized加锁,线程t会停下来
}
}
}
可见性vs原子性
volatile保证的是在多个线程之间,一个线程对volatile变量的修改对另外一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况
注意:
synchronized语句块既可以保证代码的原子性,也同事保证代码块内变量的可见性。但缺点是它属于重量级操作,性能相对较低
如果在前面的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改(源码中有synchronized)
两阶段终止模式-改进
@Slf4j
public class test2 {
public static void main(String[] args) throws InterruptedException {
Termination termination = new Termination();
termination.start();
Thread.sleep(2000);
termination.stop();
}
}
@Slf4j
class Termination{
private Thread monitor;
private volatile boolean stop = false;
//启动监控线程
public void start(){
monitor=new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if (stop){
log.info("料理后事");
break;
}
try {
Thread.sleep(1000);
log.info("执行监控记录");
} catch (InterruptedException e) {
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
stop=true;
monitor.interrupt();
}
}
同步模式之Balking
Balking(犹豫)模式用在一个线程发现另外一个线程或本地线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
例如:
@Slf4j
public class MonitorService {
//用来表示是否已经有线程已经在执行启动了
private volatile boolean starting;
public void start(){
log.info("尝试启动监控线程...");
synchronized (this){
if (starting){
return;
}
starting=true;
}
//真正启动监控线程...
}
}
它可以经常用来实现线程安全的单例
public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
5.3有序性
JVM会在不影响正确性的前提下,可以调整语句的执行顺序
static int i;
static int j;
//在某个线程内执行赋值操作
i=...;
j=...;
//指令重排
// j=...;
// i=...;
指令重排-问题
int num = 0;
boolean ready = false;
//线程1执行此方法
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
//线程2执行此方法
public void actor2(I_Result r) {
num = 2;
ready =true;
//发生指令重排
//ready =true;
//num = 2;
}
以上结果正常情况r.r1=1或是4,发生指令重排值可能为0,线程2先执行到ready =true;发生线程切换,到线程1,此时会进入到
r.r1 = num + num;但是num还未来得及赋值,导致结果未0;
指令重排-解决
在ready变量上加volatile。即:volatile boolean ready = false;
volatile原理
底层原理
对volatile变量的写指令后会加入写屏障
对volatile变量的读指令后会加入读屏障
1.如何保证可见性
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存中
public void actor2(I_Result r) {
num = 2;
ready =true; // ready是volatile修饰
//写屏障
}
而读屏障保证在该屏障后,对共享变量的读取,加载的是主内存最新的数据
public void actor1(I_Result r) {
// ready是volatile修饰
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
2.如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
不能解决指令交错
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
而有序性的保证也只是保证了本线程内相关代码不被重排序
3.double-checked locking问题
double-checked locking单例模式为例,以下方方式是有误的
public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE==null){
synchronized (Singleton.class) {
if (INSTANCE == null) {
return INSTANCE;
}
return INSTANCE;
}
}
return INSTANCE;
}
}
以上实例化
懒汉式(用的时候才创建)
只需要首次进来加锁
4.double-checked locking问题解决
public class Singleton {
private Singleton() {
}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE==null){
synchronized (Singleton.class) {
if (INSTANCE == null) {
return INSTANCE;
}
return INSTANCE;
}
}
return INSTANCE;
}
}
happens-before
![](https://i-blog.csdnimg.cn/blog_migrate/e626b10573cf8a7ea2d8bfbd4b3ee560.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6df8c561a28fa2d443cbef14f3c9e596.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e106ac33ec78e24d2ed1e4aa6a112d35.png)
![](https://i-blog.csdnimg.cn/blog_migrate/41d7a37cd5135d1d86ebdf1d260d9d5d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9c6cd63eff25a108977f2c04104bcd3e.png)
balking习题
![](https://i-blog.csdnimg.cn/blog_migrate/80bb135cf4bc84059b4f22bf56571204.png)
上述是线程不安全的,volatile只能保证一个写,其他线程读,对于又读又写的指令交错并不能保证
线程安全单例模式习题
单例模式有很多实现方法,饿汉、懒汉、静态内部内、枚举类,试着分析每种实现下获取单例对象(即调用getInstance)时的线程安全,并思考注释中的问题
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会被创建
实现1
饿汉式
//问题1:为什么加final
//问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
//问题3:为什么设置私有?是否能防止反射创建新的实例
private Singleton() {
}
//问题4:这样初始化是否能保证单例对象创建时的线程安全
private static final Singleton INSTANCE = new Singleton();
//问题5:为什么提供静态方法而不是直接将getInstance方法设置为public
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResovle(){
return INSTANCE;
}
}
问题1:为什么加final
答:可能有子类继承它,可能覆盖父类中的方法,破坏单例
问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例
答:加上readResovle()方法,如上
问题3:为什么设置私有?是否能防止反射创建新的实例
答:防止别的类访问导致创建实例,不能防止反射
问题4:这样初始化是否能保证单例对象创建时的线程安全
答:没有,静态成员变量,它的初始化操作是在类加载的阶段完成,由jvm来保证线程的安全性
问题5:为什么提供静态方法而不是直接将getInstance方法设置为public
答:更好的封装,内部可以实现懒惰的初始化,还可以在创建单例对象时有更多的控制,可以有泛型的支持
实现2
enum Singleton{
INSTANCE;
}
问题1:枚举单例是如何限制实例个数的
相当于静态成员变量(枚举里面本身多少个实例就只会有多少个)
问题2:枚举单例在创建时是否有并发问题
没有,也是静态成员变量,线程安全性也是在类加载时完成的
问题3:枚举单例能否被反射破坏单例
不能
问题4:枚举单例能否被反序列化破坏单例
不能,虽然枚举父类是实现了序列化接口的,但是枚举类单例已经防止了这个问题
问题5:枚举单例属于懒汉式还是饿汉式
饿汉式
问题6:枚举单例如果希望加一些单例创建时的初始化逻辑该如何做
加构造方法
实现3
懒汉式
public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
//分析这里的线程安全,并说明有什么特点
public static synchronized Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
可以保证线程安全,但是每次都要加锁,效率低,参考double-checked locking双检测机制
实现4
public final class Singleton {
private Singleton() {
}
//问题1:属于饿汉式还是懒汉式
private static class LazyHolder{
static final Singleton INSTANCE = new Singleton();
}
//问题2:在创建时是否有并发问题
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
问题1:属于饿汉式还是懒汉式
懒汉式,这个是静态类部类,只有在调用getInstance()方法才会去创建
问题2:在创建时是否有并发问题
没有线程安全问题
本章小结
可见性 - 由JVM缓存优化引起
有序性 - 由JVM指令重排序优化引起
happens-before规则
CPU指令运行
两阶段终止模式的volatile改进
同步模式之balking