一、使用背景,为什么要使用synchronize?synchronize能干什么?
1、Java多线程背景
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点:
- 一是存在共享数据(也称临界资源).
- 二是存在多条线程共同操作共享数据。
因此为了解决这个问题,我们可能需要这样一个方案:
当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。
2、对于以上问题,我们Java中的“synchronized”可以做到这一点,为共享数据操作达到原子性、可见性、有序性而生
在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。
3、synchronized的三种应用方式
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
二、synchronized运行原理分析
1、实例分析
编写一个简单的加了synchronized关键字的程序:
public class Test4 {
private static Object LOCK = new Object();
public static int main(String[] args) {
synchronized (LOCK){
System.out.println("Hello World");
}
return 1;
}
}
运行编译后,查看编译的class字节码文件内容如下:
通过上图字节码执行文件的分析,可以看出锁monitorenter和monitorexit来实现的。
monitorenter和monitorexit解释:
每个对象都有一个monitor监视器,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值减1。如果是线程重入,在将值+1,说明monitor对象是支持可重入的
2、对象如何记录锁状态?
锁是被记录在对象头当中
- 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
- 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可
3、对象头加锁分析
重量级锁:
最基础的实现方式,JVM会阻塞未获取到锁的线程,在锁被释放的时候唤醒这些线程。阻塞和唤醒操作是依赖操作系统来完成的,所以需要从用户态切换到内核态,开销很大。并且monitor调用的是操作系统底层的互斥量(mutex),本身也有用户态和内核态的切换,所以JVM引入了自旋的概念,减少上面说的线程切换的成本。
自旋锁:
如果锁被其他线程占用的时间很短,那么其他获取锁的线程只要稍微等一下就好了,没必要进行用户态和内核态之间的切换,等的状态就叫自旋。
自旋会跑一些无用的CPU指令,所以会浪费处理器时间,如果锁被其他线程占用的时间短的话确实是合适的,如果长的话就不如使用直接阻塞了,那么JVM怎么知道锁被占用的时间到底是长还是短呢?
因为JVM不知道锁被占用的时间长短,所以使用的是自适应自旋。就是线程空循环的次数时会动态调整的。
可以看出,自旋会导致不公平锁,不一定等待时间最长的线程会最先获取锁。
jdk1.6后加入轻量级锁:
轻量级锁:
JDK1.6之后加入,它的目的并不是为了替换前面的重量级锁,而是在实际没有锁竞争的情况下,将申请互斥量这步也省掉。锁实现的核心在与对象头(MarkWord)的结构,对象自身会有信息表示所有被锁住并且锁是什么类型。
(1)、轻量级锁分析
如果代码进入同步块时,检测到对象未锁定,即标志位为01。那么当前线程就会在自身栈帧中建立一个区域保存对象的MarkWord信息,再使用CAS的方式让这个区域指向对象的MarkWork区域,这样就算加上锁了。(这样就没有获取系统mutex变量,只是改了个值,但是如果有竞争的话,就要升级成重量级锁,这样反倒变慢了)
加锁前VS 加锁后:
(2)、偏向锁:
比轻量级锁更绝,将同步操作全部省略…设置步骤是和前面的轻量级锁一样的,不同的是标志位设置的是01,即偏向模式。
不同的是同一个线程第二次进来之后,虚拟机不会再进行任何的同步操作,比如Mark Word的update。
如果有其他线程来,偏向模式就结束了,标志位会恢复到未锁定或者偏向锁。所以如果锁总是会被多个线程访问的话,还是禁止掉偏向锁优化比较好。
(4)、锁优化流程如下:
https://www.bilibili.com/video/BV1Y4411a7Ld?from=search&seid=10271254194168906067