《Java并发编程的艺术》——Volatile关键字与Synchronized关键字

Volatile关键字的原理

提到volatile关键字, 就不得不提两个关键词:
JMM内存模型:JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,而每个线程都有一个私有的内部缓存(Local Memory),内部缓存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在内部缓存中进行,而不能直接读写主内存中的变量。
缓存一致性协议:在多线程下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性原理,每个处理器通过嗅探在总线上传播的数据来检查自己缓存里的值是否过期了,如果处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存设置为无效状态,当处理器对这个数据进行修改操作时,就会重新从主内存中读取。

加入volatile关键字时,会多出一个LOCK指令。
LOCK指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

Volatile关键字的内存语义

volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的内部缓存中的共享变量刷新到主内存。
volatile读的内存语义:当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,接着从主内存中读取共享变量。
将这两个语义结合来看的话:就可以实现线程之间的通信啦!
如:在读线程B中读一个volatile变量,那么就意味着在写线程A中写这个volatile变量之前所有可见的共享变量的值都立即为B线程所见。

单例模式下的双重锁为什么要volatile?

​ 双重锁的单例模式:

	public class TestInstance{
		private volatile static TestInstance instance;
		
	    public static TestInstance getInstance(){        //1
	        if(instance == null){                        //2
	            synchronized(TestInstance.class){        //3
	                if(instance == null){                //4
	                    instance = new TestInstance();   //5
	                }
	            }
	        }
	        return instance;                             //6
	}

因为第5步那里:新建一个对象可以分为3步: 1,分配内存 2,调用构造函数初始化对象 3,将对象的指针指向刚分配好的内存地址 。 这里需要volatile关键字去防止这3步进行重排序→变成132;因为如果是132,假如A线程先执行了13操作,还没有进行2操作时,B线程进来了,先判断instance是否为空,由于A线程进行了3操作,所以instance有具体的内存,不为空,B线程直接return了一个未完全初始化的instance。

volatile关键字不适合用于i++操作的原因:

volatile关键字虽然可以保证该变量的可见性,但是并不保证其原子性。即假如有两个线程都对volatile变量进行自增操作,是有可能得到错误结果的。因为自增操作分为三个步骤:①从主内存中读取变量的值 ②寄存器进行+1操作 ③把缓存的值刷新到主内存。假设线程A对读取了i变量的值为10,然后阻塞了,此时B线程过来完成了自增的三步操作,i变为了11,然后A线程由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。两次自增操作后,值等与11,发生错误。

改进措施:在自增这个方法引入synchronized 关键字或者是lock方法,又或者是用Atomic的包。

Synchronized关键字

synchronized的四大特性:

原子性:要么全部执行,要么全部中断。被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。synchronized和volatile,它们俩特性上最大的区别就在于原子性,volatile不具备原子性。(如volatile关键字修饰i,然后进行i++操作,由于该操作不是原子性,就不能保证线程安全)

可见性:而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性

有序性:synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

可重入性:synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

synchronized使用:①在方法上上锁:反编译后字节码中的同步方法的flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,然后隐式调用monitorenter使锁的计数器加1,方法结束后调用monitorexit计数器-1,如果获取失败就阻塞住,直到该锁被释放。

②使用在同步代码块上:同步块是由monitorenter指令进入,然后monitorexit释放锁。

synchronized底层实现原理:实例对象在JVM中分三部分组成:对象头,实例数据,对其填充。其中对象头中主要由Mark Word和Class Metadata Address 组成,其中Mark Word中有记录该对象锁的类型(总共四种类型:无锁状态,偏向锁,轻量级锁,重量级锁),锁ID等。在申请锁、锁升级,等过程中JVM都需要读取对象的Mark Word数据。获取对象锁的过程,其实是获取monitor对象的所有权的过程,每一个对象的锁都绑定了一个monitor对象(随对象一起创建销毁),当一个线程持有该对象锁,则monitor里的计数器+1。当计数器大于0时,其他线程不可进入,阻塞状态。

而在jdk1.6之前,synchronized锁被称为重量级锁。如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,这种状态的转换要耗费很多的处理时间。

锁升级的过程

在这里插入图片描述
Synchronized减重的过程,通常被称为锁膨胀或是锁升级的过程。
主要步骤是:

①先是通过偏向锁来获取锁,解决了虽然有同步但无竞争的场景下锁的消耗。若不是同一个对象,则升级为轻量级锁。
②轻量级锁是JVM将使用CAS操作尝试把该线程的对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,说明线程获取锁成功。若未成功,则进行升级。
③升级为自旋锁,如果达到最大自旋次数了,那么就直接升级为重量级锁,所有未获取锁的线程都阻塞等待。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值