volatile关键字:
使用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值
使用场景:
场景问题:统计1秒内count++的次数
给定一个线程专门进行count++操作
给定另一个线程睡眠1秒,通过两个线程共享变量来完成count++的暂停操作
private static boolean flag = true;
public static void main(String[] args) throws IOException {
Thread thread = new Thread(new Runnable() {
Integer count=0;
@Override
public void run() {
while (flag) {//标志位置为false,++操作结束
count++;
}
System.out.println("计数线程结束啦。。。");
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改标志位
flag = false;
System.out.println("修改标志位成功");
}
});
thread.start();
thread1.start();
}
在当前的代码执行过程中,一个线程修改了标志位,另一个线程没有感知到修改而程序会继续执行,
失败原因:
在JVM运行的时候,在两个线程的共享变量count运行时,每一个线程会获取当前变量的副本在自己的私有的内存区域即虚拟机栈中,当thread1线程在堆变量做修改时,在thread1的本地内存中赋值的最新值没有及时store到主内存,那么Thread线程中也就不能读取到最新的flag的变化,所有thread线程会继续执行
在flag变量上添加volatile修饰之后,thread1线程的修改操作能够立即引起Thread线程停止循环
volatile特征:
禁止指令重排序
volatile修饰的变量的的操作顺序与程序代码中的执行顺序一致
普通的变量仅会保证方法执行的最终结果获取正确,不能保证变量的操作顺序和程序代码中的执行顺序一致
保证线程间可见性
保证变量对所有的线程的可见性,“可见性”是指当一个线程修改这个变量的值,新值对于其他线程可以立即感知到
注意:volatile修饰变量可以保证有序性和可见性,不能保证原子性,也不能保证线程安全
Volatile工作原理
“观察加入volatile关键字和没有加volatile关键字时所生成的汇编代码发现,加入了volatile关键字时,会都出一个Lock前缀指令 --《深入理解Java虚拟机》
该内存屏障提供3个功能:
1、它确保指令重排时不会把后面的指令重排到屏障之前,同理,也不会把前面的指定排到屏障的后面
2、它会强制将对缓存的修改操作立即写回主内存
3、如果是写操作,它会导致其他CPU上对应的缓存行无效(缓存一致性协议)
如何保证可见性性?
变量被volatile关键字修饰
当一个共享变量被volatile修饰时,会保证修改时的值立即更新回主内存,当其他的线程读取该值时,也不会直接读取工作内存中的值,而是直接去主内存中读取
被volatile修饰的变量,在每个写操作之前,会加入一条store内存屏障命令,会强制将此变量最新值从工作内存同步到主内存,在每个读操作之前,都会加入一个load内存屏障命令,会强制从主内存中将此变量的最新值加载从主内存加载到工作内存中
变量未被volatile关键字修饰
普通变量不能保证可见性,因为普通共享变量被修改后,写入到工作内存中,什么时候写回主内存是不可知的,当其他线程去读取时,此时无论工作内存还是主内存,还是原来的值,因此无法保证可见性
volatile关键字如何保证有序性原理?
通过内存屏障来解决多线程下的有序性问题
编译器在生成字节码文件是,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
在每个volatile读操作的前面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
写操作之前后插入内存屏障后生成指令序列的示意图
读操作之后插入内存屏障后生成指令序列的示意图:
volatile应用场景
Boolean的共享状态标志位
单例模式下双重检验锁
注意:
volatile对于基本数据类型才用
对于对象来说,是没有作用的,volatile只能保证对象引用的可见性,对于对象内部的字段修改,他无法保证可见
Synchronized关键字
Synchronized关键字,也提供了线程同步的方式Synchronized的使用Synchronized关键字可以修饰方法或者是同步代码块,确保在多个线程中的同一个时刻,只能有一个线程处于方法或者同步代码块中,它保证了线程对变量访问的可见性和排他性
作用:
Synchronized加在普通方法上:Synchronized加在普通方法上,锁锁的是当前对象实例
//修饰普通方法
public synchronized void test1(){
//sometiong
}
Synchronized加在静态方法上:Synchronized加在静态方法上,锁住的是当前的class实例,因为class数据放在方法区中,相当于该类的全局锁
//修饰静态方法
public synchronized static void test2(){
//sometiong
}
Synchronized加在代码块上:Synchronized如果锁的是o实例,锁住的是代码块,该中加锁粒度会更小一些
//加在代码块上
public void test3() {
Object o = new Object();
synchronized (o) {
//业务逻辑
}
}
Synchronized的特征
Synchronized修饰的方法或者代码块,在同一时刻JVM只允许一个线程进入执行。Synchronized通过锁机制达到只允许一个线程在统一时刻进入执行的效果
在并发编程当中,Synchronized可以做到线程并发的原子性、可见性、有序性
Synchronized的原理
Synchronized的是如何做到线程安全的呢?研究其修饰的方法或者是代码块
通过javac将java的源代码编译成class字节码文件
通过javap 可以查看字节码文件
不管是同步代码块还是同步方法,在底层实现上同步代码块使用到monitorenter和monitorexit指令,同步方法是通过修饰符ACC_SYNCHRONIZED来完成的。
无论哪一种形式,本质上是获取一个对象的监视器(monitor)进行获取,而获取监视器是一个排他的过程。也就是说同一时刻只能有一个线程来获取到Synchronized所保护的对象的监视器
Synchronized允许任何的一个对象作为同步的内存够,任意的一个对象有拥有自己的监视器,当对象有同步块或者同步方法调用时,执行的方法的线程必须先获取到该对象的监视器才能进入同步块或同步方法,而没有获取到监视器的线程将会被阻塞在同步代码块或者是同步方法中,进入到则塞状态
关于monitor对象的立即
monitor对象的实现细节上,记录在对象的头部mark Word区域,关于对象的内存区域细节下
在对象的对象头中,可以看到,当是Synchronized修饰时,早期就直接是重量级锁,重量级锁会有一个指针指向Monitor对象
Monitor类的构成。
Monitor是基于C++的ObjectMonitor实现的,主要的成员
owner:执行持有objectMonitor对象的线程
WaitSet:存放处于wait状态的线程队列,即调用wait方法的线程
count:是waitset和entrylist中的节点数之和
EntrySet:存放处于等待锁block状态的线程队列
Synchronize的早期版本是一个重量级锁,其加锁释放锁需要更底层的操作系统来支持,这个会对系统西能有比较大影响,在后续版本做了优化,提供了轻量级锁,偏向锁等锁优化方案