锁分类
- 公平锁:多线程按照排队顺序获取锁,先阻塞的线程(因为没有获取到锁)先获取到锁,比如上厕所排号,里面只有一个坑,那肯定是先来排号的先上。
- 非公平锁:与公平锁相反,获取锁的顺序那就是随机的(不一定先到先得),比如上厕所大家都在排号的时候,某一时刻里面的人出来了,恰好一个人跑过来没有排队就直接进去了。
- 可重入锁:同一个线程可以多次获取同一个锁
- 独占锁:一个锁在同一时刻只能被同一个线程获取
- 乐观锁:认为拿数据的时候别人不会修改,适用于多读的场景
- 悲观锁:与乐观锁相反,使用与多写的场景
锁优化
- 自旋锁
- 自适应自旋锁
- 偏向锁
- 轻量级锁
- 重量级锁
synchronized
属于非公平锁、可重入锁、互斥锁
举例:
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method();
}
private static void method() {
}
}
查看字节码文件:
一个进程进入和退出同步方法时需要经过上面的monitorenter和monitorexit,每一个对象都有一个与之关联的monitor对象,当一个monitor被持有的时候,这个对象就处于锁定状态。sychronized的可重入性是由对象的一个计数器来控制的,每次线程获取锁,计数器加1,反之减1,当计数器为0时,其他线程可以抢这把锁。
过程如下:
如上图,这个过程没有获取到锁的线程需要进入阻塞状态,因为线程的挂起和恢复都需要切到内核态进行,是很消耗系统资源的。
优化一:自旋锁
jdk1.4引入,1.6默认开启。意思就是:虚拟机先不挂起没有获取锁的线程,先等一小会(可能时间很短),别的线程可能就释放锁了,此优化省去了上面说的线程切换的开销,但是也带来一个问题,就是自旋的线程有可能好长时间也没有获取到锁,那这时也浪费了cpu资源了。这种情况虚拟机会采用一种叫自适应自旋锁的机制:也就是判断你这个线程自旋的时候是否获取到了锁,如果是,那么下次自旋的时候就认为你还有可能获取到锁,那么就让你多自旋几次。
优化二:轻量级锁
先回顾一下对象头(Mark World)里面存的内容:
轻量级锁的实现是通过:对象头和线程互相交换信息来完成的,线程栈中有一份对象头的拷贝,对象头有一个指针指向线程栈中对象头。
过程是这样的:
当线程执行到一个同步代码时,先检查此同步对象是否被锁定。
如果没被锁定,则现在当前线程栈中分配一块空间,用来储存对象头,如下图:
然后,虚拟机用cas操作尝试使对象头指向线程栈中的对象头,如果指向成功了,那么这个线程就拥有了该锁。如下图:
如果失败了,虚拟机先检查对象头当前的指针是否就是指向这个线程栈,如果是那么就继续执行同步代码块的代码了,如果不是那么就升级为重量级锁。
解锁过程:置换线程栈中的对象头和原对象头,如果成功就释放成功了,如果失败,则需要在置换成功之后唤醒其他线程(因为锁而阻塞的线程)。
注释:轻量级锁是基于“在绝大多数情况下”,都不存在锁竞争的。否则竞争激励情况下,轻量级锁会比传统的重量级锁更慢。(多了cas操作)
优化三:偏向锁
目的在于无竞争的情况下把锁消除,偏向锁的“偏”字就是偏向的意思,偏向第一个获取锁的线程,(mark world里面存有偏向的线程id)。当线程第一次获取对象锁的时候,锁状态为偏向锁,同时在对象头的Mark world中记录此线程id,当这个线程再执行到同步代码的位置时,不需要加锁。
当发生竞时,偏向锁升级为轻量级锁。
乐观锁:通过版本号或者cas来控制
悲观锁:synchronized
和ReentrantLock
java并发包Lock(ReentrantLock
)
基于AQS和CAS实现:AQS参考:https://www.cnblogs.com/waterystone/p/4920797.html