多线程与高并发-part3
作者:互联网
volatile
- volatile是Java虚拟机提供的轻量级同步机制
- 特点
- 保证可见性
- JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
- 不保证原子性
- 禁止指令重排
- 保证可见性
JMM
- 就是Java内存模型
- 规定:
- 线程解锁前,必须把共享变量值刷新回主内存
- 线程加锁前,必须把读取主内存的最新值到自己的工作内存
- 加锁和解锁是同一把锁。
- JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。
- 具体过程
数据传输速率:硬盘<内存<<cache<CPU
Volatile保证可见性验证
/**
* 假设是主物理内存
*/
class MyData {
int number = 0;
public void addTo60() {
this.number = 60;
}
}
/**
* 验证volatile的可见性
* 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
*/
public class VolatileDemo {
public static void main(String args []) {
// 资源类
MyData myData = new MyData();
// AAA线程 实现了Runnable接口的,lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 线程睡眠3秒,假设在进行运算
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
myData.addTo60();
// 输出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
while(myData.number == 0) {
// main线程就一直在这里等待循环,直到number的值不等于零
}
// 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
// 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
/**
* 最后输出结果:
* AAA come in
* AAA update number value:60
* 最后线程没有停止,并行没有输出 mission is over 这句话,说明没有用volatile修饰的变量,是没有可见性
*/
}
}
- 结果:线程未停止,并行没有输出该输出的话。
- 修改:给number 加上Volatile关键字。最后主线程执行完毕,该输出的也输出了。
Volatile不保证原子性
- 原子性:要么同时成功,要么同时失败
测试
/**
* Volatile Java虚拟机提供的轻量级同步机制
*
* 可见性(及时通知)
* 不保证原子性
* 禁止指令重排
*/
import java.util.concurrent.TimeUnit;
/**
* 假设是主物理内存
*/
class MyData {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
/**
* 注意,此时number 前面是加了volatile修饰
*/
public void addPlusPlus() {
number ++;
}
}
/**
* 验证volatile的可见性
* 1、 假设int number = 0, number变量之前没有添加volatile关键字修饰
* 2、添加了volatile,可以解决可见性问题
*
* 验证volatile不保证原子性
* 1、原子性指的是什么意思?
*/
public class VolatileDemo {
public static void main(String args []) {
MyData myData = new MyData();
// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
}
}
- 多次测试之后,值都小于20 * 1000
原因
- 一个n++指令在字节码文件中的指令被拆分成三个
- 执行
getfield
从主内存拿到原始n - 执行
iadd
进行加1操作 - 执行
putfileld
把累加后的值写回主内存
假设三个线程同时拿到主内存的值,然后三个线程分别在自己的工作内存中进行+1操作,但是并发进行的idd,又因为写操作只能有一个,假设此时是1号线程正在写,写完了,
volatile的可见性原因,告知其他线程,主内存值已经被修改,但太快了,其他两个线程陆续执行了idd命令进行写操作,这就造成了其它线程没有接收到主内存的n改变,从而覆盖了原来的值,出现写丢失
- 执行
- 解决:
- 方法前面加synchronized关键字
- synchronized关键字是一个同量级同步机制,并发性降低,考虑使用JUC下面的原子包装类,使用AtomicInteger代替
/**
* 创建一个原子Integer包装类,默认为0
*/
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
// 相当于 atomicInter ++
atomicInteger.getAndIncrement();
}
volatile禁止指令重排
- 计算机在执行程序的时候,为了提高性能,编译器通常会对指令进行重排。
- 处理器在进行重排序的时候,必须要考虑指令之间的数据依赖性(要先有数据的声明才能进行值操作)
- 使用volatile进行读写的时候加入了屏障,防止出现指令重排
单例模式
- 这是volatile的典型应用
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
- 结果:并没有保证只有一个实例的创建,是被一些线程创建了。
- 思考:多环境下如何保证单例呢?
- 加synchronized关键字,把获取实例的方法变为同步方法。
public synchronized static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
缺点:synchronized属于重量级同步机制,降低了并发性
2. 使用双端检锁机制
public static SingletonDemo getInstance() {
if(instance == null) {
// 同步代码段的时候,进行检测
synchronized (SingletonDemo.class) {
if(instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
- 问题:因为指令重排的原因,在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。在初始化的时候,可以分为三步
1. memory = allocate(); // 1、分配对象内存空间
2. instance(memory); // 2、初始化对象
3. instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
步骤2和步骤3不存在数据依赖关系,所以可以进行重排的。
1. memory = allocate(); // 1、分配对象内存空间
2. instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
3. instance(memory); // 2、初始化对象
造成问题:执行到步骤2的时候,视图获取instance,会得到null,就因为此时对象的初始化还没有完成,而是在第三步才完成的,这样就造成了线程安全问题。
最终版
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
// a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
synchronized (SingletonDemo.class) //b
{
//c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
if(instance == null) {
// d 此时才开始初始化
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
// // 这里的 == 是比较内存地址
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
标签:SingletonDemo,并发,number,instance,part3,线程,volatile,多线程,public 来源: https://www.cnblogs.com/yunge-thinking/p/15316263.html