目录
前置知识
共享变量不可见性
JMM
volatile 关键字
使用volatile关键字
加锁
volatile 关键字 -- 更深入的问题
volatile不保证原子性
volatile禁止指令重排序
代码实例
public class VisibilityDemo01 {
// main方法,作为一个主线程。
public static void main(String[] args) {
// a.开启一个子线程
MyThread t = new MyThread();
t.start();
// b.主线程执行
while(true){
if(t.isFlag()){
System.out.println("主线程进入循环执行~~~~~");
}
}
}
}
class MyThread extends Thread{
// 成员变量
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 触发修改共享成员变量
flag = true;
System.out.println("flag="+flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
我们看到,子线程中已经将flag设置为true,但main()方法中始终没有读到修改后的最新值,从而循环没有能进入到if语句中执行,所以没有任何打印 , 这就是变量的不可见性
注意区别JMM和JVM
JVM和JMM是有区别的,它们是两个不同的概念:
JVM负责执行Java程序,并提供高级功能,而JMM则定义了线程和内存之间的抽象关系,以确保Java程序在多线程环境下的正确性
工作内存 和 主内存概念
JMM规定如下:
现在就可以解释 共享变量不可见性 的原因
为什么 main方法要去 读取自己工作内存中的 flag变量副本,而不每次都去主内存中读取,这类似 多级缓存的概念,线程从自己的工作内存中读取数据的速度会快于从主内存中读取数据的速度
如何实现在多线程下访问共享变量的可见性:也就是实现一个线程修改变量后,对其他线程可见呢?有两种方法
第一种是使用volatile关键字
第二种是加锁
使用volatile关键字修改该变量
private volatile boolean flag ;
运行结果
我们看到 使用volatile关键字解决了 共享变量不可见性的问题,即 一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
工作原理
修改main方法
// main方法
while(true) {
synchronized (t) {
if(t.isFlag()){
System.out.println("主线程进入循环执行~~~~~");
}
}
}
运行结果
可以看到同样是解决了 共享变量不可见性的问题
工作原理
虽然加锁同样能解决 共享变量不可见性的问题,但是 加锁 和 锁的释放 过程都是会有性能消耗的,所以在解决 共享变量不可见性的问题 时,首选 volatile关键字
原子性:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行
看如下程序,该程序开启了100个线程,同时对同一个变量进行自增10000次
public class VolatileDemo04 {
public static void main(String[] args) {
// 1.创建一个线程任务对象
Runnable target = new ThreadTarget01();
// 2.开始100个线程对象执行这个任务。
for(int i = 1 ; i <= 100 ; i++ ) {
new Thread(target,"第"+i+"个线程").start();
}
}
}
// 线程任务类
class ThreadTarget01 implements Runnable{
// 定义一个共享变量
private volatile int count = 0 ;
@Override
public void run() {
synchronized (ThreadTarget01.class){
for(int i = 1 ; i <= 10000 ; i++ ) {
count++;
System.out.println(Thread.currentThread().getName()+"count =========>>>> " + count);
}
}
}
}
最后的结果正常应该是 1000000
但是,实际上是有可能会少于 1000000 的
但是我已经运行了好多次,没有出先少于的情况,所以没运行结果哈哈
原理
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断
比如:
虽然计算了2次,但是只对A进行了1次修改
因此,在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)
要保证原子性操作,有两种方法:1、使用锁机制 2、原子类 这里不在展开讲
重排序:
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序
重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题,请看如下案例
public class OutOfOrderDemo06 {
// 新建几个静态变量
public static int a = 0 , b = 0;
public static int i = 0 , j = 0;
public static void main(String[] args) throws Exception {
int count = 0;
while(true){
count++;
a = 0 ;
b = 0 ;
i = 0 ;
j = 0 ;
// 定义两个线程。
// 线程A
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
i = b;
}
});
// 线程B
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
j = a;
}
});
t1.start();
t2.start();
t1.join(); // 让t1线程优先执行完毕
t2.join(); // 让t2线程优先执行完毕
// 得到线程执行完毕以后 变量的结果。
System.out.println("第"+count+"次输出结果:i = " +i +" , j = "+j);
if(i == 0 && j == 0){
break;
}
}
}
}
正常情况下,会有以下三种情况
但是,在很小的情况下会出现另外一种结果 i = 0 , j = 0
这就是发生重排序的结果
比如 线程1 中先执行了 i = b,然后切换到 进程2 且先执行 j = a,然后再分别执行 a = 1,b = 1
这样输出的结果就是 i = 0 , j = 0
而使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题 ,如下
public class OutOfOrderDemo07 {
// 新建几个静态变量
public static int a = 0 , b = 0;
public volatile static int i = 0 , j = 0;
public static void main(String[] args) throws Exception {
int count = 0;
while(true){
count++;
a = 0 ;
b = 0 ;
i = 0 ;
j = 0 ;
// 定义两个线程。
// 线程A
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
i = b;
}
});
// 线程B
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
j = a;
}
});
t1.start();
t2.start();
t1.join(); // 让t1线程优先执行完毕
t2.join(); // 让t2线程优先执行完毕
// 得到线程执行完毕以后 变量的结果。
System.out.println("第"+count+"次输出结果:i = " +i +" , j = "+j);
if(i == 0 && j == 0){
break;
}
}
}
}