Java并发编程(六)---lock
前言
前面几篇文章,我们学习了synchronized的相关知识,以及死锁的发生条件以及避免的方式,其中有一种破坏死锁的方式就是破坏不可抢占条件,通过synchronzied不能实现的,因为synchronized在申请资源的时候,如果申请不到就只能进入阻塞状态,啥都干不了,也不能中断。所以只能通过本期的主角lock
来处理。
lock 与synchronized 的区别
上面说可以通过lock来破坏不可抢占的条件,那么lock为啥可以支持呢?因为lock锁有如下三个特性:
//能够响应中断
void lockInterruptibly() throws InterruptedException;
//能够响应超时
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
//能够非阻塞的获取锁
boolean tryLock()
能够响应中断
synchronized的问题是,持有锁A后,如果尝试获取锁B失败,那么线程就进入
阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。如果阻塞状态的
线程能够响应中断信号,也就是说当我们给阻塞线程发送中断信号的时候,能够唤醒它,
那么它就有机会释放曾经持有的锁A。
支持超时
如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,
那么这个线程也有机会释放曾经持有的锁,
非阻塞地获取锁
通过调用tryLock()方法,如果返回true,则表示获取到锁,如果返回false,则表示获取锁失败,不过其并不会进入阻塞状态,而是直接返回。
Lock的实现原理简介
典型的lock的使用如下所示:
public class LockTest2 {
final Lock lock = new ReentrantLock();
int value = 0;
public void addOne() {
lock.lock();
try {
value + = 1;
} finally {
lock.unlock();
}
}
}
Java SDK里面的ReentrantLock,
内部持有一个volatile的成员变量state,
获取锁的时候,会读写state的值,解锁的时候也会读写state的值,
也就是说在执行value+=1之前,会读写一次volatile变量state,在执行value+=1之后
又读写了一次volatile变量state。
根据Happens-Before规则:
顺序性规则:对于线程T1 value+=1 Happens-Before 释放锁的操作 unlock()
valatile变量规则:由于线程T1获取锁之后state为1,之后释放会后才会变成0,T2回去锁必须先读取state,所以线程T1的unlock()操作Happens-Before线程T2的lock()操作。
根据传递性:所以线程T1的value+=1 Happens-Before 线程T2的lock()操作
第一个lock的实例
public class LockTest {
public static void main(String[] args) {
MyThreadService myThreadService = new MyThreadService();
for (int i = 0; i < 5; i++) {
MyThread myThread = new MyThread("线程" + i, myThreadService);
myThread.start();
}
}
static class MyThread extends Thread {
MyThreadService myThreadService = null;
public MyThread(String name, MyThreadService myThreadService) {
super(name);
this.myThreadService = myThreadService;
}
@Override
public void run() {
myThreadService.printThread();
}
}
}
public class MyThreadService {
final Lock lock = new ReentrantLock();
public void printThread() {
lock.lock();
try {
for (int i = 0; i < 5; i++) {
System.out.println("*******" + Thread.currentThread().getName() + (" " + i));
}
} finally {
lock.unlock();
}
}
}
运行结果是:
从如上运行结果可以看出,每个线程顺序执行,lock 起到了锁的作用,被锁住的代码块同一时刻只有一个线程在执行。
Condition
利用synchronized关键字与wait()和notify/notifyAll() 方法结合实现等待/通知截止机制,同样的运用ReentrantLock类与Condition接口与newCondition()方法同样可以实现等待/通知机制。一个lock对象可以创建多个Condition实例,线程对象可以注册在制定的Conditon中,从而可以选择性的进行线程通知,在调度线程上更加灵活。
notifyAll() 会通知锁对象上面所有等待的线程,效率比较低,而Condition实例上的signAll()方法只会唤醒注册在该Condition实例中的所有等待线程。不会通知锁对象上其余Condition实例中的等待线程。
下面我们通过一个程序来看看。
public class ConditionTest {
public static void main(String[] args) throws InterruptedException {
MyThreadConditionService myThreadConditionService = new MyThreadConditionService();
MyThread myThread = new MyThread(“线程A”, myThreadConditionService);
myThread.start();
Thread.sleep(3000);
myThreadConditionService.signal();
}
static class MyThread extends Thread {
MyThreadConditionService myThreadConditionService = null;
public MyThread(String name, MyThreadConditionService myThreadConditionService) {
super(name);
this.myThreadConditionService = myThreadConditionService;
}
@Override
public void run() {
myThreadConditionService.await();
}
}
static class MyThreadConditionService {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println("await的时间是="+System.currentTimeMillis());
//等待
condition.await();
System.out.println("******这是await之后的代码,signal之后才会执行");
} catch (InterruptedException exception) {
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
System.out.println("signal的时间是="+System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("**********这是signal之后的代码");
} catch (InterruptedException exception) {
} finally {
lock.unlock();
}
}
}
}
程序运行结果:
如上程序,我们定义了await()方法,用于调用condition.await();,定义了signal()方法用于调用condition.signal(),我们可以看到await()的执行时间跟signal()执行时间相差了3秒,而调用condition.signal()方法之后,执行完其后续的代码之后。立马去执行了
condition.await();的后续代码,这里是因为此处只有一个等待线程。唤醒等待队列的时间基本可以忽略。
在使用wait/notify实现等待通知截止的时候我们知道必须执行完notify()方法所在的synchronized代码块之后才释放锁。在这里也差不多,必须执行完sinal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。
注意:必须在condition.await()方法调用之前调用lock.lock()代码获得同步监控器,不然会报错。
重入锁
重入锁的意思是:线程可以重复获取同一把锁(锁的对象相同)
重进入意味着所有的请求是基于"每线程",而不是基于"每调用"的,重进入的实现是通过为每个锁关联一个请求计数和一个占有它的线程,当计数为0时,认为锁是未被占用的,线程请求一个未被占用的锁时,JVM
将记录锁的占用这,并且将请求计数置为1,如果同一个线程再次请求这个锁,计数将递增,每次占用线程退出同步块,计数器值将递减,直到计数器达到0时,锁被释放。
如下程序所示:
public class ReentryLock {
final Lock lock = new ReentrantLock();
int value;
public int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
public void setValue() {
lock.lock();
try {
//在此处如果锁不能重入,则会发生阻塞。如果可以重入则可以加锁成功。
value = getValue() + 100; //1
} finally {
lock.unlock();
}
}
}
调用serValue()方法,在1处,需要调用getValue()方法获取锁,如果不能重入的话,这再此处会发生阻塞,如果可以重入的话则会加锁成功。
公平锁和非公平锁
在使用ReentrantLock的时候,你会发现ReentrantLock
这个类有两个构造函数,一个是传入fair参数的参数,fair参数代表的是锁的公平策略,如果传入true就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序,而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。
公平锁的示例代码如下:
public class FairLock {
final Lock lock = new ReentrantLock(true);
//创建十个线程
public static void main(String[] args) {
final FairLock fairLock = new FairLock();
Runnable runnable = new Runnable() {
public void run() {
System.out.println("★线程" + Thread.currentThread().getName()
+ "运行了");
fairLock.setup();
}
};
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(runnable);
}
for (int i=0;i<10;i++) {
threads[i].start();
}
}
//执行方法
public void setup() {
lock.lock();
try {
System.out.println("********" + Thread.currentThread().getName() + "获得了锁定");
} finally {
lock.unlock();
}
}
}
运行结果:
如上,构建公平锁之后,线程的执行顺序跟其加入的顺序相同,如果我们将其改成非公平锁的话。
final Lock lock = new ReentrantLock(false);
运行结果如下:
则执行顺序不一定是按照其加入的顺序来的,会出现抢占。
总结
本文主要介绍了lock的相关知识,介绍了其余synchronized相比较的不同点。lock 作为并发包中的一员,使用要比synchronized要更加灵活,然后,介绍了Conditon对象,重入锁以及公平锁和非公平锁。
本文首发于:
https://mp.weixin.qq.com/s/kat0u5qQE4u_-eyjG9smKg
参考
https://time.geekbang.org/column/article/88487
https://juejin.im/post/5ab9a5b46fb9a028ce7b9b7e
《Java并发编程实践》
作者:码农飞哥
微信公众号:码农飞哥