锁是并发中非常非常重要的部分,从最开始学并发常用的synchronized或者Lock到更进一步了解并发编程,会发现锁非常的多,概念也很多,不容易区分。
在较为全面的了解了之后决定先写下这篇博客打个底,并在后期的学习中进一步完善我的锁的知识体系
Lock中声明了四个方法来获取锁
lock.lock();
try {
//获取本锁保护的资源
System.out.println(Thread.currentThread().getName()+"开始执行任务");
}finally {
lock.unlock();
}
tryLock()
tryLock(long time,TimeUnit unit)
/**
* 〈用trylock避免死锁〉
*
* @author Chkl
* @create 2020/3/11
* @since 1.0.0
*/
public class TryLockDeadLock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程1获取到了锁2");
System.out.println("线程1获取到了两把锁");
break;
}finally {
lock2.unlock();
}
}else {
System.out.println("线程1获取锁2失败");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程2获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程2获取到了锁2");
System.out.println("线程2获取到了两把锁");
break;
}finally {
lock1.unlock();
}
}else {
System.out.println("线程2获取锁2失败");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程2获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 〈验证尝试获取锁期间可中断线程〉
*
* @author Chkl
* @create 2020/3/11
* @since 1.0.0
*/
public class LockInterruptibly implements Runnable {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread thread0 = new Thread(lockInterruptibly);
Thread thread1 = new Thread(lockInterruptibly);
thread0.start();
thread1.start();
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
//线程启动2秒后,一个线程获得锁并处于睡眠,另一个线程处于等待锁状态
thread1.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName()+"获取到了锁");
//等待5秒,期间第二个线程被中断
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期间被中断");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "等锁期间被中断");
}
}
}
运行结果可能如下图所示(线程先执行顺序不一定)
中断thread0
Thread-0尝试获取锁
Thread-0获取到了锁
Thread-1尝试获取锁
Thread-1等锁期间被中断
Thread-0释放了锁
中断thread1
Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-0睡眠期间被中断
Thread-0释放了锁
Thread-1获取到了锁
Thread-1释放了锁
悲观锁:
乐观锁:
CAS
算法,典型例子是原子类,并发容器案例演示:实现累加器
public class PessimismOptimismLock {
int a;
//悲观锁
public synchronized void testMethod(){
a++;
}
public static void main(String[] args) {
//乐观锁
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
//悲观锁
new PessimismOptimismLock().testMethod();
}
}
Git:Git是乐观锁的典型应用,当我们向远程仓库push的时候,git会检查远程仓库的版本是不是领先我们现在的版本,
数据库:
update set num = 2 , version = vsersion+1 where version = mversion and id = 5
非可重入锁就是最常见的锁,一旦锁被使用,如果没有释放,就不能再使用这个锁了
可重入锁以ReentrantLock为例进行展开
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
运行结果:
0
1
2
3
2
1
0
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource(){
lock.lock();
try {
System.out.println("已经对资源进行处理");
if (lock.getHoldCount()<5){
//递归调用
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
new RecursionDemo().accessResource();
}
}
运行结果:
已经对资源进行处理
1
已经对资源进行处理
2
已经对资源进行处理
3
已经对资源进行处理
4
已经对资源进行处理
4
3
2
1
从结果可以看出获得锁之后是可以重复获得锁再最后释放的,这就是可重入锁
演示案例:模拟打印机打印任务,有两个类,一个是打印作业Job类,一个是打印队列PrintQueeue 类,一个打印任务包含两次打印,两次获得锁。在main方法中创建10个线程执行Job,当锁使用公平锁时:
/**
* 〈演示公平锁和不公平锁〉
*
* @author Chkl
* @create 2020/3/11
* @since 1.0.0
*/
public class FairLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
PrintQueeue printQueeue = new PrintQueeue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueeue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
PrintQueeue printQueeue;
public Job(PrintQueeue printQueeue) {
this.printQueeue = printQueeue;
}
@Override
public void run() {
System.out.println(
Thread.currentThread().getName() + "开始打印");
printQueeue.printJob(new Object());
System.out.println(
Thread.currentThread().getName() + "打印结束");
}
}
class PrintQueeue {
//公平锁
private Lock queueLock = new ReentrantLock(true);
//非公平锁
// private Lock queueLock = new ReentrantLock();
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);
} finally {
queueLock.unlock();
}
}
}
使用公平锁进行打印操作,每个锁会依次执行,一定是一个锁结束之后另一个锁开始打印,不会出现插队。一次运行结果如下,因为每次打印后需要休眠n秒模拟打印耗时,休眠时间足够所有的线程依次启动,所以执行顺序一定是线程0-9执行第一个打印后线程0-9执行第二次打印,顺序一定不会变
Thread-0开始打印
Thread-0正在打印,需要时间1
Thread-0正在打印,需要时间2
Thread-0打印结束
Thread-1开始打印
Thread-1正在打印,需要时间9
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-2正在打印,需要时间9
Thread-3正在打印,需要时间1
Thread-4正在打印,需要时间8
Thread-5正在打印,需要时间3
Thread-6正在打印,需要时间5
Thread-7正在打印,需要时间2
Thread-8正在打印,需要时间6
Thread-9正在打印,需要时间2
Thread-1正在打印,需要时间4
Thread-1打印结束
Thread-2正在打印,需要时间6
Thread-2打印结束
Thread-3正在打印,需要时间6
Thread-3打印结束
Thread-4正在打印,需要时间7
Thread-4打印结束
Thread-5正在打印,需要时间8
Thread-5打印结束
Thread-6正在打印,需要时间1
Thread-6打印结束
Thread-7正在打印,需要时间1
Thread-7打印结束
Thread-8正在打印,需要时间3
Thread-8打印结束
Thread-9正在打印,需要时间5
Thread-9打印结束
修改PrintQueeue 中的锁为非公平锁
//非公平锁
private Lock queueLock = new ReentrantLock();
一次运行结果如下,从结果可以看到,打印顺序并没有再按照0-9、0-9执行了,线程2的第一次打印结束后马上又开始了第二次打印,这就是非公平锁的好处了,线程2执行完第一个打印之后,线程3准备打印,但是准备的空窗期线程2干脆一次性把第二次打印也完成了,不影响线程3打印的正常运行,同理下面的线程56789都是这种情况,提高了效率,充分利用了空窗期
Thread-0开始打印
Thread-0正在打印,需要时间1
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-1正在打印,需要时间4
Thread-2正在打印,需要时间5
Thread-2正在打印,需要时间5
Thread-3正在打印,需要时间8
Thread-2打印结束
Thread-3正在打印,需要时间9
Thread-3打印结束
Thread-4正在打印,需要时间8
Thread-5正在打印,需要时间10
Thread-5正在打印,需要时间5
Thread-5打印结束
Thread-6正在打印,需要时间2
Thread-6正在打印,需要时间10
Thread-6打印结束
Thread-7正在打印,需要时间2
Thread-7正在打印,需要时间5
Thread-7打印结束
Thread-8正在打印,需要时间5
Thread-8正在打印,需要时间9
Thread-8打印结束
Thread-9正在打印,需要时间8
Thread-9正在打印,需要时间6
Thread-9打印结束
Thread-0正在打印,需要时间5
Thread-0打印结束
Thread-1正在打印,需要时间6
Thread-1打印结束
Thread-4正在打印,需要时间1
Thread-4打印结束
以ReetrantReadWriteLock读写锁为例
要么多读,要么一写
创建4个线程,前两个获取读锁,后两个获取写锁
运行后可以看到读锁可以同时获取,写锁必须获取释放了才能再获取
public class CinemaReadWrite {
private static ReentrantReadWriteLock
reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁
private static ReentrantReadWriteLock.ReadLock
readLock = reentrantReadWriteLock.readLock();
//写锁
private static ReentrantReadWriteLock.WriteLock
writeLock = reentrantReadWriteLock.writeLock();
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ "得到了读锁,正在读取ing");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ "得到了写锁,正在写入ing");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()
+"释放写锁");
writeLock.unlock();
}
}
}
运行结果:
Thread1得到了读锁,正在读取ing
Thread2得到了读锁,正在读取ing
Thread1释放读锁
Thread2释放读锁
Thread3得到了写锁,正在写入ing
Thread3释放写锁
Thread4得到了写锁,正在写入ing
Thread4释放写锁
不能插队的代码演示:将上面的案例的调用进行修改,顺序为w,r,r,w,r
线程2和线程3执行读的操作的时候,线程5不能插队,因为等待队列头的线程4是写锁
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
}
运行结果:
Thread1得到了写锁,正在写入ing
Thread1释放写锁
Thread2得到了读锁,正在读取ing
Thread3得到了读锁,正在读取ing
Thread3释放读锁
Thread2释放读锁
Thread4得到了写锁,正在写入ing
Thread4释放写锁
Thread5得到了读锁,正在读取ing
Thread5释放读锁
代码演示:
public class CinemaReadWrite {
private static ReentrantReadWriteLock
reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁
private static ReentrantReadWriteLock.ReadLock
readLock = reentrantReadWriteLock.readLock();
//写锁
private static ReentrantReadWriteLock.WriteLock
writeLock = reentrantReadWriteLock.writeLock();
public static void main(String[] args) {
new Thread(() -> write(), "Thread1").start();
new Thread(() -> read(), "Thread2").start();
}
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ "得到了读锁,正在读取ing");
Thread.sleep(1000);
writeLock.lock();
System.out.println("在不释读锁情况下,获取写锁,升级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ "得到了写锁,正在写入ing");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不释放写锁情况下,获取读锁,降级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()
+ "释放写锁");
writeLock.unlock();
}
}
}
运行结果如下,降级成功了,而升级发生了阻塞
Thread1得到了写锁,正在写入ing
在不释放写锁情况下,获取读锁,降级成功
Thread1释放写锁
Thread2得到了读锁,正在读取ing
/**
* 〈演示自旋锁〉
*
* @author Chkl
* @create 2020/3/12
* @since 1.0.0
*/
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void Lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋锁获取失败");
}
}
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.Lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
Java中,synchronized就是不可中断锁,而Lock是可中断锁,因为trylock(time)
和lockInterruptibly
都可以响应中断
如果某个线程A正在执行锁中的代码,另一个线程B正在等待获取该锁,可能由于等待时间太长了,线程B不相等了,可以去处理其他事情,把它中断,这就是中断锁
本文参考了:《玩转Java并发工具》
更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接