欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
5、多线程各种锁
公平锁/非公平锁/可重入锁/递归锁/自旋锁,谈谈你的理解?请手写一个自旋锁
5.1、公平锁与非公平锁
是个啥玩意儿?
-
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
-
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象
-
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
公平锁与非公平锁的区别
公平锁
- 公平锁:Threads acquire a fair lock in the order in which they requested it
- 公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
非公平锁
- 非公平锁:a nonfair lock permits barging:threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.
- 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
题外话
- Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
- 对于Synchronized而言,也是一种非公平锁
5.2、可重入锁
可重入锁(又名递归锁)是啥玩意儿?
- 可重入锁(也叫做递归锁)指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
- 就像有了家门的锁,厕所、书房、厨房就为你敞开了一样
- 也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
- ReentrantLock,synchronized 就是一个典型的可重入锁
- 可重入锁的最大作用就是避免死锁
可重入锁的代码示例
synchronized 示例
-
代码
/*
- 可重入锁(也就是递归锁)
- 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
- 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。
- t1 invoked sendSMS() t1线程在外层方法获取锁的时候
- t1 invoked sendEmail() t1在进入内层方法会自动获取锁
- t2 invoked sendSMS()
- t2 invoked sendEmail()
*/ public class RenenterLockDemo { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "t1").start();
new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "t2").start(); }}
class Phone { public synchronized void sendSMS() throws Exception { System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()"); sendEmail(); }
public synchronized void sendEmail() throws Exception { System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()"); }}
-
程序运行结果
t1 invoked sendSMS() t1 invoked sendEmail() t2 invoked sendSMS() t2 invoked sendEmail() 1234
ReentrantLock 示例
-
代码
/*
- 可重入锁(也就是递归锁)
- 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
- 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。
*/ public class RenenterLockDemo { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { try { phone.get(); } catch (Exception e) { e.printStackTrace(); } }, "t1").start();
new Thread(() -> { try { phone.get(); } catch (Exception e) { e.printStackTrace(); } }, "t2").start(); }}
class Phone implements Runnable { //Reentrant TEST Lock lock = new ReentrantLock();
@Override public void run() { get(); } public void get() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "get()"); set(); } finally { lock.unlock(); } } public void set() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "set()"); } finally { lock.unlock(); } }}
-
程序运行结果
t1 get() t1 set() t2 get() t2 set() 1234
锁两次,释放两次
-
代码
class Phone implements Runnable { //Reentrant TEST Lock lock = new ReentrantLock();
@Override public void run() { get(); } public void get() { lock.lock(); lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "get()"); set(); } finally { lock.unlock(); lock.unlock(); } } public void set() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "set()"); } finally { lock.unlock(); } }}
-
程序运行结果:正常执行
t1 get() t1 set() t2 get() t2 set() 1234
锁两次,释放一次
-
代码
class Phone implements Runnable { //Reentrant TEST Lock lock = new ReentrantLock();
@Override public void run() { get(); } public void get() { lock.lock(); lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "get()"); set(); } finally { lock.unlock(); } } public void set() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "set()"); } finally { lock.unlock(); } }}
-
程序运行结果:由于 t1 线程未释放 lock 锁,程序卡死(死锁)
结论:锁几次,就释放几次
5.3、自旋锁
什么是自旋锁?
自旋锁(SpinLock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
12345678
自旋锁代码示例
-
代码:使用 AtomicReference 封装 Thread ,通过 CAS算法实现线程的自旋锁
/**
- 写一个自旋锁
- 自旋锁的好处:循环比较获取直到成功为止,没有类似wait的阻塞。
- 通过CAS操作完成自旋锁:
- A线程先进来调用myLock方法自已持有锁5秒钟
- B随后进来后发现当前有线程持有锁,不是null,
- 所以只能通过自旋等待,直至A释放锁后B随后抢到
*/ public class SpinLockDemo { // 泛型为 Thread AtomicReference atomicReference = new AtomicReference<>();
public void myLock() { // 获取当前线程 Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "\t come in "); /* 自旋: 期望值为 null 表示当前没有线程 新值为 thread ,即 Thread.currentThread() */ while (!atomicReference.compareAndSet(null, thread)) { } } public void myUnLock() { // 获取当前线程 Thread thread = Thread.currentThread(); // 解锁当前线程 atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName() + "\t invoked myUnLock()"); } public static void main(String[] args) { // 原子引用线程 SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.myLock(); // 加锁 try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnLock(); // 解锁 }, "AA").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { spinLockDemo.myLock(); // 加锁 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnLock(); // 解锁 }, "BB").start(); }}
-
程序运行结果:核心为 CAS 算法
- 线程 A 先执行,此时期望值为 null ,线程 A 将获得锁,并将期望值设置为线程 A 自身
- 线程 B 尝试获取锁,发现期望值并不是 null ,就在那儿原地自旋
- 线程 A 释放锁之后,将期望值设置为 null ,此时线程 B 获得锁,将期望值设置为线程 B 自身
- 最后线程 B 释放锁
AA come in BB come in AA invoked myUnLock() BB invoked myUnLock() 1234
5.4、读写锁
独占锁(写锁)、共享锁(读锁)、互斥锁
- 独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和synchronized而言都是独占锁
- 共享锁:指该锁可以被多个线程所持有
- 对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
- 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
读写锁的代码示例
-
代码:使用 ReentrantReadWriteLock 完成读锁、写锁分离
/**
- 多个线程同时读一个资源类没有问题,所以为了满足并发量,读取共享资源应该可以同时进行。
- 但是写资源只能有一个线程。
- 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断。
- 小总结:
- 读-读能共存
- 读-写不能共存
- 写-写不能共存
- 写操作:原子性+独占,整个过程必须是一个完整的统一体,中间不许被分隔,被打断
public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 1; i <= 5; i++) { int tempInt = i; new Thread(() -> { myCache.put(tempInt + "", tempInt + ""); }, String.valueOf(i)).start(); }
for (int i = 1; i <= 5; i++) { int tempInt = i; new Thread(() -> { myCache.get(tempInt + ""); }, String.valueOf(i)).start(); } }}
class MyCache { // 凡缓存,一定要用 volatile 修饰,保证内存可见性 private volatile Map<String, Object> map = new HashMap<>(); // ReentrantReadWriteLock:读写锁 private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void put(String key, Object value) { reentrantReadWriteLock.writeLock().lock(); // 加写锁 try { System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key); try { TimeUnit.MILLISECONDS.sleep(300); // 模拟网络传输,暂停线程一会儿 } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); System.out.println(Thread.currentThread().getName() + "\t 写入完成"); } catch (Exception e) { e.printStackTrace(); } finally { reentrantReadWriteLock.writeLock().unlock(); // 释放写锁 } } public void get(String key) { reentrantReadWriteLock.readLock().lock(); // 加读锁 try { System.out.println(Thread.currentThread().getName() + "\t 正在读取:" + key); try { TimeUnit.MILLISECONDS.sleep(300); // 模拟网络传输,暂停线程一会儿 } catch (InterruptedException e) { e.printStackTrace(); } Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t 读取完成" + result); } catch (Exception e) { e.printStackTrace(); } finally { reentrantReadWriteLock.readLock().unlock(); // 释放读锁 } }}
-
程序运行结果:写操作没有被打断
2 正在写入:2 2 写入完成 3 正在写入:3 3 写入完成 1 正在写入:1 1 写入完成 4 正在写入:4 4 写入完成 5 正在写入:5 5 写入完成 1 正在读取:1 2 正在读取:2 3 正在读取:3 4 正在读取:4 5 正在读取:5 2 读取完成2 4 读取完成4 1 读取完成1 5 读取完成5 3 读取完成3 1234567891011121314151617181920
-
在ReentrantReadWriteLock中分别维护了一把读锁和一把写锁
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; /** Inner class providing readlock / private final ReentrantReadWriteLock.ReadLock readerLock; /* Inner class providing writelock / private final ReentrantReadWriteLock.WriteLock writerLock; /* Performs all synchronization mechanics */ final Sync sync; 123456789
6、线程通信
CountDownLatch、CyclicBarrier、Semaphore使用过吗?
6.1、CountDownLatch
CountDownLatch 的用法
- 让一些线程阻塞,直到另一些线程完成一系列操作后才被唤醒
- CountDownLatch 维护了一个计数器,有两个核心方法:
countDown()和await()- 调用
countDown()方法会将计数器减一 - 当计数器的值不为零时,线程调用
await()方法时,会被阻塞 - 当计数器的值变为0时,因调用
await()方法被阻塞的线程会被唤醒,继续执行
- 调用
CountDownLatch 代码示例
-
代码
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { leaveClassroom(); }
private static void leaveClassroom() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); // 初始化次数为 6 for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t上完自习,离开教室"); countDownLatch.countDown(); // 计数器减 1 }, String.valueOf(i)).start(); } countDownLatch.await(); // 等待上述线程执行完成(等待计数减为 0) System.out.println(Thread.currentThread().getName() + "\t ******班长最后关门走人"); }} 123456789101112131415161718192021222324
-
程序运行结果:班长等待所有同学都完成自习后,再锁门
2 上完自习,离开教室 1 上完自习,离开教室 3 上完自习,离开教室 4 上完自习,离开教室 5 上完自习,离开教室 6 上完自习,离开教室 main ******班长最后关门走人 1234567
CountDownLatch + 枚举类的使用
-
定义枚举类:可以通过 CountryEnum.ONE 获得齐国对应的 CountryEnum 对象
public enum CountryEnum { ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");
private Integer retCode; private String retMsg; CountryEnum(Integer retCode, String retMsg) { this.retCode = retCode; this.retMsg = retMsg; } public Integer getRetCode() { return retCode; } public void setRetCode(Integer retCode) { this.retCode = retCode; } public String getRetMsg() { return retMsg; } public void setRetMsg(String retMsg) { this.retMsg = retMsg; } public static CountryEnum list(int idx) { // 获取枚举类中的所有值 CountryEnum[] countryEnums = CountryEnum.values(); for (CountryEnum countryEnum : countryEnums) { if (idx == countryEnum.getRetCode()) { return countryEnum; } } return null; }}
-
秦灭六国,后一统华夏
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { county(); }
private static void county() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); // 初始化次数为 6 for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 国被灭"); countDownLatch.countDown(); // 计数器减 1 }, CountryEnum.list(i).getRetMsg()).start(); } countDownLatch.await(); // 等待上述线程执行完成(等待计数减为 0) System.out.println(Thread.currentThread().getName() + "\t ******秦国一统华夏"); }} 123456789101112131415161718192021222324
-
程序运行结果
齐 国被灭 魏 国被灭 燕 国被灭 楚 国被灭 韩 国被灭 赵 国被灭 main ******秦国一统华夏 1234567
6.2、CyclicBarrier
CyclicBarrier 的使用
- CyclicBarrier 字面意思是可循环使用的屏障。它要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续干活
- 通过 CyclicBarrie r的 await() 方法,使线程进入屏障
- CountDownLatch 是减,而 CyclicBarrier 是加,理解了CountDownLatch,CyclicBarrier 就很容易
- 一句话总结:集齐7颗龙珠召唤神龙
CyclicBarrier 代码示例
-
代码
public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("召唤神龙"); // 龙珠收集完成,召唤神龙 });
for (int i = 1; i <= 7; i++) { final int tempInt = i; new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 收集到第:" + tempInt + "龙珠"); try { cyclicBarrier.await(); // 等待龙珠收集完成 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } }} 1234567891011121314151617181920212223242526272829
-
程序运行结果(感觉像是线程完成后的回调函数)
2 收集到第:2龙珠 3 收集到第:3龙珠 1 收集到第:1龙珠 6 收集到第:6龙珠 5 收集到第:5龙珠 4 收集到第:4龙珠 7 收集到第:7龙珠 召唤神龙 12345678
6.3、Semaphore
Semaphore 的使用
- Semaphore 即信号量,信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
- 构造器
Semaphore(int)用于指定共享资源的数目,如果设定为 1 ,则 Semaphore 信号量退化为 Lock 锁或者 synchronized 锁 - 调用
semaphore.acquire()方法获取对共享资源的使用,调用semaphore.release()释放对共享资源的占用 - 一句话讲明白:抢车位
Semaphore 代码示例
-
代码:使用 Semaphore 完成对共享资源的并发控制
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); // 模拟3个车位 for (int i = 1; i <= 6; i++) { // 模拟6部车 new Thread(() -> { try { semaphore.acquire(); // 尝试抢车位(获取信号量) System.out.println(Thread.currentThread().getName() + "\t抢到车位"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t停车3秒后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放车位(释放信号量) } }, String.valueOf(i)).start(); } } } 123456789101112131415161718192021222324252627282930
-
程序运行结果
2 抢到车位 1 抢到车位 3 抢到车位 3 停车3秒后离开车位 2 停车3秒后离开车位 1 停车3秒后离开车位 5 抢到车位 6 抢到车位 4 抢到车位 4 停车3秒后离开车位 6 停车3秒后离开车位 5 停车3秒后离开车位