从头说起:冯洛伊曼计算机体系的缺陷,CPU 运算器的运算速度远比内存读写速度快,所以增加缓存来提高读写速度,但又产生了一个问题,多线程修改共享变量会不安全,这就是非常著名的缓存一致性问题,于是就有了下面的锁机制(Synchronized & Lock)来解决此问题。
synchronized
sychronized作为很早就出现的锁,在Java中被大量使用,后续版本的优化使其获得了更高的性能。
- synchronized:通过使用synchronized关键字可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
- 工作流程:在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
- 原子性:synchronized 的锁不能被中断,保证原子性。
- 可见性:线程解锁前,必须把共享变量的最新值刷新到主内存中;线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值。
- 分类:悲观锁,不可中断锁,可重入锁,非公平锁
使用方法
概述:总体来说,可以修饰代码块和方法,修饰方法又分是否为静态方法,这三种不同的用法同步锁是不同的。
- 同步代码块:
synchronized
关键字可以用于修饰方法中的某个代码块中,表示只对这个区块的资源实行互斥访问。- 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized(同步锁){
需要同步操作的代码
}
- 同步方法::使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
- 修饰普通方法(实例方法):对于非static方法,同步锁就是this。创建多少个实例对象就有多少把锁,这仍然是不安全的,所以可以用静态方法。
- 修饰静态方法:对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。这时候无论创建多少实例对象,类对象都只有一个。
public [static] synchronized void method(){
可能会产生线程安全问题的代码
}
底层原理
synchronized在对象头标记锁,底层是操作系统来完成加锁的过程的。
- 加锁是如何实现的?:在对象头做标记
- 对象布局:在 JVM 中对象的内存布局包括对象头,实例数据和对其填充三块区域
- 对象头:两部分,记录锁标志位信息,hashcode()与分代年龄的标记字段与类型指针。
- 实例数据:这部分主要是存放类的数据信息,父类的信息。
- 对其填充:由于64位虚拟机要求对象大小必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
- 利用字节码分析底层原理:在使用的过程中我们无法看到的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。可以看到在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,这两个指令分别对应获取锁和释放锁。锁竞争的实质也就是对象头指向的Monitor对象的争夺
- 字节码:普通的高级语言编译器把其翻译成机器语言就可以了。但java文件要到通过编译器编译成java字节码文件(.class文件)因为要保障跨平台性,Java底层加了一个JVM,而我们的java虚拟机执行的就是字节码文件。
- JDK 反编译指令: javap -c -v xxx
- 操作系统层面:监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的。
- 为什么会有两个monitorexit:最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
- synchronized可重入的原理:重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,标识有无锁和锁的状态。
锁升级
-
起因:这是Java虚拟机对synchronized的一种优化,在没有优化以前,sychronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。
-
效率低下的底层原理:因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
- 用户态和内核态:比如Linux系统体系结构中的用户空间和内核,所有的程序都在用户空间运行,进入用户运行状态也就是(用户态),但是很多操作可能涉及内核运行,比我I/O,我们就会进入内核运行状态(内核态)。
-
锁的优化:JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
-
锁升级的原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
-
重量级锁:1.6之前synchronized是重量级锁,用于有实际竞争,且锁竞争时间长的情况。
-
偏向锁:在大多数情况下,锁不仅不存在多线程竞争,偏向锁的核心思想是,如果一个线程获得了锁,当这个线程再次请求锁时,无需获取锁的过程。用于无实际竞争,且将来只有第一个申请锁的线程会使用锁。
-
轻量级锁:对绝大部分的锁,在整个同步周期内都不存在竞争。用于无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
-
自旋:很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁不太值,这时不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
-
自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。自旋锁不会使线程状态发生切换,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
比较
- synchronized 和 Lock 有什么区别:
- Synchronized 是内置的Java关键字,Lock是一个Java类
- Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
- Synchronized 会系自动释放锁,Lock必须要手动释放锁。
- Synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断进行中断,默认非公平
- 到现在Lock锁和Synchronized锁的性能其实差别不是很大,一般用synchronized更方便,各有各的用处,需要使用到Lock显式锁的特性就再用Lock锁
volatile
-
作用:volatile修饰的变量具有可见性与有序性。
- 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
-
原理:
- 可见性: 只要被volatile修饰的变量的赋值一旦变化就会强制将修改的值立即写入主存,并且立即通知到其他线程,如果其他线程的工作内存中存在这个同一个变量拷贝副本,那么其他线程会放弃这个副本中变量的值,重新去主内存中获取
- 有序性:产生了内存屏障,防止指令进行了重排序。
- 内存屏障:它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。
- 指令重排序:编译器和处理器为了优化程序执行的性能而对指令序列进行重排的一中手段。
-
final的对写并发应用:把数据变成不可变的,就不会有共享数据的修改问题了。不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
参考资料
https://blog.csdn.net/javazejian/article/details/72828483
https://www.zhihu.com/question/57794716?sort=created
https://thinkwon.blog.csdn.net/article/details/104863992