Java中的volatile与synchronized

简介

Java中多线程并发编程中synchronized和volatile是对于线程的安全保证的两个重要机制;简单来说synchronized提供了一种独占加锁的机制,使得当前锁住的对象只被一个线程访问;volatile用于保证了线程的在共享内存中变量对于所有线程可见与禁止cpu指令重排作用;从而保证部分状态下的线程安全;
线程安全的三大特性:
原子性:一组语句作为一个不可分割的单元被执行。
可见性:修改一个共享变量时,其他线程可以读到这个修改的值;
有序性:对于多个线程执行来说,他们之间必须是相对有序的;

1、synchonized

synchronized是java中用于提供内置锁的一种加锁机制;其修饰的代码块一般称为:同步代码块。
同门代码块分为两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
这个关键字可以修饰变量、方法或是代码块
对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized加的锁具有可重入的特性;(当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。)

synchronized(lock){

//访问或修改由锁保护的共享状态

}

1.1、执行原理

这些锁是什么?
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

首先代码对象经JVM编译后后加入对象头,而且如果是Synchronized修饰的会加入monitorexit指令和monotorentor指令用于控制对象锁。即编译到moniterentor时会尝试获取monitor所有权;获取到修改指针;JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽
(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。
对象头里的内容:
1)MarkWord:用于存储对象的hashcode、分代年龄和锁信息(据不同类型锁包括锁状态、锁指针、标识位等)
2)Class Metadata:存储对象类型的指针
3)ArrayLength:如果是数组有这个属性;

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结
束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有
一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter
指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

Java的内置锁相当于一种互斥锁,即只能有一个线程能够持有这种锁;通过这个方式就保证了原子性;
Java平台中的锁在获取时会进行缓存的刷新,而在释放锁时会进行缓存的冲刷。从而保证线程执行的可见性。
锁它使得写线程在临界区的执行操作在读线程的临界区看起来像是完全有序的。

1.2、可重入性实现

可重入性的实现:为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。通过这种方式保证了可重入性;优点是其可以避免一些情况下的死锁,比如需要递归调用才能执行的方法;

1.3、锁的升级

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁能升级而不能降级;目的是保证执行效率;
1)偏向锁
特定的锁机制,在对象头中多存储偏向的线程id,保证该线程下次获取锁时不需要CAS来加锁和解锁,测一下对象头中的是否有指向该线程的线程id是否存在即可。如果没有则判断它是不是偏向锁,然后CAS设定线程id;

锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当出现有非偏向的线程时就会出现偏向锁撤销;

偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

2)轻量级锁
默认竞争不够强,如果得不到锁就进行一个原地的自选操作,获取到锁再进行执行;当出现在解锁的过程中仍有线程在抢占锁,则升级为重量级锁;

加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

3)重量级锁
当发生竞争时不会采用自旋操作,而是原地阻塞等待锁;当获取得到锁之后再执行;

————————————————————————————————————————

2、volatile

volatile是可以说是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,即共享内存中的值修改对于每一个线程都是可见的,另一个重要的作用是它还可以禁止指令重排;但是其无法保证原子性;

volatile int n;

当且仅当满足以下所有条件时,才应该使用volatile变量:
·对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
·该变量不会与其他状态变量一起纳入不变性条件中。
·在访问变量时不需要加锁。

2.1、实现原理

当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
主要有两点:
1、强制从共享内存中读取数据;将缓存数据回写到内存;
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令于共享变量操作前,将这个变量所在缓存行的数据写回到系统内存。
2、一个线程回写缓存并使其他线程的缓存失效;
将自己的缓存回写后,还需要处理多线程下其他线程的缓存,让这些缓存失效,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。

2.2、volatile与内存屏障

为了实现volatile的内存语义,编译器在生成字节码的时候,JMM采取保守策略会向指令序列中插入内存屏障来禁止特定类型的处理器重排序。在每个volatile写操作前面插入一个StoreStore屏障。在每个volatile写操作后面插入一个StoreLoad屏障。在每个volatile读操作后面插入一个LoadLoad屏障。在每个volatile读操作后面插入一个LoadStore屏障指令通过将其和其后面的指令不放入序列缓存区,而放在一个FIFO的队列中执行。当这一批指令执行完成后在进入乱序执行。

屏障指令通过将其和其后面的指令不放入序列缓存区,而放在一个FIFO的队列中执行。当这一批指令执行完成后在进入乱序执行。
写屏障保证了对该屏障之前对于变量的改动都同步到主存中;
读屏障保证在改屏障之后的读取加载的都是主存中的最新指令;
此外屏障还对读写指令的前后代码重排序加了控制,保证写屏障之前的代码不会排在后面,
而读屏障之后的代码不会排在读指令之前。
————————————————————————————————————————————

synchronized和volatile区别

volatile只能修饰变量,且多线程访问时不会出现阻塞;而synchronized可以修饰变量,代码块,方法;synchronized可以保证原子性,常用于操作同步,volatile常用于保证变量可见性。

おすすめ

転載: blog.csdn.net/qq_44830792/article/details/121337906