文章目录
锁
synchronized与Lock
Java中有两种加锁的方式:一种是用synchronized关键字,另一种是用Lock接口的实现类。
其实只需要关注三个类就可以了:ReentrantLock类、ReadLock类、WriteLock类。ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”
Synchronized和ReentrantLock的区别
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LOCK {
private static Lock lock=new ReentrantLock();
public static void main(String[] args) {
new Thread(()->test(),"线程1").start();
new Thread(()->test()).start();
}
public static void test() {
try {
lock.lock();//加锁
System.out.println(Thread.currentThread().getName()+"---已经获取锁");
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"---已经释放锁");
lock.unlock();//解锁
}
}
}
功能区别
- Synchronized是Java语言的关键字,是通过JVM实现的。
- ReentrantLock是API层面的一种互斥锁,而且它的锁粒度和灵活性都优于Synchronized。
- Synchronized使用较为简单方便,由编译器去保证锁的加锁和释放。而ReentrantLock需要手动加锁和释放锁,如果忘记手工释放锁,会造成死锁。
性能区别
在Synchronized优化之前,性能是比较差的,因为都是重量级锁。但是Synchronized引入偏向锁,轻量级锁之后,两者的性能相差不大。当不需要使用ReentrantLock的特有性质时,官方建议使用Synchronized。
ReentranLock的特有性质
等待可中断: 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
公平锁: 多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁是非公平锁,ReentrantLock可以实现公平锁。
选择性通知(锁绑定多个条件): 一个ReentrantLock可以同时绑定多个对象,然后通过condition(条件)类,来分组唤醒线程,而Synchronized是随机唤醒一个线程或者所有线程。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
private static Lock lock=new ReentrantLock(true);
public static void main(String[] args) {
new Thread(()->test(),"线程1").start();
new Thread(()->test(),"线程2").start();
new Thread(()->test(),"线程3").start();
new Thread(()->test(),"线程4").start();
new Thread(()->test(),"线程5").start();
}
private static void test() {
for(int i=0;i<2;i++) {
try {
lock.lock();//加锁
System.out.println(Thread.currentThread().getName()+"---以获得锁");
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"---以释放锁");
lock.unlock();
}
}
}
}
悲观锁与乐观锁:
- 锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。
- 我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。
悲观锁:悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。
乐观锁:乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。
悲观锁阻塞事务,乐观锁回滚重试, :它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
乐观锁的基础—CAS:
Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
2、设置:如果是,将A更新为B,结束。如果不是,则什么都不做。
上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。
3、有了CAS,就可以实现一个乐观锁:
public static void main(String[] args) {
int data =123;//共享数据
/*
* 更新数据的线程会进行如下操作
*/
boolean flag=true;
while(flag) {
int oldValue = data;//保持原始数据
int newValue = new Random().nextInt();
//下面的部分为CAS操作,尝试更新data的值
if(data==oldValue) {//比较
data=newValue;//设置
flag=false;//结束
}else {
//啥也不干,循环重试
}
}
/*
* 很明显这样的代码根本不是原子性的,
* 因为真正的CAS利用了CPU指令,
* 这里只是为了展示执行流程,本意一样
*/
}
4、Java中真正的CAS操作调用的native方法因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!
自旋锁
有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。
synchrnized锁升级:偏向锁->轻量级锁->重量级锁
- 前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。
- 初次执行到synchronized代码块的时候,锁对象变成偏向锁,字面意思是==“偏向于第一个获得它的线程”==的锁。
- 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
==在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。==获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为 重量级锁 (依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
一个锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀),不允许降级。 - 偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
可重入锁(递归锁)
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
设计一个不可重入锁
public class Lock{
private boolean islocked=false;
public synchronized void lock() throws InterruptedException {
while(islocked) {
wait();
}
islocked=true;
}
public synchronized void unlock() {
islocked=false;
notify();
}
}
使用该锁:
public class LockTest {
Lock lock=new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑必须先释放锁,这很好说明了不可重入锁。
公平锁、非公平锁
- 如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。
- 对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。
- ReentrantLock构造器可以指定为公平或非公平,对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。
可中断锁
可中断锁,字面意思是“可以响应中断的锁”。
这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。
在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。
读写锁、共享锁、互斥锁(了解)
- 读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。
- 读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。
虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程。
JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。
JDK并发包到底有没有乐观锁呢?
有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。