一 、基本使用
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:
1、原子性:确保线程互斥的访问同步代码;
2、可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
3、有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。
注意,synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。其可重入最大的作用是避免死锁,如:
子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
Synchronized总共有三种用法:
1、当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
public synchronized void increase() {
i++;
}
2、当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
public static synchronized void increase() {
i++;
}
3、当synchronized作用在某一个对象实例时,即修饰代码块,监视器锁(monitor)便是synchronized后面括号()括起来的对象实例;
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方法对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。
成员锁:锁的对象是变量
public Object synMethod(Object a) {
synchronized(a) {
// 操作
}
}
实例对象锁:this 代表当前实例
synchronized(this) {
for (int j = 0; j < 100; j++) {
i++;
}
}
类锁:当前类的 class 对象锁
synchronized(AccountingSync.class) {
for (int j = 0; j < 100; j++) {
i++;
}
}
二 、同步原理
数据同步需要依赖锁,那锁的同步又依赖谁?
synchronized给出的答案是在软件层面依赖JVM,而j.u.c.Lock给出的答案是在硬件层面依赖特殊的CPU指令。
1、测试类
/**
* 归根结底它上锁的资源只有两类:一个是对象,一个是类。
*/
public class SynchronizedDemo {
public static int i = 0;
SynchronizedDemo test = new SynchronizedDemo();
/**
* 对实例方法加锁,必须获得该类的实例对象的锁才能进入同步块
*/
public synchronized void test1() {
i++;
}
/**
* 对静态方法加锁,必须获得该类Class的锁才能进入同步块
*/
public static synchronized void test2() {
System.out.println("Hello static test2");
}
/**
* 对代码块加锁
*/
public void test3(Object a) {
/**
* 必须获得类锁
*/
synchronized (SynchronizedDemo.class) {
System.out.println("Hello test3-1");
}
/**
* 必须获得对象锁
*/
synchronized (test) {
System.out.println("Hello test3-2");
}
/**
* 必须获得对象锁
*/
synchronized (a) {
System.out.println("Hello test3-3");
}
}
}
2、查看字节码文件
// 编译生成字节码文件
javac SynchronizedDemo.java
// 反编译查看字节码文件
javap -verbose SynchronizedDemo.class
3、通过反编译的结果分析同步块的实现
当synchronized作用在实例方法test1时:
当synchronized作用在静态方法test2时:
flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。
在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
当synchronized修饰代码块时:
由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
a、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
b、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
c、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
4、总结
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
三 、同步概念
1、Java对象头
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
实例数据:存放类的属性数据信息,包括父类的属性信息;
对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述如下: