文章目录
- 前言
- 一、简介
-
* [volatile](about:blank#volatile_11)-
* [Q: volatile 如何保证数据可见性?](about:blank#Q_volatile__16) - synchronized
-
- 二、名词解释
-
* * [可见性](about:blank#_27) - 二、实战使用
-
* * [1 Volatile可以解决的问题](about:blank#1_Volatile_51) - 总结
前言
volatile与synchronized 都是java的关键字
- volatile一般修饰变量,被修饰的变量会及时将计算值刷新回主内存;
- synchronized 一般作用于 方法, 代码块等,一般分为 对象锁 和 类锁;
一、简介
volatile
- 能够保证数据可见性
- 禁止指令重排,在一定程度上保证了多线程情况下的执行顺序,从而保证结果执行正确
- 不能完全保证多线程情况下的结果正确: 非原子性操作无法保证
Q: volatile
如何保证数据可见性?
1)将当前处理器缓存行的数据写回到系统内存。(会引起处理器缓存回写到内存)
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。(一个处理器的缓存回写到内存会导致其他处理器的缓存无效)
synchronized
- 被synchronized 修饰后,大体分为两种,一种是对象锁,一种是类锁
- synchronized 可以保证被修饰的部分,一个时间点只有一个线程执行
- 在多线程情况下,相当于将多线程转为了串行执行,不能完全达到多线程的性能,但是可以通过控制锁的粒度,尽量减少锁的时间;
- 对象锁,锁住的是实例对象,类锁,锁住的是字节码
二、名词解释
可见性
- 当写一个 volatile 变量时,JMM 会把该线程本地内存中的变量强制刷新到主内存中去;这个写操作会导致其他线程中的 volatile 变量缓存无效,无效后也就是强制让其他线程再次读取主内存数据,达到了数据被修改后能被其他线程立马感知到,即可见性;
原子性
- 没错,与事务中的原子性可以认为是等价的;
指令重排
-
jvm为了优化代码运行的一种手段,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
-
举个例子 ABC 三步
int a = 1; // A操作 int b = 2; // B操作 int sum = a + b;// C 操作 System.out.println(sum);
多线程情况下, 可能是ABC 也可能是 BAC,这种指令重排不会影响结果,所以是允许的;
临界区
- 所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。说人话就是: 被锁 锁住的代码部分;
对象锁
当synchronized 作用于普通方法,代码块,this 的时候,都是对象锁,每个对象实例一把锁,多个对象实例多个锁,多个锁之间没有影响, 那么多个对象实例之间的数据就不应该用对象锁,只适合单个对象;
类锁
当synchronized 作用于静态方法,this.getClass() 的时候,因为每个对象只有一份class,也就是唯一的,所以这种锁,无论你new 多少个对象实例,它们都公用一把锁
二、实战使用
1
Volatile可以解决的问题
public class Volatile可以解决的问题 {
private boolean flag = true;
// private volatile boolean flag = true;
private int num = 0;
public void write() {
flag = false;
System.out.println(Thread.currentThread().getName() + "开始执行.....");
}
public void read() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
num++;
}
System.out.println(Thread.currentThread().getName() + "结束.....");
System.out.println("跳出循环" + num);
}
public static void main(String[] args) throws InterruptedException {
Volatile可以解决的问题 handle = new Volatile可以解决的问题();
new Thread(handle::read).start();
Thread.sleep(2000);
new Thread(handle::write).start();
}
}
两个线程运行 第一个线程判断 当 flag 为ture ,就一直执行
第二个线程改变flag 的值,时期变为flase;
如果不使用volatile修饰,那么flag 虽然会被改变,但是由于没有强制刷新会主内存,那么其实第一个线程也感知不到
所以仍然会继续num++ ,不会停止;
解决问题的关键在于,volatile可以强制将线程内部的计算结果,刷新会主内存,并且使其他线程的值失效,不得不再次去主内存获取
就是如上代码,.不用volatile修饰的时候,就会导致即使改变了flag 的值,但是第一个线程仍然无法感知到,所以就一直运行,不会结束
上述代码中如果在while 循环中加入一个打印语句,例如打印num 数值,或者加入一句打印的话,都会导致非volatile修饰的也能正常退出,这是因为:
线程的flag是从其工作内存取,while语句调用太频繁意味着访问flag太频繁,导致主内存不会及时刷新工作内存的flag值(详细可了解JMM(Java memory Model)),所以一直会访问到flag为false,当加入System.out.println()后,访问flag就会有明显间隔,主内存就会有空去刷新到MyThread线程的工作内存,这样才能正常退出while循环
2
volatile无法解决非原子性操作问题–synchronized
@Data
public class 多线程问题 implements Runnable {
public static int num = 0;
private synchronized void add() {
for (int i = 0; i < 500; i++) {
num++;
}
}
// public volatile static int num = 0;
// private void add() {
// for (int i = 0; i < 500; i++) {
// num++;
// }
// }
@Override
public void run() {
add();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
多线程问题 pro = new 多线程问题();
Thread thread = new Thread(pro);
thread.setName("第一个线程");
Thread thread1 = new Thread(pro);
thread1.setName("第二个线程");
thread1.start();
thread.start();
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(num);
}
}
}
出现问题的现象是,计算结果无法保证,计算错误; i++ 非原子操作
所以需要用类似 synchronized 的锁,解决这类问题
总结
类似于 synchronized 的属于悲观锁,相对性的就是乐观锁;
乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。
synchronized的不足之处
- 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
- synchronized无法知道线程有没有成功获取到锁
- 使用synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
因此就有了juc 的出现~~
本文转自 jimolvxing.blog.csdn.net/article/det…,如有侵权,请联系删除。