文章目录
Java关键字——synchronized: https://blog.csdn.net/qq_41822345/article/details/105144315
一、多线程下变量的不可见性
在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值。
代码示例:
package com.liuwen.JVM虚拟机.关键字synchronized.三个特性;
import java.util.concurrent.TimeUnit;
/**
* @description: 验证可见性
* @author: Liu Wen 一个线程对共享变量的修改,另一个线程不能立即得到最新值
* @create: 2020-03-26 23:13
**/
public class Test01Visibility {
public static boolean flag = true;
// public static volatile boolean flag = true; //保证可见性
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
//代码不会停止
}
},"Thread-1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
flag = false;
System.out.println(Thread.currentThread().getName()+"已经将flag改为:"+flag);
},"Thread-2").start();
}
}
子线程中已经将flag设置为true,但main()方法中始终没有读到修改后的最新值,从而循环不会停止。
分析原因:这要从Java内存模型(JMM)说起。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
JMM有以下规定:
1.所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
2.每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
3.线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
4.不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
所以:
1.子线程t从主内存读取到数据放入其对应的工作内存
2.将flag的值更改为true,但是这个时候flag的值还没有写回主内存
3.此时main方法读取到了flag的值为false
4.当子线程t将flag的值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值。
所以while(true)读取到的值一直是false。(如果有一个时刻main线程从主内存中读取到了主内存中flag的最新值,那么if语句就可以执行,main线程何时从主内存中读取最新的值,我们无法控制)
总结原因:所有共享变量存在于主内存中,每个线程由自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
二、不可见性的解决方案
1.加锁synchronized
代码示例:
package com.liuwen.JVM虚拟机.关键字synchronized.三个特性;
import java.util.concurrent.TimeUnit;
/**
* @description: 验证可见性
* @author: Liu Wen 一个线程对共享变量的修改,另一个线程不能立即得到最新值
* @create: 2020-03-26 23:13
**/
public class Test01Visibility {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
// 增加对共享数据的打印,println是同步方法
System.out.println(flag);
}
},"Thread-1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
flag = false;
System.out.println(Thread.currentThread().getName()+"已经将flag改为:"+flag);
},"Thread-2").start();
}
}
因为println()是同步方法,它是由synchronized关键字修饰的,对于加锁有以下两个规则:
1.如果对一个变量执行lock操作,将会清空工作内存中此变量的值。
2.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中。
所以:某一个线程进入synchronized代码块前后,执行过程入如下:
a.线程获得锁(lock)
b.清空工作内存
c.从主内存拷贝共享变量最新的值到工作内存成为副本
d.执行代码
e.将修改后的副本的值刷新回主内存中
f.线程释放锁(unlock)
2.volatile关键字修饰
代码示例:
package com.liuwen.JVM虚拟机.关键字synchronized.三个特性;
import java.util.concurrent.TimeUnit;
/**
* @description: 验证可见性
* @author: Liu Wen 一个线程对共享变量的修改,另一个线程不能立即得到最新值
* @create: 2020-03-26 23:13
**/
public class Test01Visibility {
public static volatile boolean flag = true; //保证可见性
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
//代码运行完毕
}
},"Thread-1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
flag = false;
System.out.println(Thread.currentThread().getName()+"已经将flag改为:"+flag);
},"Thread-2").start();
}
}
volatile关键字工作原理:
- 子线程t从主内存读取到数据放入其对应的工作内存
- 将flag的值更改为true,但是这个时候flag的值还没有写会主内存
- 此时main方法main方法读取到了flag的值为false
- 当子线程t将flag的值写回去后,失效其他线程对此变量副本
- 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中
总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
建议:使用volatile关键字,它是一个轻量级操作,保证并发共享变量可见性的同时,还能保证高并发性能。即比synchronized更高效,一样安全。
三、volatile关键字的特性
1.volatile可以实现并发下共享变量的可见性
这个见:二、不可见性的解决方案
2.volatile不保证原子性
这个典型的例子就是number++操作,它包含三个步骤:
从主内存中读取数据到工作内存;
对工作内存中的数据进行++操作;
将工作内存中的数据写回到主内存;
代码示例:
package com.liuwen.JVM虚拟机.关键字synchronized.三个特性;
import java.util.ArrayList;
/**
* @description: 验证原子性
* @author: Liu Wen
* @create: 2020-03-26 23:26
**/
public class Test02Atomicity {
private static volatile int number = 0; //不保证原子操作
public static void main(String[] args) throws InterruptedException{
Runnable runnable = ()->{
for (int i = 0; i < 1000; i++) {
number++;
}
};
ArrayList<Thread> arrayList = new ArrayList<>();
//开启五个线程运行runnable任务。
for (int i = 0; i < 5; i++) {
Thread t = new Thread(runnable);
t.start();
arrayList.add(t);
}
//为了避免main主线程先执行完毕
for (Thread t : arrayList) {
t.join();
}
System.out.println("number:"+number);
}
}
/*
输出<=5000
*/
分析原因:
1)假设此时number的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量number的值。由CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态。
2)线程B也需要从主内存中读取number变量的值,由于线程A没有对number值做任何修改因此此时B读取到的数据还是100。
3)线程B工作内存中number执行了+1操作,但是未刷新之主内存中。
4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作。
5)线程B将101写入到主内存。
6)线程A将101写入到主内存。
虽然计算了2次,但是只对number进行了1次修改。
如何解决?
1.使用加锁机制。在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。
2.使用原子类。java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效(CAS操作代替锁),线程安全地更新一个变量的方式。
3.volatile关键字可以禁止指令重排序(可以保证有序性)
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。一般重排序可以分为如下三种:
1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
源代码→编译器优化重排→指令级并行重排→内存系统重排→最终执行的指令序列
重排序可以提高处理速度:
使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题。
代码示例:
package com.liuwen.JVM虚拟机.关键字synchronized.三个特性;
/**
* @description: volatile 可以禁止指令重排序
* @author: Liu Wen
* @create: 2020-03-27 19:05
**/
public class Test03Order2 {
private static int i = 0, j = 0; //未禁止重排序
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int count = 0; // 计数
while(true) {
count++;
i = 0;
j = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
i = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
j = a;
}
});
two.start();
one.start();
one.join();
two.join();
String result = "第" + count + "次( i= " + i + ", j= " + j + ")";
if (i == 1 && j == 1) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
/*
代码会出现i=0,j=0的输出
*/
分析原因:在线程1和线程2内部的两行代码的实际执行顺序和代码在Java文件中的顺序是不一致的,代码指令并不是严格按照代码顺序执行的,他们的顺序改变了,这样就是发生了重排序,这里颠倒的是 a = 1 ,i = b 以及b=1,j=a的顺序,从而发生了指令重排序。造成i=b(0)先于b=1执行,j=a(0)先于a=1执行。显然这个值是不对的。
解决方案:
private volatile static int i = 0, j = 0; //禁止重排序
private volatile static int a = 0, b = 0;
总结:使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题。
为什么volatile关键字可以禁止重排序?↓
4.volatile写读建立的happens-before关系
背景:指令重排序导致的多个线程操作之间的不可见性。如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。
从JDK 5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。(happens-before原则,让线程之间遵守这些原则,相当于给了编译器优化的约束)。
happens-before规则一共6条:
1.程序顺序规则(单线程规则)
解释:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
同一个线程中前面的所有写操作对后面的操作可见。
2.锁规则(Synchronized,Lock等)
解释:对一个锁的解锁,happens-before于随后对这个锁的加锁。
如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2也可以是同一个线程)。
3.volatile变量规则
解释:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2也可以是同一个线程)。
4.传递性
解释:如果A happens-before B,且B happens-before C,那么A happens-before C。
A h-b B , B h-b C 那么可以得到 A h-b C。
5.start()规则
解释:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happensbefore于线程B中的任意操作。
假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见
6.join()规则
解释:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。
volatile重排序规则:
- 写volatile变量时,无论前一个操作是什么,都不能重排序。
- 读volatile变量时,无论后一个操作是什么,都不能重排序。
- 当先写volatile变量,后读volatile变量时,不能重排序。
四、volatile高频面试与总结
1.long和double的原子性
在java中,long和double都是8个字节,共64位(一个字节=8bit),那么如果是一个32位的系统,读写long或double的变量时会涉及到原子性问题,因为32位的系统要读完一个64位的变量,需要分两步执行,每次读取32位,这样就对double和long变量的赋值就会出现问题: 如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。
结论:如果是在64位的系统中,那么对64位的long和double的读写都是原子操作的,即可以以一次性读写long或double的整个64bit。如果在32位的JVM上,long和double就不是原子性操作了。
解决方法:需要使用volatile关键字来防止此类现象。
- 如果使用volatile修饰long和double,那么其读写都是原子操作;
- java中对于其他类型的读写操作都是原子操作(除long和double类型以外);
- 对于引用类型的读写操作都是原子操作,无论引用类型的实际类型是32位的值还是64位的值;
- Java商业虚拟机已经解决了long和double的读写操作的原子性。
2.volatile在双重检查加锁的单例中的应用
单例是需要在内存中永远只能创建一个类的实例。
单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。
单例模式的适用场景:
全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
单例模式有8种,两种饿汉单例,四种懒汉单例,一种静态内部类单例,一种枚举单例。
8种单例模式:https://blog.csdn.net/qq_41822345/article/details/104488560
懒汉式(volatile双重检查模式,推荐):线程安全,延迟加载,效率较高!
package com.liuwen.JVM虚拟机.关键字volatile.单例模式;
/**
* @description: 懒汉式(volatile双重检查模式,推荐)
* @author: Liu Wen
* @create: 2020-03-27 23:34
**/
public class SingletonDemo6 {
//静态属性,volatile保证可见性和禁止指令重排序
private volatile static SingletonDemo6 INSTANCE;
//私有化构造,不对外提供访问
private SingletonDemo6(){
}
public static SingletonDemo6 getInstance(){
// 第一重检查锁定
if(INSTANCE==null){
// 同步锁定代码块
synchronized (SingletonDemo6.class){
// 第二重检查锁定
if(INSTANCE==null){
// 注意:这里是非原子操作,所以需要volatile关键字修饰INSTANCE
INSTANCE = new SingletonDemo6();
}
}
}
return INSTANCE;
}
}
1.为什么要保证可见性?
- 由于可见性问题,线程A在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程B在主存中判断instance还是null,那么线程B又将在自己的工作线程中创建一个实例,这样就创建了多个实例。
- 如果加上了volatile修饰instance之后,保证了可见性,一旦线程A返回了实例,线程B可以立即发现Instance不为null。
2.为什么要禁止重排序?
- 对象实际上创建对象要进过如下几个步骤: a. 分配内存空间。
b. 调用构造器, 初始化实例。
c. 返回地址给引用 - 所以,new Singleton()是一个非原子操作,编译器可能会重排序而线程B在线程A赋值完时判断instance就不为null了,此时B拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程C会继续拿着个没有初始化的对象中的数据进行操作,此时容易触发“NPE异常”( NullPointerException )。
3.volatile的使用场景
1.赋值操作
volatile不适合做a++等操作,适合做纯复制操作:如 boolean flag = false/true;
volatile可以适合做多线程中的纯赋值操作:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
2.触发器
按照volatile的可见性和禁止重排序以及happens-before规则,volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见。
4.volatile与synchronized的区别
- volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
- volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
- volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
- volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。
五、总结
- volatile 修饰符适用于以下场景;某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,但它不能替代synchronized ,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
- volatile提供了happens-before保证,对volatile变量v的写入happens- before所有其他线程后续对v的读操作。
- volatile可以使得long和double的赋值是原子的。
- volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。