ReentLock的使用
前言
目前状态:对《Java并发编程的艺术》全书进行的两边学习。第一次学习主要是初步了解Java内存模型、Java并发编程基础以及Java并发编程工具包。而后对遗忘较多。尤其是对Java内存模型这块忘记较多。随后又对全书进行了第二次学习,在第一次学习了解并发众多概念基础上更细一步了解并发编程。而后发现对Java并发编程的了解还仅限于皮毛;为此,我需要对并发工具包的特性做一个更细致的了解和源码的解读,以进一步提高对并发编程的了解。其实在网上并不缺少相关技术文章,并且内容非常详尽;我发布此栏博文的目的有三个。
学习希望有所输出,输出是最好的输入;准确和良好的输出是已更加完成的输入和整理为基础,以此来督促自己更加深入的完成相关的学习内容。
网络上相关文章很多,内容也很翔实;甚至包括《Java并发编程的艺术》的作者也开立博客,刊登相关文章;对概念描述精准清晰,对内容全面详细。但我的博客更多是大白话来表述我的理解,尽管在内容上不够全面,却希望初学者能够以更低的门槛窥探到并发编程的美丽世界。
最后一点,若读者朋友发现谬误之处,忘读者们及时指出文章的错误;我会虚心接受及时修改错误内容并及时提升自己,不要让错误的内容误导其他读者,造成读者的困惑。
此处我也不清楚我能坚持此博文能够到哪里一步,但是你们的阅读和评论是对我最好的支持和监督;望所有读者们能在我的文章中能够得到对你有用的内容。
1、ReentrantLock与Condition的使用
什么是ReentrantLock?
锁是控制多线程安全方法共享资源访问的工具。锁有两种实现方式,一种是使用synchronized(隐性锁),另一种是Lock显性锁,这里主要是讲Lock的其中一个实现ReentrantLock。
相对与synchronized关键字,Lock锁的lock()和unlock()方法包含的代码等同于synchronized包含的代码块或者方法体。相比synchronized更加灵活,更加容易理解,但是在使用时更加容易出错。
虽然synchronized的锁,出现异常时能够自动释放锁,使用不易出错;但更推荐使用Lock锁,因为Lock接口有多个实现ReentrantLock、ReentrantReadWriteLock等多个锁实现,在不同读写的情况下,效率往往比synchronized的效率要高。
我们用并发编程的目的也正是为了能够提高代码执行效率嘛。(细粒度锁能够提高同步代码访问的效率)
什么是Condition?
Condition是对Lock状态的管理以及对锁更精确的空中。
Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。(这里需要对synchronized锁有一个初步了解)
public class Cliect {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread t1 = new Thread(new Workder1(lock, condition));
Thread t2 = new Thread(new Workder2(lock, condition));
t1.start();
t2.start();
}
static class Workder1 implements Runnable {
private Lock lock;
Condition condition;
public Workder1(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
condition.signal();
System.out.println("worker1 notify...");
} finally {
lock.unlock();
}
}
}
static class Workder2 implements Runnable {
private Lock lock;
Condition condition;
public Workder2(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
lock.lock();
try {
condition.await();
System.out.println("worker2 wait...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
注意:这里有两点需要非常注意的。
- 在Lock代码块中不能错误使用condition.wait()和condition.notify(),这是隐性锁对应同步对象的等待和唤醒机制。若错误使用会抛出java.lang.IllegalMonitorStateException异常,wait()和notify()无法找到对应synchronized修饰的同步代码。
- condition对象的await()和signal()一定要在Lock代码块中,否则会抛出java.lang.IllegalMonitorStateException异常。
2、ReentrantLock中的公平锁与非公平锁
ReentrantLock有两个子类实现,分别为公平锁和非公平锁。
首先明确一个前提,在ReentrantLock中,每当一个线程去请求锁时,若此时锁被其他线程持有。当前线程会被加入到CLH等待队列中,而公平锁和非公平锁的区别就在于这些等待线程请求锁的过程。
公平锁:线程遵循FIFO原则,当锁释放时,等待队列中先等待的线程先获得锁,后面线程继续排队。
非公平锁:线程遵循先到先得原则,当锁释放时,等待队列中所有线程都尝试请求锁,最快的线程获得锁,其他线程继续等待。
这就有点像去柜台买火车票,公平锁就像去排队购买,按照队伍顺序购买;非公平锁就像大家一拥而上,谁力气大动作快就可以先在窗口购票。
根据ReentrantLock构造函数可以看出默认的是非公平锁,这里可以猜测,作者在不清楚线程占用锁时长的情况下,倾向于以效率优先。
public ReentrantLock() {
sync = new NonfairSync();
}
非公平锁相对于公平锁的优缺点:
- 非公平锁比公平锁的效率更高,加锁解锁次数越多,效率相差越大。
- 由于非公平锁是所有线程都在抢占锁,可能会出现线程等待很久或者线程饿死。
公平锁和非公平锁执行顺序观察:根据日志很明显的可以看出公平锁是根据启动顺序与获取锁顺序相同。非公平锁启动与获取锁顺序差异较大。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockClient {
public static void main(String[] args) {
// ReentrantLock lock = new ReentrantLock(true);//公平锁
ReentrantLock lock = new ReentrantLock(false);//非公平锁
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new Worker(lock));
t.start();
}
}
static class Worker implements Runnable {
private Lock lock;
public Worker(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "启动");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得锁");
} finally {
lock.unlock();
}
}
}
}
公平锁获取日志
Thread-0启动
Thread-2启动
Thread-1启动
Thread-0获得锁
Thread-5启动
Thread-4启动
Thread-6启动
Thread-3启动
Thread-2获得锁
Thread-1获得锁
Thread-7启动
Thread-8启动
Thread-5获得锁
Thread-9启动
Thread-4获得锁
Thread-6获得锁
Thread-3获得锁
Thread-7获得锁
Thread-8获得锁
Thread-9获得锁
非公平锁获取日志
Thread-2启动
Thread-1启动
Thread-0启动
Thread-1获得锁
Thread-2获得锁
Thread-7启动
Thread-6启动
Thread-0获得锁
Thread-5启动
Thread-7获得锁
Thread-3启动
Thread-4启动
Thread-8启动
Thread-9启动
Thread-6获得锁
Thread-5获得锁
Thread-3获得锁
Thread-4获得锁
Thread-8获得锁
Thread-9获得锁
公平锁和非公平锁的效率比对
下面的代码启动了20个线程,每个线程对race变量进行了10w次的自增,每次都包含一次锁获取(lock())和释放(unlock())。一共进行了20*10w=200w次锁获取/释放。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockClient {
public static Integer threadCount = 20;
public static CountDownLatch cdl = new CountDownLatch(threadCount);
public static ReentrantLock lock = new ReentrantLock(true);//公平锁
// public static ReentrantLock lock = new ReentrantLock(false);//非公平锁
public static Integer race = 0;
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
Thread t = new Thread(new Worker(lock, cdl));
t.start();
}
cdl.await();
long end = System.currentTimeMillis() - start;
System.out.println(end);
}
public static void incrRace() {
lock.lock();
try {
race++;
} finally {
lock.unlock();
}
}
static class Worker implements Runnable {
private Lock lock;
private CountDownLatch cdl;
public Worker(Lock lock, CountDownLatch cdl) {
this.lock = lock;
this.cdl = cdl;
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
incrRace();
}
cdl.countDown();
}
}
}
公平锁耗时5915毫秒
非公平锁耗时58毫秒
公平锁和非公平锁在200w的锁获取/释放中性能相差100倍。
3、ReentrantLock的可重入锁
(1)可重入锁:任意线程在获取到锁之后,再次获取该锁而不会被该锁阻塞。这样的锁称之为可重入锁。
可以使用 lock.getHoldCount() 方法统计锁被重入次数。
下面代码示例展示了如何获取可重入锁的重入次数
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockClient {
public static ReentrantLock lock = new ReentrantLock();
public static Integer count = 10;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Worker());
t.start();
}
public static void getLock() {
lock.lock();
count--;
try {
//统计锁被重入次数
System.out.println("锁重入次数:" + lock.getHoldCount());
if (count > 0) {
getLock();
}
} finally {
lock.unlock();
}
}
static class Worker implements Runnable {
@Override
public void run() {
getLock();
}
}
}
上面的那段代码启用了一个新线程来重入锁,下面的代码直接在Main线程中重入锁,更加直观。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockClient {
public static void main(String[] args) {
Worker worker = new Worker();
worker.lockService();
}
static class Worker {
public ReentrantLock lock = new ReentrantLock();
public Integer count = 10;
public void lockService() {
lock.lock();
count--;
try {
System.out.println("锁重入次数:" + lock.getHoldCount());
if (count > 0) {
lockService();
}
} finally {
lock.unlock();
}
System.out.println("释放后:" + lock.getHoldCount());
}
}
}
执行结果:
(2)synchronized也具有可重入性
同一线程在调用类中其他synchronized方法/代码块或调用父类的synchronized方法/代码块都不会被该锁阻塞,就是说同一线程对同一对象锁是可重入的。