Java中锁的分类
1. 悲观锁(互斥同步锁)
代表:synchronized、Lock相关类、数据库
示例:
- 数据库悲观锁修改
-- 锁表修改
select * from x_user for update;
缺点:
- 阻塞和唤醒带来的性能劣势。
- 永久阻塞,如果持有锁的线程无限循环、死锁等活跃性问题,那么等待该线程释放锁的其它线程,将永远也得不到执行。
- 优先级反转,比如优先级低的线程阻塞了,导致优先级高的线程得不到执行
优点:
- 一劳永逸:正常执行时效率高、消耗低
总结:并发写入多,读取少的场景。适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
- 临界区的IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
2. 乐观锁(非互斥同步锁)
代表:利用CAS算法来实现,比如各种原子类(Atomic)、并发容器(ConcurrentHashMap)
示例:
- 数据库乐观锁版本号修改
update x_user set name = '张三', version = version + 1 where version = 1 and id = 3;
缺点:
- 异常执行时,如自旋时间长、重试多的情况消耗资源也越来越多
优点:
- 效率高
总结:写入少,读取多的场景。不加锁的能让读取性能大幅提高。
3. 可重入锁
当前线程多次调用lock()方法并不需要解锁,该特性称之为可重入性。该特性原因是内部维持了一个状态值(state)变量来记录当前线程持有锁的次数,只有该状态值(state)为0才表示该线程真正的释放了锁。
代表:synchronized关键字、ReentrantLock类
场景:递归调用
private static final ReentrantLock lock = new ReentrantLock();
private static void recursion() {
System.out.println("调用lock方法之前Hold Count:" + lock.getHoldCount());
try {
lock.lock();
System.out.println("调用lock方法之后Hold Count:" + lock.getHoldCount());
if (lock.getHoldCount() < 2) {
recursion();
}
} finally {
System.out.println("---调用unlock方法之后Hold Count:" + lock.getHoldCount());
lock.unlock();
System.out.println("---调用unlock方法之后Hold Count:" + lock.getHoldCount());
}
}
调用lock方法之前Hold Count:0
调用lock方法之后Hold Count:1
调用lock方法之前Hold Count:1
调用lock方法之后Hold Count:2
---调用unlock方法之后Hold Count:2
---调用unlock方法之后Hold Count:1
---调用unlock方法之后Hold Count:1
---调用unlock方法之后Hold Count:0
ReentrantLock的其它方法介绍:
isHeldByCurrentThread可以获取锁是否被当前线程持有
getQueueLength可以返回当前正在等待这包所的队列有多长
4. 公平锁和非公平锁
公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定的情况下,可以插队。
代表:ReentrantLock,其构造参数true表示公平,false表示非公平。
/**
* 描述: 演示公平和不公平两种情况
*/
public class FairLock {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[6];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}
class PrintQueue {
private Lock queueLock = new ReentrantLock(true);
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
// 这个地方存在公平、非公平锁的情况
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
需要注意的是,ReentrantLock的tryLock方法是个特例。例如当有线程执行tryLock的时候,一旦有线程释放了锁,那么tryLock的线程就能获取到锁,即使在它之前已经有其它在等待的队列。
锁 | 优势 | 劣势 |
---|---|---|
公平锁 | 各线程公平等待,每个线程在等待一会儿后都能获得执行的机会 | 更慢,吞吐量小 |
非公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿,也就是某些线程长时间得不到执行 |
5. 共享锁和排它锁
- 共享锁:又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其它线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。
- 排它锁:又称为独占锁、独享锁、写锁
代表:ReentrantReadWriteLock(其中读锁是共享锁,写锁是独享锁)。
读写锁规则:一个或多个线程同时可以有读锁,或者一个线程有写锁,但两者不会同时出现。即要么多读,要么一写。
共享锁插队策略
- 公平锁
不允许插队 - 非公平锁
- 写锁可以随时插队
- 读锁仅在等待队列头节点不是写锁的线程可以插队
public class CinemaReadWriteLock { private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); /** * 读锁 */ private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); /** * 写锁 */ private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); /** * 读锁:只能多读 */ public static void read() { readLock.lock(); System.out.println(Thread.currentThread().getName() + ":获取到了读锁"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + ":释放了读锁"); readLock.unlock(); } } /** * 写锁:只能一写 */ public static void write() { writeLock.lock(); System.out.println(Thread.currentThread().getName() + ":获取到了写锁"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + ":释放了写锁"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(CinemaReadWriteLock::read,"1").start(); new Thread(CinemaReadWriteLock::read,"2").start(); new Thread(CinemaReadWriteLock::write,"3").start(); new Thread(CinemaReadWriteLock::write,"4").start(); } }
6. 自旋锁
代表:Atmoic原子类基本都是自旋锁的实现(死循环),其原理是CAS。
7. 可中断锁
如果某一线程a正在执行锁的代码,另一个线程b正在等待获取该锁,可能由于等待世间过长,这个时候线程b可以中断先处理其它的代码。
代表:Lock是可中断锁,而synchronized就是不可中断。