一、JMM
我们知道Java是一个跨平台的语言,那么Java是如何屏蔽掉各种操作系统、各种机器对内存访问的差异呢?
在JVM规范中,Java定义了一种内存模型,用来屏蔽掉各种硬件和操作系统的内存访问差异
1.JMM中的三大特性
- 可见性
- 当一个线程修改了某一个共享变量的值,其他线程能够立即知道该值变更。
- 类似于git,在仓库中拷贝下去,自己独立的备份,仓库原来的不变,更新完成后可以提交到仓库(主内存)
-
- 有序性
- 对于一个线程的执行代码,Java规范规定JVM线程内部维持顺序,只要运行结果和顺序化执行的结果相同,那么指令执行的顺序可以和代码顺序不一样
- 原子性
2.happens-before(先行发生原则)
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。
比如
int x = 5; //线程A执行 int y = x; //线程B执行
y是否一定等于5呢?
如果线程A的操作复核happens-before操作,则y为5,反之则不一定。
hanppens-before原则有8条
①. 次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作(强调的是一个线程)
前一个操作的结果可以被后续的操作获取。将白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1
②. 锁定规则
(一个unlock操作先行发生于后面((这里的"后面"是指时间上的先后))对同一个锁的lock操作(上一个线程unlock了,下一个线程才能获取到锁,进行lock))
③. volatile变量规则
(对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面"同样是指时间是的先后)
④. 传递规则
(如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出A先行发生于操作C)
⑤. 线程启动规则(Thread Start Rule)
(Thread对象的start( )方法先行发生于线程的每一个动作)
⑥. 线程中断规则(Thread Interruption Rule)
对线程interrupt( )方法的调用先发生于被中断线程的代码检测到中断事件的发生
可以通过Thread.interrupted( )检测到是否发生中断
⑦. 线程终止规则(Thread Termination Rule)
(线程中的所有操作都先行发生于对此线程的终止检测)
⑧. 对象终结规则(Finalizer Rule)
(对象没有完成初始化之前,是不能调用finalized( )方法的 )
A Happens-before B,则意味着A发生过的事情对B来说是可见的
private int value=0; public void setValue(){ this.value=value; } public int getValue(){ return value; }
如果get、set方法由两个进程进行,为了满足A先于B,要么加锁,要么使用volatile关键字
二、volatile
为了满足JMM内存模型的要求,我们使用了volatile关键字,关键字由两个特点:可见性、有序性。(并没有完全实现JMM的三个要求)
使用volatile修饰变量的时候,
如果写一个volatile变量,JMM会把该线程对应的本地内存中的共享变量立即刷新回到主内存中。
如果读一个volatile变量时,JMM会把该线程对应的工作内存设置为无效,直接从主内存中读取共享变量。
总结为一句话:一句话,volatile修饰的变量在某个工作内存修改后立刻会刷新会主内存,并把其他工作内存的该变量设置为无效。
那么volatile怎么保证呢?====》内存屏障
内存屏障
内存屏障粗分两种:
写屏障
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
读屏障
在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。
细分四种
因此volatile实现了有序性和和可见性
但是,volatile并不适合运算,如i++,因为volatile并没有实现原子性
那么volatile平常适用于哪些场合呢
状态标志,判断业务是否结束
//这个前面讲过 public class UseVolatileDemo { private volatile static boolean flag = true; public static void main(String[] args) { new Thread(() -> { while(flag) { //do something......循环 } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { flag = false; },"t2").start(); } }
开销较低的读,写锁策略
public class UseVolatileDemo { // // 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销 // 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性 public class Counter { private volatile int value; public int getValue() { return value; //利用volatile保证读取操作的可见性 } public synchronized int increment() { return value++; //利用synchronized保证复合操作的原子性 } } }
DCL双锁案例
public class SafeDoubleCheckSingleton { private static SafeDoubleCheckSingleton singleton; //-----这里没加volatile //私有化构造方法 private SafeDoubleCheckSingleton(){ } //双重锁设计 public static SafeDoubleCheckSingleton getInstance(){ if (singleton == null){ //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象 synchronized (SafeDoubleCheckSingleton.class){ if (singleton == null){ //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取 singleton = new SafeDoubleCheckSingleton(); //实例化分为三步 //1.分配对象的内存空间 //2.初始化对象 //3.设置对象指向分配的内存地址 } } } //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象 return singleton; } }
标签:volatile,线程,内存,Volatile,JMM,操作,public From: https://www.cnblogs.com/wintermist/p/17220506.html