公平和非公平锁
ReetrantLock默认非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁在高并发的情况下,有可能会造成优先级反转或者饥饿现象
Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁
可重入锁
ReentrantLock/Synchronized就是一个典型的默认非公平的可重入锁(但Synchronized无法修改)
作用:避免死锁
当lock和unlock两两配对时,都可以正常执行
当lock和unlock不匹配时,会出现阻塞等待
自旋锁
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
以上篇说的unsafe类的CAS为例
请手动实现一个自旋锁
public class SpinLockDemo {
/**
* 要求手动实现一个自旋锁
*/
//原子引用线程
AtomicReference atomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread(); //当前进来的线程
System.out.println(thread.getName() + " \t come in...");
do {
//模仿CAS的实现
} while (!atomicReference.compareAndSet(null, thread));
}
public void myUnLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.getName()+ " \t invoke 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();
new Thread(()->{
spinLockDemo.myLock();
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
},"BB").start();
}
}
输出结果
独占锁(写锁)/共享锁(读锁)/互斥锁
ReentrantReaddriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock;
可以理解为大家可以一起看同一份名单,但是当有一个人哪去边看边写的时候,其他人无法看,或者有一个人直接拿走去写了别人也无法看到
栗子
public class MyCache {
private volatile Map map = new HashMap<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
WriteLock writeLock = lock.writeLock();
ReadLock readLock = lock.readLock();
public void put(String key, Object value) {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 正在写入...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入完成,写入结果是 " + value);
} finally {
writeLock.unlock();
}
}
public void get(String key) {
try {
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 正在读...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object res = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取完成,读取结果是 " + res);
} finally {
readLock.unlock();
}
}
}
测试代码:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
cache.put(temp + "", temp + "");
}).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
cache.get(temp + "");
}).start();
}
}
}
输出结果:
Thread-0 正在写入...
Thread-0 写入完成,写入结果是 0
Thread-1 正在写入...
Thread-1 写入完成,写入结果是 1
Thread-2 正在写入...
Thread-2 写入完成,写入结果是 2
Thread-3 正在写入...
Thread-3 写入完成,写入结果是 3
Thread-4 正在写入...
Thread-4 写入完成,写入结果是 4
Thread-5 正在读...
Thread-7 正在读...
Thread-8 正在读...
Thread-6 正在读...
Thread-9 正在读...
Thread-5 读取完成,读取结果是 0
Thread-7 读取完成,读取结果是 2
Thread-8 读取完成,读取结果是 3
Thread-6 读取完成,读取结果是 1
Thread-9 读取完成,读取结果是 4
CountDownLatch/CyclicBarrier/Semaphore
CountDownLatch
让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒。CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,调用线程会被堵塞,其他线程调用 countDown 方法会将计数减一(调用 countDown 方法的线程不会堵塞),当计数其值变为零时,因调用 await 方法被堵塞的线程会被唤醒,继续执行。
假设我们有这么一个场景,教室里有班长和其他6个人在教室上自习,怎么保证班长等其他6个人都走出教室在把教室门给关掉。
此时可能输出
发现班长都没有等其他人理他教室就把门给关了,此时我们就可以使用 CountDownLatch 来控制
public class CountDownLanchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 离开了教室...");
countDownLatch.countDown(); //需要注意的
}, String.valueOf(i)).start();
}
countDownLatch.await(); //需要注意的 等待countDownLatch减少到zero才会执行
System.out.println("班长把门给关了,离开了教室...");
}
}
这样,不论执行多少次都会输出以下结果
0 离开了教室...
1 离开了教室...
2 离开了教室...
3 离开了教室...
4 离开了教室...
5 离开了教室...
班长把门给关了,离开了教室...
用枚举类测试:
import java.util.concurrent.CountDownLatch;
public enum CountryEnum {
ONE(1, "齐"),
TWO(2, "楚"),
THREE(3, "燕"),
FOUR(4, "赵"),
FIVE(5, "魏"),
SIX(6, "韩");
private Integer retCode;
private String retMessage;
CountryEnum(Integer retCode, String retMessage) {
this.retCode = retCode;
this.retMessage = retMessage;
}
public Integer getRetCode() {
return retCode;
}
public String getRetMessage() {
return retMessage;
}
public static CountryEnum forEach_CountryEnum(int index) {
CountryEnum[] myArray = CountryEnum.values();
for (CountryEnum element : myArray) {
if (index == element.getRetCode()) {
return element;
}
}
return null;
}
//测试
public static void main(String[] args) throws InterruptedException {
int count = 6;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 1; i <=6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 国,被灭");
countDownLatch.countDown();
},CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t *********秦帝国,一统华夏!");
}
}
输出结果:
齐 国,被灭
魏 国,被灭
赵 国,被灭
燕 国,被灭
楚 国,被灭
韩 国,被灭
main *********秦帝国,一统华夏!
CyclicBarrier
public class CyclicbarrierDemo {
/**
* 当指定的线程都执行完成后,才会触发想要执行的函数
* 类比集齐7颗龙珠才能召唤神龙.
*
* @param args
*/
public static void main(String[] args) {
//cyclicBarrier(int parties,Runnable barrierAction)
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();
}
}
}
输出:
Semaphore
可伸缩量,CountDownLatch是减减到0才开工,CyclicBarrier是++到一定量才开工,而Semaphore是可增可减的
假设我们有 3 个停车位,6 辆车去抢
public class SemaphoreDemo {
public static void main(String[] args) {
//假设停车场目前只有3个空余车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个许可
System.out.println(Thread.currentThread().getName() + " 抢到车位...");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放一个许可
}
}).start();
}
}
}
输出:
Thread-1 抢到车位...
Thread-2 抢到车位...
Thread-0 抢到车位...
Thread-2 离开车位
Thread-0 离开车位
Thread-3 抢到车位...
Thread-1 离开车位
Thread-4 抢到车位...
Thread-5 抢到车位...
Thread-3 离开车位
Thread-5 离开车位
Thread-4 离开车位
阻塞队列
- 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
- 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素
同样
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增
LinkedBlockingQueue 过大
BlockQueue的核心方法
SynchronousQueue
生产一个后必须要消费一个,否则阻塞
synchronized 和 Lock 有什么区别?
- 原始结构
1.synchronized 是关键字属于 JVM 层面,反应在字节码上是 monitorenter 和 monitorexit,其底 层是通过 monitor 对象来完成,其实 wait/notify 等方法也是依赖 monitor 对象只有在同步快或方法中才能调用 wait/notify 等方法。- Lock 是具体类(java.util.concurrent.locks.Lock)是 api 层面的锁。
- 使用方法
- synchronized 不需要用户手动去释放锁,当 synchronized 代码执行完后系统会自动让线程释放对锁的占用。
- ReentrantLock 则需要用户手动的释放锁,若没有主动释放锁,可能导致出现死锁的现象,lock() 和 unlock() 方法需要配合 try/finally 语句来完成。
等待是否可中断
synchronized 不可中断,除非抛出异常或者正常运行完成。
ReentrantLock 可中断,设置超时方法 tryLock(long timeout, TimeUnit unit),lockInterruptibly() 放代码块中,调用 interrupt() 方法可中断。
- 加锁是否公平
- synchronized 非公平锁
- ReentrantLock 默认非公平锁,构造方法中可以传入 boolean 值,true 为公平锁,false 为非公平锁。
- 锁可以绑定多个 Condition
1.synchronized 没有 Condition。
2.ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。