悲观锁/乐观锁
这是一种策略。
悲观锁的意思就是对一个数据的并发操作前都会有其他线程修改,然后我每次都需要加锁。在Java中的应用就是各种锁。
乐观锁的意思就是认为不会有其他线程干扰,在尝试更新的时候先会读取现在的值,然后在更新前判断这个值是否被更新过,更新过那么就不断循环判断。
思路代码:
sychronized
1.6之前 synchronized 都是重量级锁
1.6 之后进行了 锁的升级 优化,产生了 偏向锁,轻量级锁(自旋锁),重量级锁
- 开始的时候synchronized 锁住一个对象,这时候属于偏向锁,这样当自己再次进入这个循环的时候,不需要再加锁,因为会默认给拥有锁的对象,然后假如又来一个线程申请这个锁,持有这个锁的还是不释放,那么就会进行锁的升级到轻量级锁,然后等待锁的线程自旋了10次,超过了忙等的时间,就会把自己挂起,然后把锁升级为重量级锁,等待唤醒。
可重入锁
ReentrantLock 是一个独占/排他锁 可以显示加锁,必须手动释放
公平锁和非公平锁
多个线程等待一把公平锁,那么先申请的先得到,后申请得后得到,这就是公平锁,反之就是非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
独享锁/共享锁
都是基于AQS设计实现
独享锁 是只能被一个线程持有
共享锁是可以被多个线程持有
RnentrantWriteLock ReentrantLock就是独享锁
ReadWriteLock 读锁是共享锁 写锁是独享锁
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
字面意思是线程之间互相排斥的锁,也就是表明锁只能被一个线程拥有。
互斥锁在Java中的具体实现就是ReentrantLock synchronized
读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。读写锁在Java中的具体实现就是ReadWriteLock
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
-
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
-
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
-
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。
在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。
在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。
CAS
乐观锁的基础
本质使用Unsafe 类的CompareAndSwap 实现。底层是用汇编的函数,实现了 有一个旧的地址V 一个估计值 A 想要设置的值B
V == A ? B:null
-
比较:读取到一个值 A,在将其更新为 B 之前,检查原值是否为 A(未被其它线程修改过,这里忽略 ABA 问题)。
-
替换:如果是,更新 A 为 B,结束。如果不是,则不会更新。
-
ABA 问题
就是线程1操作数据a,线程2 也操作数据 a,但是 2删除了又添加a ,这样线程1读到的还是a ,但是如果是链表之类的呢?虽然还是a 的引用,但是a.next 为null 那么就不对了,所以方法就是可以加一个version 版本号
public int data = 123;
boolean flag = true;
public void setData(int newData){
while (flag){
int oldDate = data;
newData = newData + 1;
//下面的就是CAS操作
if(data == oldDate){
data = newData;
flag = false;
}else{
//啥也不干,再次循环 直到满足
}
}