Java并发编程中各种锁的分析。
原文链接:https://www.leahy.club/archives/java%E5%9F%BA%E7%A1%80java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E4%B8%AD%E7%9A%84%E5%90%84%E7%A7%8D%E9%94%81
-
公平锁 VS 非公平锁
公平锁:获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁。
非公平锁:获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争。
ReentrantLock默认是非公平锁,但是其还有有参的构造器可以将参数设置为true,就是公平锁了。
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
Synchronized是非公平锁。
-
可重入锁 VS 不可重入锁
可重入锁指的是可重复可递归调用的锁,拿到外层的锁之后,也就拿到了内部的锁。可重入不是可重复利用的意思,而是可以递归进入的意思。
不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁,同一个线程两次调用lock方法 ,如果不去unlock的话,第二次lock会产生死锁。
综上,可重入锁并不是可以多次使用的锁,而是可重入可递归!举个例子:
public class UnReentrant{ Lock lock = new Lock(); public void outer(){ lock.lock(); inner(); lock.unlock(); } public void inner(){ lock.lock(); //do something lock.unlock(); } }
outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入 。相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
不可重入锁的基本原理:
当isLocked被设置为true后,在线程调用unlock()解锁之前不管线程是否已经获得锁,都只能wait()。
代码如下: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(); } }
可重入锁的基本原理:
修改Lock,加入一个变量lockBy用来保存已经获得锁的线程,这样就能对有锁的线程放行。代码如下:
public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(isLocked && lockedBy != callingThread){ wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock(){ if(Thread.curentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } }
具体实现:Synchronized和ReentrantLock都是可重入锁。
public class Demo{ public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sms(); }, "A").start(); new Thread(() -> { phone.sms(); }, "B").start(); } } class Phone { public synchronized void sms() { System.out.println(Thread.currentThread().getName() + "sms"); call(); } public synchronized void call() { System.out.println(Thread.currentThread().getName() + "call"); } }
不可重入锁:使用自己写的自旋锁(自旋锁是不可重入锁)来实现;但是也可以将自旋锁改为可重入锁,详见代码。
UnReentryLock.java
import java.util.concurrent.atomic.AtomicReference; public class UnReentryLock { private AtomicReference<Thread> owner = new AtomicReference<>(); private int state = 0; public void lock() { Thread current = Thread.currentThread(); //自旋锁 while (!owner.compareAndSet(null, current)); } public void unlock() { Thread current = Thread.currentThread(); owner.compareAndSet(current, null); } //可重入的lock public void relock() { Thread current = Thread.currentThread(); if(current == owner.get()) { state++; return; } //自旋锁,owner的初始值为null,故先进来的thread获得锁 while (!owner.compareAndSet(null, current)); } //可重入的unlock public void reunlock() { Thread current = Thread.currentThread(); //在执行每次操作之前,判断当前锁持有者是否是当前对象,采用state计数,不用每次去释放锁。 if (current == owner.get()) { if (state != 0) state--; else owner.compareAndSet(current, null); } } }
Test
public class Test{ public static void main(String[] args) { Phone3 phone = new Phone3(); new Thread(() -> { phone.sms(); }, "A").start(); new Thread(() -> { phone.sms(); }, "B").start(); } } class Phone { private UnReentryLock lock = new UnReentryLock(); public void sms() { lock.lock(); System.out.println(Thread.currentThread().getName() + "sms"); call(); lock.unlock(); } public synchronized void call() { lock.lock(); System.out.println(Thread.currentThread().getName() + "call"); lock.unlock(); } } class Phone2 { public synchronized void sms() { System.out.println(Thread.currentThread().getName() + "sms"); call(); } public synchronized void call() { System.out.println(Thread.currentThread().getName() + "call"); } } class Phone3 { private UnReentryLock lock = new UnReentryLock(); public void sms() { lock.relock(); System.out.println(Thread.currentThread().getName() + "sms"); call(); lock.reunlock(); } public synchronized void call() { lock.relock(); System.out.println(Thread.currentThread().getName() + "call"); lock.reunlock(); } }
根据结果可知,Phone和Phone3均调用了可重入锁,Phone2调用的是不可重入锁。
-
自旋锁 VS 互斥锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
自旋锁存在的问题:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的优点:
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
自旋锁的其他变种:
TicketLock主要解决的是公平性的问题。
思路:每当有线程获取锁的时候,就给该线程分配一个递增的id,我们称之为排队号,同时,锁对应一个服务号,每当有线程释放锁,服务号就会递增,此时如果服务号与某个线程排队号一致,那么该线程就获得锁,由于排队号是递增的,所以就保证了最先请求获取锁的线程可以最先获取到锁,就实现了公平性。
可以想象成银行办理业务排队,排队的每一个顾客都代表一个需要请求锁的线程,而银行服务窗口表示锁,每当有窗口服务完成就把自己的服务号加一,此时在排队的所有顾客中,只有自己的排队号与服务号一致的才可以得到服务。
public class TicketLock { /** * 服务号 */ private AtomicInteger serviceNum = new AtomicInteger(); /** * 排队号 */ private AtomicInteger ticketNum = new AtomicInteger(); /** * 新增一个ThreadLocal,用于存储每个线程的排队号 */ private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>(); public void lock() { int currentTicketNum = ticketNum.incrementAndGet(); // 获取锁的时候,将当前线程的排队号保存起来 ticketNumHolder.set(currentTicketNum); while (currentTicketNum != serviceNum.get()) { // Do nothing } } public void unlock() { // 释放锁,从ThreadLocal中获取当前线程的排队号 Integer currentTickNum = ticketNumHolder.get(); serviceNum.compareAndSet(currentTickNum, currentTickNum + 1); } }
TicketLock存在的问题:
多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
修改自:https://zhuanlan.zhihu.com/p/40729293
-
乐观锁 VS 悲观锁
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。使用场景:
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
详见:https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484911&idx=1&sn=1d53616437f50b353e33edad6fda2e4f&source=41#wechat_redirect