1. 公平锁和非公平锁
- 定义:
- 公平锁:多个线程按照申请锁的顺序来获取锁,按照FIFO规则从等待队列中拿到等待线程获取相应锁
- 非公平锁:多个线程并不是按照申请锁的顺序来获取锁,有可能出现后申请锁的线程先申请到锁。在高
并发环境下,非公平锁有可能造成 优先级反转 或者 饥饿 的现象。如果非公平锁抢占失败,就要继续采取类似公平锁的机制。非公平锁的优点在于吞吐量大。
- 常见的非公平锁:
- ReentrantLock可以通过指定构造函数的boolean类型来获取公平/非公平锁,默认情况下是非公平锁
- 对于Synchronized而言,也是一种非公平锁
2 可重入锁(递归锁)
- 定义:可重入锁的定义要类比递归的定义来理解。指在同一个线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码,
即进入内层函数时会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块。一个同步方法内部仍然存在一个同步方法,那么可以进入内层同步方法,且内存同步方法和外层同步方法持有的是同一把锁。
具体看一个案例来理解可重入锁:synchronized就是可重入锁,现在问题是synchronized块中能够使用System.out.println()方法?
public void println(String x) {
// println方法内部使用了synchronized
synchronized (this) {
print(x);
newLine();
}
}
/**
* 演示可重入锁
*
* @author sherman
*/
public class LockReentrantDemo1 {
public static void main(String[] args) {
// 程序正常运行输出:hello
new LockReentrantDemo1().lockReentrant();
}
public synchronized void lockReentrant() {
/**
* 注意这个println方法内部就使用了synchronized关键字,锁住了this
* 即synchronized块中仍然能够使用synchronized关键字 -> 可重入的
*/
System.out.println("hello");
}
}
可重入锁的意义有一点类似于事务的传播行为(一个方法运行在另一个开启事务的方法中,那么当前方法的事务行为是什么样的?),类比来说可重入锁意义就是:一个synchronized(锁)块运行在另一个synchronized(块)中,那么当前synchronized的具体表现行为是什么,是直接中断?还是阻塞等待?又或者是正常执行,因为两个synchronized锁住的是同一个对象?
可重入锁的含义就是最后一种:正常执行,因为可重入锁,锁的是同一个对象。
- 典型的可重入锁:ReentrantLock & synchronized关键字
- 作用:最大作用就是防止死锁,因为多层嵌套的锁,其实锁的是同一个对象,另一个含义就是:嵌套方法持有的是同一把锁
具体示例:
/**
* 可重入锁演示
*
* @author sherman
*/
// 演示ReentrantLock是可重入的
class ShareResouce implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
private void get() {
lock.lock();
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": get()");
set();
} finally {
lock.unlock();
}
}
private void set() {
lock.lock();
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": set()");
} finally {
lock.unlock();
}
}
}
public class LockReentrantDemo2 {
// outer()和inner()方法演示synchronized是可重入的
private synchronized void outer() {
System.out.println(Thread.currentThread().getName() + ": outer method()");
inner();
}
// outer()和inner()方法演示synchronized是可重入的
private synchronized void inner() {
System.out.println(Thread.currentThread().getName() + ": inner method()");
}
public static void main(String[] args) {
// 验证synchronized是可重入的
LockReentrantDemo2 lrd = new LockReentrantDemo2();
new Thread(lrd::outer, "thread-1").start();
new Thread(lrd::outer, "thread-2").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 验证ReentrantLock是可重入的
System.out.println("===================");
new Thread(new ShareResouce(), "thread-3").start();
new Thread(new ShareResouce(), "thread-4").start();
}
}
补充:
在使用ReentrantLock类演示可重入锁时,lock.lock()和lock.unlock()数量一定要匹配,否则:
- 当lock.lock()数量 > lock.unlock():程序一直运行
- 当lock.lock()数量 < lock.unlock():抛出java.lang.IllegalMonitorStateException异常
3 自旋锁(SpinLock)
自旋锁尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是避免线程上下文切换的消耗,缺点是如果一直自旋会消耗CPU:
/**
* 自旋锁演示
*
* @author sherman
*/
public class LockSpin {
AtomicReference<Thread> ar = new AtomicReference<>();
private void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": come in!");
while (!ar.compareAndSet(null, thread)) {
}
}
private void unlock() {
Thread thread = Thread.currentThread();
ar.compareAndSet(thread, null);
System.out.println(thread.getName() + ": get out!");
}
public static void main(String[] args) throws InterruptedException {
LockSpin lockSpin = new LockSpin();
new Thread(() -> {
lockSpin.lock();
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockSpin.unlock();
}, "线程A").start();
// 保证线程A先进行获取到锁,让线程B之后自旋
Thread.sleep(1000);
new Thread(() -> {
lockSpin.lock();
lockSpin.unlock();
}, "线程B").start();
}
}
4 读写锁
写锁(独占锁):指该锁一次只能被一个线程所持有,ReentrantLock和Synchronized都是独占锁
读锁(共享锁):指该锁可以被多个线程所持有
-
读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的
/**
演示读写锁
-
@author sherman
*/
class Cache {
private volatile HashMap<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();public Object get(String key) {
lock.readLock().lock();
Object res = null;
try {
System.out.println(Thread.currentThread().getName() + ": 正在读取+++");
Thread.sleep(100);
res = map.get(key);
System.out.println(Thread.currentThread().getName() + ": 读取完成---");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return res;
}public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + ": 正在写入>>>");
Thread.sleep(1000);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + ":写入完成<<<");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
}
public class LockReadWrite {
public static void main(String[] args) {
Cache cache = new Cache();// 写入操作是被一个线程独占的,一旦写线程开始 // 其它线程必须等待其完成后才能继续执行 for (int i = 0; i < 10; i++) { final int tmp = i; new Thread(() -> cache.put(tmp + "", tmp + ""), String.valueOf(i)).start(); } // 读操作可以被多个线程持有 // 其它线程不必等待当前读操作完成才操作 for (int i = 0; i < 10; i++) { final int tmp = i; new Thread(() -> cache.get(tmp + ""), String.valueOf(i)).start(); } }
}
5 CountDownLatch
CountDownLatch是一个计数器闭锁,它通过一个初始化定时器latch,在latch的值被减到0之前,其它线程都会被await()方法阻塞。
以模拟火箭发射过程解释CountDownLatch使用:
/**
* CountDownLatch模拟火箭发射过程:
* 火箭发射之前需要十个线程进行前期检查工作,每个线程耗时0-4s,
* 只有10个线程对应的检查工作全部完成后,火箭才能发射
*
* @author sherman
*/
public class CountDownLatchDemo implements Runnable {
public static final int TASK_NUMBERS = 10;
private static CountDownLatch cdl = new CountDownLatch(TASK_NUMBERS);
public static void main(String[] args) throws InterruptedException {
CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.submit(countDownLatchDemo);
}
cdl.await();
System.out.println("检查工作检查完毕:fire!");
executorService.shutdown();
}
@Override
public void run() {
try {
// 模拟火箭发射前的各种检查工作
int millis = new Random().nextInt(5000);
Thread.sleep(millis);
System.out.println(Thread.currentThread().getName() + ":检查完毕! 耗时:" + millis + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 每次检查完毕后都将计数器减1
cdl.countDown();
}
}
}
6 CyclicBarrier
CyclicBarrier是可循环使用的屏障,它的功能是:让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会被打开,所有被屏障阻塞的方法都会被打开。
A synchronization aid that allows a set of threads to all wait for each other to reach a **common
barrier point**. CyclicBarriers are useful in programs involving a fixed sized party of threads that
must occasionally wait for each other. The barrier is called cyclic because it can be re-used
after the waiting threads are released.
示例:模拟集齐七颗龙珠才能召唤神龙:
/**
* CyclicBarrier模拟集齐七颗龙珠才能召唤神龙
* 设置common barrier point为7,每个线程收集到七颗龙珠之前都会被阻塞
* 每个线程都到达common barrier point时候才会召唤神龙
*
* @author sherman
*/
public class CyclicBarrierDemo implements Runnable {
private static CyclicBarrier cb = new CyclicBarrier(7, () -> System.out.println("召唤神龙"));
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ": 到达同步点(收集到一个龙珠)!");
cb.await();
System.out.println(Thread.currentThread().getName() + ": 阻塞结束,继续执行!");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CyclicBarrierDemo cbd = new CyclicBarrierDemo();
ExecutorService executorService = Executors.newFixedThreadPool(7);
for (int i = 0; i < 7; i++) {
try {
Thread.sleep(new Random().nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.submit(cbd);
}
executorService.shutdown();
}
}
7 Semaphore
Semaphore信号量主要有两个目的:
- 用于多个共享资源的互斥使用;
- 用于并发数量的控制(是synchronized的加强版,当并发数量为1时就退化成synchronized);
主要方法:
- Semaphore(int permits):构造函数,允许控制的并发数量;
- acquire():请求一个信号量,导致信号量的数量减1;
- release():释放一个信号量,信号量加1;
示例:使用Semaphore模拟请车位过程(3个车位,10辆车):
/**
* 使用Semaphore模拟抢车位过程(3个车位,10辆车)
* 任意时刻只有3辆车持有线程
*
* @author sherman
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 模拟三个车位,十辆车
// 任意时刻只有三辆车持有车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ": 抢到车位");
// 每辆车占有车位[3,8]秒时间
Thread.sleep((new Random().nextInt(6) + 3) * 1000);
System.out.println(Thread.currentThread().getName() + ": 释放车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}