synchronized简介
并行程序开发的一大关注重点就是线程安全,而synchronized是实现线程安全最简单的一种方法。
关键字synchronized的作用是实现线程间的同步。他的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性。
除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized完全可以替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,所以被synchronized限制的多个线程是串行执行的。
synchronized的使用
关键字synchronized有多种用法,这里做一个简单的整理:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁;
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁;
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
这里假设每一个线程对一个变量i自加10000000次,最终要保证每一个线程都加了这么多次。可以想见,如果没有同步机制,最终i肯定不是我们想要的那个数值。于是我们可以使用synchronized来进行同步,这里synchronized的加锁方法有很多,这就是上述的synchronized的几种用法。
1、作用于对象
/**
* Created by makersy on 2019
*/
public class SynchronizedTest implements Runnable{
static int i = 0;
static SynchronizedTest instance = new SynchronizedTest();
@Override
public void run() {
for (int j = 0; j < 10000000; ++j) {
synchronized (instance) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
这里synchronized作用的是instance这个对象,因此,每次当线程进入synchronized包裹的代码段,就都会要求请求instance实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就要继续等待。这样就保证了每次只能有一个线程执行i++操作。
2、作用于实例方法
/**
* Created by makersy on 2019
*/
public class SynchronizedTest implements Runnable{
static int i = 0;
static SynchronizedTest instance = new SynchronizedTest();
public synchronized void increace() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; ++j) {
increace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
用一个increase方法实现i++,synchronized直接作用于这个实例方法。也就是说在进入increase方法前,线程必须获得当前对象实例的锁。在此例中就是instance对象。要注意的是,两个线程传入的Runnable对象必须是同一个,否则两个线程获取的就不是同一把锁,因为这个锁是作用于实例的。
这里假如传入两个不同的 SynchronizedTest 对象,那么结果就会出现错误。仅仅修改main方法中2个Thread的传入参数,如下:
Thread t1 = new Thread(new SynchronizedTest());
Thread t2 = new Thread(new SynchronizedTest());
结果就不对了:12491868
。
如果要求传入实例不同,但是获得锁一样,那么就可以使用第三种方法。
3、作用于静态方法
/**
* Created by makersy on 2019
*/
public class SynchronizedTest implements Runnable{
static int i = 0;
static SynchronizedTest instance = new SynchronizedTest();
private static synchronized void increace() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; ++j) {
increace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SynchronizedTest());
Thread t2 = new Thread(new SynchronizedTest());
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
相比错误的实现,只是把increase方法改为了静态方法,这是利用了静态方法的特点,无论实例有多少个,静态方法只有一个,线程请求的还是当前类的锁,而不是当前实例的锁,因此,线程间还是可以正确同步。
synchronized底层实现原理
这里首先要了解在JVM堆内存中对象的布局是怎么样的,如下图:
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
对象头
重点要了解的是对象头,对象头的结构有两种情况:
- 数组对象,虚拟机使用3个字宽存储对象头。
- 非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32位。
具体结构是:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64 bit | Mark Word | 默认存储对象的hashCode,分带年龄,锁类型,锁标志位等信息 |
32/64 bit | Class Metadata Address | 类型指针,指向对象的类元数据,JVM通过这个指针确定该对象是那个类的数据 |
32/64 bit | Array Length | (数组对象)数组长度 |
其中,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Mark Word内部结构如下所示:
Monitor
属于一种同步机制,通常被视为一个对象,所有Java对象都可以成为monitor,每一个Java对象一旦生成就会自带一把锁,这把锁被称为Monitor锁或内部锁。
任何一个对象都有一个monitor与之关联,当一个monitor被某个线程持有之后,该对象将处于锁定状态。同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
代码块同步是使用monitorenter和monitorexit指令实现。monitorenter和monitorexit指令是在编译后插入到同步代码块开始和结束的的位置。
monitorenter :每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:执行monitorexit的线程必须是对象所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
在HotSpotJVM实现中,锁有个专门的名字:对象监视器。
synchronized锁优化
在JDK 1.6之前,synchronized被认为是重量级的,由于它的易用性导致了synchronized的滥用,这也是它常被诟病的原因。
从jdk1.6之后对synchronized进行了很多优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
自旋锁:通过让线程执行几次忙循环等待锁的释放,不让出CPU。
自适应自旋锁:自选次数不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除:更彻底的锁优化,通过逃逸分析,若变量不会逃出某个作用域,就将该作用域中的无用锁去除
锁粗化:遇到连续的对同一个锁加锁解锁,扩大加锁范围,避免反复加锁解锁
偏向锁:减少同一线程获取锁的代价。若一个线程获得锁,那么锁进入偏向模式,此时Mark Word结构也变为偏向锁结构,下次获取锁只需要检查Mark Word的锁标记位为偏向锁以及当前线程id为Mark Word的Thread ID即可。不适合锁竞争比较激烈的多线程场合。
轻量级锁:适合线程交替执行同步块。若有同一时间访问同一锁的情况,且自旋了一定次数还没获得锁,就会导致轻量级锁膨胀为重量级锁。
synchronized四种状态
锁膨胀方向:无锁->偏向锁->轻量级锁->重量级锁