JAVA:synchronized总结
用法
修饰实例方法
将整个实例方法标记为同步方法,锁对象是当前实例。
修饰代码块
可以指定任意对象作为锁,使代码块在获取指定对象的锁后执行
修饰静态方法
将静态方法标记为同步方法,锁对象为类的 class 对象
特性
原子性
保证操作的原子性,不可被中断
可见性
确保线程对共享变量的修改能及时被其他线程看到
有序性
禁止指令重排序,保证代码块内操作按照顺序执行
锁类型与升级过程
乐观锁VS悲观锁
悲观锁
假设总是出现最坏的情况,在获取数据总担心其他线程修改数据,因此,在每次获取数据时都会 加锁,这样其他线程再想拿到数据只会阻塞等待,直到这个线程拿到锁。
乐观锁
假设拿数据一般不会发生并发冲突问题,直到在进行数据更新的时候才再考虑是否有并发冲突的问题。
synchronized 初始使用时采取乐观锁策略,如果检测到锁竞争比较频繁的时候会自动切换悲观锁策略
自旋锁VS挂起等待锁
挂起等待锁
线程在获取锁失败后就会进入阻塞状态,直到持有锁的线程释放锁以后才能获取到锁继续执行。
自旋锁
线程在获取锁失败以后就会进入阻塞状态,放弃CPU,需要很久才能被再次调度。
其实大部分情况下获取锁失败以后,没多久再进行获取锁操作就能获取成功;因此就出现了自旋锁。
自旋锁就是相当于循环操作,一直尝试获取锁,直到获取到为止。
自旋锁是一种典型的 轻量级锁
优点:不涉及到线程阻塞与调度,没有放弃CPU,一旦锁被释放立即就能获取到锁。
缺点:如果其他线程持有锁的时间很久就会持续消耗CPU资源
重量级锁VS轻量级锁
在上文我们了解到锁有原子性的特征,锁的原子性追根溯源是由CPU提供的;CPU又提供了原子操作指令,操作系统又根据CPU的原子操作指令提供了 mutex 互斥锁,JVM 又根据操作系统的互斥锁提供了 synchronized 与 ReentrantLock 等关键字和类。
注:synchronized 不仅仅封装了 mutex 互斥锁,还实现了其他功能。
重量级锁
在某个线程访问同步代码块时,它会向操作系统请求获取互斥锁,如果互斥锁可用,线程将获取锁继续执行;如果不可用,线程就会进入操作系统的阻塞队列中等待被唤醒。加锁机制重度依赖 mutex ,成本较高
涉及了大量内核态用户态的切换以及很容易就引发了线程调度
轻量级锁
轻量级锁的加锁机制尽可能的不使用 mutex ,尽量在用户态代码完成,实在不行再调用 mutex
少量内核态用户态切换,不易引发线程调度
synchronized 一开始是轻量级锁,如果锁竞争太激烈就会变成重量级锁
(用户态VS内核态
用户态是指应用程序的运行状态;在用户态下,程序只能访问有限的资源,例如自身的存储空间,并且只能执行非特权指令,类似普通的算术运算、逻辑运算等
内核态是指操作系统内核运行状态;在内核态下程序可以访问系统所有的资源,并且能够执行特权指令)
公平锁VS非公平锁
公平锁
多个线程获取锁时,会按照线程请求锁的先后顺序进行获取,就像排队一样,先到先得。
非公平锁
不考虑线程请求的先后顺序,只要锁一释放其他等待获取锁的线程就都有机会抢占锁。
注意:操作系统对线程的调度是随机的,锁就是非公平锁,如果要实现公平锁,通常通过维护一个等待队列来实现。
synchronized 是非公平锁
可重入锁VS不可重入锁
可重入锁
可重入锁允许同一个线程在未释放锁的情况下多次获取该锁
不可重入锁
不可重入锁不允许同一个线程在未释放锁的情况下多次获取该锁
java 中只要以ReentrantLock 开头命名的锁都是可重入锁,JDK中所有的Lock的实现类也都是可重入锁,包括 synchronized 也是可重入锁
而在Linux 中的 mutex 是不可重入锁
读写锁
读写锁是一种用于多线程环境下的同步机制,它把对共享资源的访问划分为读操作与写操作。允许多个线程同时进行读操作,但是在写操作时会阻塞其他线程
其中,读操作与读操作之间不互斥,读操作与写操作之间互斥,写操作与写操作之间互斥
synchronized 不是读写锁
结合以上,我们可以看出synchronized
- 开始是乐观锁,锁竞争激烈会变成悲观锁
- 开始是轻量级锁,锁被持有的时间很长就会变成重量级锁
- 实现轻量级锁时大概率用的是自旋锁策略
- 是不公平锁
- 是可重入锁
- 不是读写锁
加锁的工作过程
JVM把synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁
偏向锁不是真的加锁,先做一个标记,如果存在其他线程竞争该锁,再进行真正加锁操作;如果没有,就不加了避免了加锁解锁的开销
锁消除
无锁,程序员写了加锁的代码,但其实是单线程的代码,无需加锁,JVM+编译器就判断是否可以进行锁消除,如果可以,就直接消除。
例如:
StringBuffer 中的 append 操作就进行了加锁,但是我在单线程中使用了它,就会进行锁消除操作。
锁粗化
一段代码逻辑中出现多次加锁解锁操作,JVM+编译器就会自动进行锁粗化