前言
- 在多线程编程中,会出现多个线程同时访问一个共享、可变资源(共享:资源被多个线程同时访问;可变:资源可被多个线程修改)的情况,这个资源我们称为临界资源,这种资源可能是对象、变量、文件等等;
- 由于线程执行是不可控的,所以需要采用同步机制来协助对象可变状态访问,即在同一时刻,只能有一个线程访问临界资源,所以被称作同步互斥访问;
- 在Java中提供了两种方式来实现同步互斥访问:synchronized 和 Lock;
- 同步器的本质就是加锁,其目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源;
- 注意:当多个线程执行同一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具备共享性,不会导致线程安全问题;
Synchronized基本分析
- synchronized内置锁是一种对象锁(锁住的是对象而非引用),作用颗粒度是对象,是一种可重入锁;
- 加锁方式:
- 同步实例方法(普通方法),锁的是当前实例对象
- 同步类方法(静态方法),锁的是当前类对象
- 同步代码块,锁的是括号内的对象
- synchronized是基于JVM内置锁实现的,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁,性能较低。在java 1.5版本后做了大量优化,比如锁粗化、锁消除、偏向锁、轻量锁、适应性自旋等技术来减少锁操作的开销,synchronized的性能基本与lock一致。
- synchronized关键字被编译成字节码后会翻译成monitorenter和monitorexit两条指令分别在同步代码块逻辑的开始和结束位置,如下图:
- 每个同步对象都有一个自己的monitor(监视器锁),加锁过程如下:
什么是Monitor
- 可以把它理解为一个同步工具或机制,通常被描述成一个对象,所有java对象是天生的monitor。因为在java的设计中,每一个java对象一开始就带有一把锁,它就叫monitor,也就是Synchronized的对象锁,MarkWord锁标识位是10,其指针指向的是monitor对象的起始位置;
- 在java虚拟机中,monitor是由ObjectMonitor实现的,其结构如下(位于hotspot虚拟机源码ObjectMonitor.hpp文件, C++ 实现的)
- ObjectMonitor有两个队列,_WaitSet和 _EntryList,用来保存ObjectWaiter对象列表(每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,但多个线程同时访问同步代码块时:
- 先进入_EntryList集合,当线程获取到monitor后,进入_Owner区域并把monitor中owner变量设置为当前线程,同时monitor进入数加1; - 若线程调用wait方法,将释放当前持有的monitor,owner变量恢复null,monitor进入数减1,同时进入waitSet集合中等待被唤醒; - 当前线程执行完,将释放monitor并使monitor进入恢复到0,其他线程可以进入获取monitor;
- 同时,monitor对象存在于每个java对象的对象头markword中,Synchronized锁便是通过这种方式获取锁的,这也是java任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用monitor对象,所以必须在同步代码块中使用
Monitor基本分析
- 任何一个对象都有一个monitor关联,当一个monitor被持有了后,它将处于锁定状态。
- Synchronized在JVM中实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然细节有可能不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
- monitorEnter:当monitor被占用时会处于锁定状态,线程就是执行monitorEnter指令时获取monitor所有权;
- monitorExit:执行monitorExit必须是objectref所对应的monitor的所有者;
- 执行过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,将进入数(_count)设置为1,即为monitor的所有者;
- 如果线程已经占有monitor,只是重新进入,则monitor的进入数加1;
- 如果其他线程已经占有monitor,则该线程进入阻塞状态,知道monitor的进入数count为0,再尝试获取monitor的所有权;
- 执行monitorExit指令时,monitor进入数减1,如果减1后为0,那线程退出monitor,不再是这个monitor的所有者,其他被monitor阻塞的线程可以尝试获取monitor的所有权;
- 看一个同步块的方法和其反编译的结果
public void test1() {
synchronized(this) {
System.out.println("test1");
}
}
- monitorExit指令出现了两次,第一次为同步正常退出释放锁,第二次为异常退出释放锁;
- 再看一个同步方法和其反编译的结果
public synchronized void method() {
System.out.println("Hello World!");
}
- 看上图,方法的同步并没有使用monitorEnter和monitorExit来完成,不过相对于普通方法,其常量池多了ACC_SYNCHRONIZED标识符,JVM就是通过这个标识符实现方法同步的;
- 当方法调用,指令会检查ACC_SYNCHRONIZED访问标志是否被设置,如果被设置了就先获取monitor,获取成功才能执行方法,然后在释放monitor。
- 两种同步方式没有本质的区别,只是同步方法使用了一种隐式的方式来实现,两个指令的执行是JVM通过调用操作系统的互斥源语mutex实现的,被阻塞的线程会被挂起、重新等待调度,会导致“用户态和内核态”之间来回切换,对性能影响较大。
最后
- 虚心学习,共同进步 -_-