1.简介
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行
synchronized主要有三种使用形式
- 修饰普通同步方法
- 锁对象就是当前实例对象
- 修饰静态同步方法
- 锁对象就是当前类的Class字节码对象
- 修饰同步代码块
- 锁对象就是synchronized括号里面配置的对象,可以是某个对象,也可以是某个类的.class对象
2.synchronized的特性
1.原子性
原子性是指在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行
2.可见性
可见性是指一个线程对共享变量进行了修改,另外一个线程可以立即读取到修改后的最新值
synchronized可见性是通过内存屏障来实现的,按可见性划分,内存屏障分为
- Load屏障
- 执行refresh,从其他处理器的高速缓冲、主内存,加载数据到自己的高速缓冲,保证数据时最新的
- Store屏障
- 执行flush操作,将自己处理器更新的变量值刷新到高速缓存、主内存去
- 获取锁时,会清空当前线程工作内存中的共享变量的副本,重新从主内存中获取变量最新的值
- 释放锁时,会将工作内存的值重新刷新回主内存
int a = 0;
synchronized(this){ //monitorenter
// Load内存屏障
int b = a; //读,通过Load内存屏障,强制执行refresh,保证读取到最新的
a = 10; //写,释放锁时会通过Store,强制执行flush,将值刷新到高速缓存或主内存
} //monitorexit
//Store内存屏障
3.有序性
有序性是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化(重排序),会导致程序最终的执行不一定就是我们编写代码时的顺序
例如,instance = new Singleton(); 实例化对象的语句分为三步
- 分配对象的内存空间
- 初始化对象
- 设置实例对象指向刚分配的内存地址
上述第二步需要依赖第一步,但是第三步不需要依赖第二步,所以顺序可能是 1 -> 2 -> 3,1 -> 3 -> 2,当执行顺序为 1 -> 3 -> 2 时,可能实例独显该没有初始化好,我们拿到使用的时候可能会报错
synchronized的有序性是靠内存屏障实现的
按照有序性,内存屏障可分为
- Acquire屏障
- Load屏障之后,加一个Acquire屏障。它会禁止同步代码块内的读操作,和外面的读写操作发生指令重排
- Release屏障
- 静止同步代码块内的写操作,和外面的读写发生指令重排
在monitorenter指令和Load屏障之后会加一个Acquire屏障,这个屏障的作用是禁止同步代码块里面的读操作和外面的写操作发生指令重排,在monitorexit指令前会加一个Release屏障,也是禁止同步代码块内的写操作和外面的读写操作发生指令重排
int a = 0;
synchronized(this){ //monitorenter
//Load屏障,强制执行refresh,保证读取到外部变量的最新值
// Acquire屏障,静止代码块内的读操作与外面的读写操作发生指令重排
int b = a;
a = 10;
// Release屏障,静止代码块内部的写操作与外面的读写操作发生指令重排
} //monitorexit
//Store,强制执行flush,将最新值刷新到高速缓冲,主内存
4.可重入特性
可重入是指一个线程可以多次执行synchronized,重复获取同一把锁
如下例子
public class Main {
static Object lock = new Object();
public static void main(String[] args) throws Exception {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + ", 第一次获得锁资源....");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + ", 第二次获得锁资源....");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + ", 第三次获得锁资源....");
}
}
}
}
}
运行输出:
main, 第一次获得锁资源....
main, 第二次获得锁资源....
main, 第三次获得锁资源....
3.synchronized的使用通过反汇编分析其原理
1.修饰代码块
public class Main {
static Object lock = new Object();
public static void main(String[] args) {
Integer a = 0;
synchronized(lock){
Integer b = a;
a = 10;
}
System.out.println(a);
}
}
javac Main.java
javap -p -v Main.class
monitorenter 指令
官网对 monitorenter 指令的介绍,就是说每一个对象都会和一个监视对象 monitor 关联,监视器被占用时会被锁住,其他线程来无法获取该 monitor(监视器)。当JVM执行到某个线程的某个方法内部的 monitorenter 时,它会尝试去获取当前对象的 monitor 的所有权,大体如下
- 若 monitor 的进入数为 0,线程可以进入 monitor 并将 monitor的进入设置为1,当前线程成为 monitor的 owner(拥有这把锁的线程)
- 若线程拥有 monitor 的所有权,运行它重入 monitor ,则进入 monitor 的进入数加1(记录线程拥有锁的次数)
- 若其他线程占用了 monitor 的所有权,那么当前尝试获取 monitor 的线程将会被堵塞,直到 monitor 的进入数变为 0 ,才能重新尝试获取 monitor 的所有权
monitorexit 指令
官网对 monitorexit 指令的介绍,就是说执行 monitorexit 指令的线程一定是拥有对象 monitor 的所有权的线程;执行monitorexit时会将 monitor 的进入数减1,当monitor的进入数为0时,当前线程退出
为什么字节码中存在两个 monitorexit指令?
其实第二个是monitorexit指令模式程序发生异常的时候会用到,也就说明了synchronized在发生异常时会自动释放锁
ObjectMonitor 对象监视器结构如下
ObjectMonitor() {
_header = NULL; //锁对象的原始对象头
_count = 0; //抢占当前锁的线程数量
_waiters = 0, //调用wait方法后等待的线程数量
_recursions = 0; //记录锁重入次数
_object = NULL;
_owner = NULL; //指向持有ObjectMonitor的线程
_WaitSet = NULL; //处于wait状态的线程队列,等待被唤醒
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //等待锁的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
2.修饰普通方法
public class Main {
public static void main(String[] args) {}
public synchronized void test(){
System.out.println(Thread.currentThread().getName()+", 获取到了锁...");
}
}
javac Main.java
javap -p -v Main.class
如上图,我们可以看到同步方法在反汇编后,不再是通过插入 monitorentry 和 monitorexit 指令实现了,而是会增加 ACC_SYNCHRONIZED 标识隐式实现的,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标识被设置,那么线程在执行方法前会先去获取对象的 monitor 对象,如果获取成功则执行方法代码,执行完毕后释放 monitor 对象,如果 monitor 对象已经被其他线程获取了,那么当前线程会被堵塞
3.修饰静态方法
public class Main {
public static void main(String[] args) {}
public static synchronized void test(){
System.out.println(Thread.currentThread().getName()+", 获取到了锁...");
}
}
javac Main.java
javap -p -v Main.class
4.synchronized锁对象存在哪里?
存在锁对象的对象头中的MarkWord中,如下图
5.synchronized与Lock的区别?
区别 | synchronized | Lock |
1 | 关键字 | 接口 |
2 | 自动释放锁 | 必须手动调用unlock()方法释放锁 |
3 | 不知道线程是否拿到锁 | 可以知道线程是否拿到锁 |
4 | 能锁住方法和代码块 | 只能锁住代码块 |
5 | 读,写操作都堵塞 | 可以使用读锁,提高线程读效率 |
6 | 非公平锁 | 通过构造方法可以指定是公平锁/非公平锁 |
6.总结
- synchronized修饰代码块是,通过在生成的字节码指令中插入monitorenter和monitorexit指令来完成对 对象监视器的获取和释放
- synchronized修饰普通方法和静态方法的时候,通过在字节码中的方法头信息中添加ACC_SYNCHRONIZED标识,线程在执行方法前会先获取对象的 对象监视器(monitor)如果获取成功则执行方法代码,执行完毕后释放 monitor对象
- synchronized的使用
- 修饰代码块,锁对象就是代码块中的对象
- 修饰普通方法,锁对象就是当前类的实例对象
- 修饰静态代码块,锁对象就是当前类的class字节码对象(类对象)
- 使用synchronized修饰实例对象时,如果一个线程正在访问实例对象中的一个synchronized方法,其他线程不仅不能访问该synchronized方法,该对象中的其他synchronized方法也不能访问,因为一个对象只有一个监视器对象,但是其他线程可以访问该对象中非synchronized方法
- 线程A访问实例的非static synchronized方法时,线程B也可以访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,后者获取的是类对象的监视器锁,两者不存在互斥