本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本节简介
这一节主要介绍Lock以及死锁现象。需要了解进程与线程的区别、并发问题出现的原因、synchronized关键字的同学可以看这个专栏里的前两篇文章。
1.Lock
jdk5.0以后,提供了一个更强大,更便于操控的锁,即Lock接口。你可以通过使用Lock来手动地完成加锁和释放锁的操作,这使得上锁的过程更加可控。通过Lock还可以知道线程有没有成功获取到锁,也可以让多个线程同时对一个类完成读操作,而不像synchronized那样把一个类完全地锁到小房间里,不允许其他线程进行任何操作。
首先你要知道synchronized是java中的一个关键字,但Lock是一个接口,在使用它之前,首先需要创建一个Lock对象。最常被使用的Lock接口的实现类是ReentrantLock(可重入锁,关于可重入锁的详细介绍以及AQS、公平锁等内容将会在后续的面经篇中展现),本文也只介绍这一种。
看看下面这个demo,我定义了一个ReentrantLock,并且使用了最基础的lock和unlock方法来完成加锁和解锁。
public class LearnThread implements Runnable {
private static int count=0;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " 阻塞的 " + (count++));
}
lock.unlock();//你可以尝试在其他时机解锁,比如i==5时,来观察发生的情况。
}
public static void main(String[] args) {
LearnThread t1 = new LearnThread();
new Thread(t1, "线程1").start();
new Thread(t1, "线程2").start();
}
}
复制代码
这里使用Lock的方式实际上是不规范的,我们必须确保Lock总是会被释放,一般来说会把要锁住的代码和lock.lock()这一行一起包括在try代码块里,而把lock.unlock()这一语句写在finally部分。
你也可以使用tryLock方法来试图获取锁,并且针对成功或失败情况做不同的处理
public class LearnThread implements Runnable {
private static int count=0;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
if(lock.tryLock()) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 阻塞的 " + (count++));
}
lock.unlock();
}
else{
System.out.println(Thread.currentThread().getName() + " 获取锁失败! ");
}
}
public static void main(String[] args) {
LearnThread t1 = new LearnThread();
new Thread(t1, "线程1").start();
new Thread(t1, "线程2").start();
}
}
复制代码
我的电脑上给出了这样的输出
线程1 阻塞的 0
线程1 阻塞的 1
线程1 阻塞的 2
线程2 获取锁失败!
线程1 阻塞的 3
线程1 阻塞的 4
复制代码
你可以清楚地看到,线程2在中途尝试过获取锁,但是它失败了,执行了失败策略(打印一句“获取锁失败”)之后就被销毁。
2.死锁现象
想象一个这样的场景,你和你的哥哥在家里,都想给自己做一杯美味的香蕉奶昔。可是家里只剩下一根香蕉和一杯牛奶了。你率先拿到了香蕉,而你哥哥占有了牛奶。现在你们两僵持不下,彼此都不想让步,情况一直这样持续下去,你们都没法喝到美味的香蕉奶昔。
线程之间也会出现这样的情况,看看下面这个demo
class A{
public static int n=10;
}
class B{
public static int n=5;
}
public class LearnThread implements Runnable {
private final static A a=new A();
private final static B b=new B();
@Override
public void run() {
if(Thread.currentThread().getName().equals("线程1")) {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 拿到了a ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 拿到了b ");
}
System.out.println(a.n + b.n);
}
}else{
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 拿到了b ");
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 拿到了a ");
}
System.out.println(a.n + b.n);
}
}
}
public static void main(String[] args) {
LearnThread t1 = new LearnThread();
new Thread(t1, "线程1").start();
new Thread(t1, "线程2").start();
}
}
复制代码
我们的类t1里只有一个A和一个B,我们首先让线程1拿到了a,再用Threa.sleep函数强制让它等待0.1秒,这个间隙里,线程2拿到了b。现在它们都想拿到对方手上的资源,很可惜,我们没有设置让步策略,它们会这样持续下去,直到你自己中断程序。
输出结果如下:
线程1 拿到了a
线程2 拿到了b
复制代码
死锁的其他知识点将在面经篇中展示,这里只对概念做一个基本介绍。