前言:
synchronized的简介
Synchronized的用法就是为了避免因资源抢占导致的数据错乱,从而让线程进行同步,保证同步内的对象只有一个线程来执行值更改使用操作,是并发控制中必不可少的部分,懂的都懂,今天来深扒一下它
synchronized的特性
原子性、指一个操作或多个操作,要么全部执行并且执行的过程并不会被任何因素打断,要么就都不执行,比如
int i=1;这个操作就是原子性要么执行要么不执行,而i++就不是原子性的,因为包含了读取、计算、赋值几步,原值可能还没完成时就已经被赋值了,而原子性保证了执行过程不会被中断
synchronized与volatile的最大区别就是原子性,volatile不具备原子性
可见性、指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的,比如
int i =1;这个值如果是全局变量,在synchronized内执行变化它时,其他线程也可以读取到当前i的值
有序性、程序执行的顺序都是按照代码先后顺序执行,由于Java允许编译器和处理器对指令进行重排,但他并不影响单线程的顺序,而是影响了多线程的并发执行顺序性,synchronized则保证了同步代码块内是有顺序的
可重入性、就是拥有了这个锁还能重复申请
一、synchronized的使用
1.修饰实例方法
锁的是当前实例对象,进入同步代码获得当前实例
public synchronized void add(){...};
反编译时,add方法的flags多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。
2.修饰静态方法
锁的是当前类,进入同步代码前获取当前类对象
public static synchronized void add(){...};
3.同步代码块
锁括号内的对象或当前类
public class Test{
private Test instance = new Test();
synchronized(instance); //锁对象
synchronized(Test.class){...} //锁类
}
反编译时,同步代码块是由monitorenter指令进入,然后monitorexit释放锁
二、为什么任何对象都可以作为锁
在JVM 中每个对象分为三部分存在:对象头、示例数据、对齐填充
对象头中又有MarkWorld(运行时元数据)
锁状态标志中便记录了加锁的信息
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
偏向锁、轻量级锁、重量级锁的具体使用会在下面具体介绍区别和联系
而查看对象头信息
总结
由于每个对象的Mark Word中都有储存锁的信息,可以说 锁是对象,任何对象都可以作为锁
偏向锁、轻量级锁、重量级锁
偏向锁
只有第一个申请锁的线程会使用锁,有其他线程竞争就膨胀为轻量级锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的所记录里存储锁偏向的线程ID,以后该线程进入和退出同步块不需要CAS(Compare and Swap,比较并替换)操作来争夺锁资源。对一个线程的偏向,如果有其他线程竞争才去释放(再替换线程ID即可),当新线程发起替换对象头中的线程ID为自身的CAS请求时,回去判断拥有此偏向锁的线程是否还活着,如果不活着,则置位无锁状态,如果或者则挂起线程,并将只想当前线程的锁记录地址放入头对象,膨胀成轻量级锁,然后恢复持有锁的线程
ps:当前线程挂起再恢复的过程中并没有发声锁的转移,只是“将头对象中的线程ID变更为只想锁记录地址的指针”,偏向锁是在单线程执行代码块时使用的机制,如果多线程并发时(线程A并未执行完同步代码块,B线程发起了锁的申请),则一定会转化为轻量级锁或重量级锁。
轻量级锁
一个线程自旋等待持有锁线程释放锁,若自旋后还没获得膨胀为重量级锁
每次线程想进入同步代码块的时候,都得通过CAS尝试将对象头中的所指针替换为自身栈中的记录,如果没有成功,则进入而自适应的自旋(动态改变自旋等待次数,有另外一个线程来竞争锁时,线程在原地循环等待而不是阻塞,获得锁的线程释放后就立马获得锁),如果自适应自旋转时还没有获得锁,则膨胀为重量锁
重量级锁
实际的多线程阻塞等待,切换锁的过程
通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
应用场景
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的所竞争
重量级锁:有实际竞争,且所竞争时间长
三、synchronized的底层实现
每个对象都有一个监视器锁monitor 当monitor被占用时就会处于锁定状态,执行monitorenter指令时,尝试获取monitor的所有权,过程如下:
前提:
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程
ObjectMonitor成员变量
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;=============》计数器
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;===========》所持有该对象的线程
_WaitSet = NULL;==============》等待线程集合
_WaitSetLock = 0 ;========》保护等待队列简单的自旋锁
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;====》阻塞上entry上最近科大的县城列表,该列表是由waitNode构成,扮演者线程代理
FreeNext = NULL ;
_EntryList = NULL ;============》入口线程集合
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
ObjectMonitor工作过程
1. 当多一个线程访问同一段同步代码块时,进入_EntryList集合
2.当线程获取到对象的monitor后进入_Owner区域,并把monitor中的owner变量设为当前线程,monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex这时候其他线程无法获取该metux、并将monitor的count+1(此时表示当前线程持有当前对象并加锁)
3.当线程调用wait()方法后,释放当前持有的monitor既释放所持有的mutex,owner变量恢复为null,count-1,同时进入_WaitSet集合等待调用notify/notifyAll被唤醒
4.若当前线程执行完毕,释放当前持有的monitor,owner变量恢复为null
总结:同步锁在这种实现方式中,因为Monitor是依赖于底层的操作系统实现,这样就存在用户态和内核态之间的切换,所以会增加性能的开销
详细流程图
详细代码
头文件:
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.hpp
实现:
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.cpp
代码太长 详细解析可看具体代码解析
四、其他相关线程同步相关
Volatile实现原理
Volatile只能修饰变量,不能修饰方法或代码块
Volatile变量的可见性
Java虚拟机中定义了一种Java内存模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,Java的内存模型目标:定义程序中各个变量的访问规则,既在虚拟机中将变量村存储到内存和从内从中去出变量这样的细节
而对于普通变量,线程A修改值后此时该值在此线程的工作内存,尚未同步到主内存时,若常出现B线程使用此变量,此时拿到的是主内存修改前的值,便发生了可见性不一致的问题
volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
当Volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存中
写操作会导致其他线程中的缓存无效
这样,其他线程使用缓存时,发现本地工作内存此变量无效,就会从主内存获取,这样获取到的就是最新的值,实现了线程的可见性
Volatile变量的有序性
volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序等
JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上保证有序性。如下所示:
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
Volatile的使用
public class TestVolatile {
public static volatile int counter = 1;
public static void main(String[] args){
counter = 2;
System.out.println(counter);
}
}
字节码层面
volatile在字节码层面,就是使用访问标志:ACC_VOLATILE来表示
Volatile与synchronized区别
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的;
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
Lock原理
Lock的使用
由于Lock是一个Java接口,所以需要new它的实现类常用的ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
private Lock lock = new ReentrantLock();
lock.lock();//获取锁
//do something
lock.unlock()//释放锁
lock.trylock()//获取锁 如果锁被占有就放开
而lock的核心类是(AQS)AbstractQueuedSynchronizer 自旋锁
synchronized与Lock的区别
五、各种名称锁总结
详细介绍:https://tech.meituan.com/2018/11/15/java-lock.html
互斥锁/读写锁
互斥锁在Java中的具体实现就是ReentrantLock。
读写锁在Java中的具体实现就是ReadWriteLock。