并发编程中用到最多的关键字就是synchronized。下面来探究一下synchronized:
- synchronized如何使用?
- synchronized是如何实现同步加锁的,原理是什么?
- synchronized解决了并发编程的哪些问题?
1 synchronized使用
1.1 线程安全问题
并发编程中,当多个线程同时访问同一个资源的时候,就会引发线程安全问题。
由于每个线程执行的过程是不可控的,所以很有可能导致最终的结果和实际期望的结果违背或者直接导致程序出错。
例子
public class VolatileTest{
public int inc = 0;
public void increase(){
inc++;
}
public static void main(String[] args){
final VolatileTest test = new VolatileTest();
for(int i = 0; i < 10; i++){
new Thread(){
public void run(){
for(int j = 0; j < 1000 ; j++){
test.increase();
}
}
}.start();
}
while(Thread.activeCount()>1){
Thread.yield();
System.out.println(test.inc);
}
}
}
目的:test.inc = 10000;
结果: 多次执行得到的结果都小于10000;
分析: 多个线程同时读到inc的值,并对其进行加1的操作, 导致多次操作只对inc的值只增加了1。
基本上所有的并发模式在解决线程安全问题的时候,都会采用对临界资源进行序列化访问的方式,简单来说就是,在同
一时刻,只能有一个线程访问该临界资源,实现同步互斥访问。通常来说,就是在临界资源代码前进行加锁的操作,必须先获得锁,才能访问,访问完毕后释放锁,其他线程可以访问。
在java中,提供了两种方式来实现同步互斥访问:synchronized和lock
1.2 synchronized用法
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
例子
public class MyClass{
int count;
// 1.实例方法
public synchronized void add(int value){
count += value;
}
// 2.实例方法中的同步块 (等价于1)
public void add(int value){
synchronized(this){
count += value;
}
}
// 3.静态方法
public static synchronized void add(intvalue){
count += value;
}
// 4.静态方法中的同步块 (等价于3)
public static void add(int value){
synchronized(MyClass.class){
count += value;
}
}
}
2. 原理探究
如下代码,利用javap工具查看生成的class文件信息来分析Synchronized的实现。 代码:
public class synchronized Test {
// 同步代码块
public void doSth1(){
synchronized (synchronizedTest.class){
System.out.println("HelloWorld");
}
}
// 同步方法
public synchronized void doSth2(){
System.out.println("HelloWorld");
}
}
使用javap对class文件进行反编译后结果:
javap命令:
D:\install\java\jdk8\bin\javap.exe -v .\synchronizedTest.class

从反编译后的结果中可以看到:对于同步方法,JVM采用ACC_synchronized标记符来实现同步。对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。
同步代码块
JVM采用monitorenter、monitorexit两个指令来实现同步。 查询JVM规范The Java® Virtual Machine Specification[1]中关于monitorenter和monitorexit的介绍:

大致内容如下:
- 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。
- 每个对象维护着一个记录着被锁次数的计数器。
- 未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。
- 当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
同步方法
JVM采用ACC_synchronized标记符来实现同步。 查询JVM规范The Java® Virtual Machine Specification[2]中关于方法级同步的介绍:

-
方法级的同步是隐式的。同步方法的常量池中会有一个ACC_synchronized标志。
-
当某个线程要访问某个方法的时候,会检查是否有ACC_synchronized,如果有设置,则需要先获得监视器锁(monitor),然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
-
值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
3 Monitor
无论是同步方法还是同步代码块都是基于监视器Monitor实现的
Monitor是什么
所有的java对象是天生的Monitor,每一个Java对象都有称为Monitor的潜质,因为在Java的设计中,每一个对象天生就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
每个对象都存在着一个Monitor与之关联,对象与其Monitor之间的关系有存在多种实现方式,如Monitor可以与对象一起创建销毁。
Moniter如何实现线程的同步?
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)。
ObjectMonitor中有几个关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
-
线程T等待对象锁:_EntryList中加入T。
-
线程T获取对象锁:_EntryList移除T,_owner置为T,计数器_count加1。
-
线程T中锁对象调用wait():_owner置为null,计数器_count减1,_WaitSet中加入T等待被唤醒。
-
持有对象锁的线程T执行完毕:复位变量的值,以便其他线程进入获取monitor

5 总结
多并发编程中通过同步互斥访问临界资源来解决线程安全问题,Java中常用synchronized标记同步块达到加锁的目的。
synchronized用法有两种,修饰方法和修饰同步代码块。
synchronized的实现原理:每一个Java对象都会关联一个Monitor,通过Monitor对线程的操作实现synchronized对象锁。
并发编程中synchronized可以保证原子性、可见性、有序性。
参考资料
[1]The Java® Virtual Machine Specification: docs.oracle.com/javase/spec…
[2]The Java® Virtual Machine Specification: docs.oracle.com/javase/spec…
转载自 《Java进阶架构师》 公众号