Java多线程:线程间通信之Lock
Java 5 之后,Java在内置关键字sychronized的基础上又增加了一个新的处理锁的方式,Lock类。
由于在Java线程间通信:volatile与sychronized中,我们已经详细的了解了synchronized,所以我们现在主要介绍一下Lock,以及将Lock与synchronized进行一下对比。
1. synchronized的缺陷
synchronized修饰的代码只有获取锁的线程才能够执行,其他线程只能等待该线程释放锁。一个线程释放锁的情况有以下方式:
- 获取锁的线程完成了synchronized修饰的代码块的执行。
- 线程执行时发生异常,JVM自动释放锁。
我们在Java多线程的生命周期,实现与调度中谈过,锁会因为等待I/O,sleep()方法等原因被阻塞而不释放锁,此时如果线程还处于用synchronized修饰的代码区域里,那么其他线程只能等待,这样就影响了效率。因此Java提供了Lock来实现另一个机制,即不让线程无限期的等待下去。
思考一个情景,当多线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,但读操作和读操作不会有冲突。如果使用synchronized来修饰的话,就很可能造成多个读操作无法同时进行的可能(如果只用synchronized修饰写方法,那么可能造成读写冲突,如果同时修饰了读写方法,则会有读读干扰)。此时就需要用到Lock,换言之Lock比synchronized提供了更多的功能。
使用Lock需要注意以下两点:
- Lock不是语言内置的,synchronized是Java关键字,为内置特性,Lock是一个类,通过这个类可以实现同步访问。
- 采用synchronized时我们不需要手动去控制加锁和释放,系统会自动控制。而使用Lock类,我们需要手动的加锁和释放,不主动释放可能会造成死锁。实际上Lock类的使用某种意义上讲要比synchronized更加直观。
2. Lock类接口设计
Lock类本身是一个接口,其方法如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
下面依次讲解一下其中各个方法。
- lock() 方法使用最多,作用是用于获取锁,如果锁已经被其他线程获得,则等待。
通常情况下,lock使用以下方式去获取锁:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
- lockInterruptibly() 和lock()的区别是lockInterruptibly()锁定的线程处于等待状态时,允许线程的打断操作,线程使用Thread.interrupt()打断该线程后会直接返回并抛出一个InterruptException();lock()方法锁定对象时如果在等待时检测到线程使用Thread.interrupt(),仍然会继续尝试获取锁,失败则继续休眠,只是在成功获取锁之后在把当前线程置为interrupt状态。也就使说,当两个线程同时通过lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
因此,lockInterruptibly()方法必须实现catch(InterruptException e)代码块。常见使用方式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
- tryLock() 和lock()最大的不同是具有返回值,或者说,它不去等待锁。如果它成功获取锁,那么返回true;如果它无法成功获取锁,则返回false。
通常情况下,tryLock使用方式如下:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
- tryLock(long time, TimeUnit unit) 则是介于二者之间,用户设定一个等待时间,如果在这个时间内获取到了锁,则返回true,否则返回false结束。
- unlock() 从上面的代码里我们也看到,unlock()一般放在异常处理操作的finally字符控制的代码块中。我们要记得Lock和sychronized的区别,防止产生死锁。
- newCondition() 该方法我们放到后面讲。
3. ReentrantLock可重入锁
3.1. ReentrantLock概述
ReentrantLock译为“可重入锁”,我们在Java多线程:synchronized的可重入性中已经明白了什么是可重入以及理解了synchronized的可重入性。ReentrantLock是唯一实现Lock接口的类。
3.2. ReentrantLock使用
考虑到以下情景,一个仅出售双人票的演唱会进行门票出售,有三个售票口同时进行售票,买票需要100ms时间,每张票出票需要100ms时间。该如何设计这个情景?
package com.cielo.LockTest;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
/**
* Created by 63289 on 2017/4/10.
*/
class SoldTicket implements Runnable {
Lock lock = new ReentrantLock();//使用可重入锁
private volatile Integer ticket;//保证从主内存获取
SoldTicket(Integer ticket) {
this.ticket = ticket;//提供票数
}
private void sold() {
lock.lock();//锁定操作放在try代码块外
try {
if (ticket <= 0) return;//当ticket==2时可能有多个线程进入sold方法,一个线程运行后另外两个线程需要退出。
sleep(200);//买票0.1s,出票0.1s
--ticket;
System.out.println("The first ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");//获取线程id来识别出票站。
sleep(100);//出票0.1s
--ticket;
System.out.println("The second ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void run() {
while (ticket > 0) {
sold();
}
}
}
public class LockTest {
public static void main(String[] args) {
SoldTicket soldTicket = new SoldTicket(20);
new Thread(soldTicket).start();
new Thread(soldTicket).start();
new Thread(soldTicket).start();
}
}
上面这段代码结果如下:
The first ticket is sold by 11, 19 tickets leave.
The second ticket is sold by 11, 18 tickets leave.
The first ticket is sold by 13, 17 tickets leave.
The second ticket is sold by 13, 16 tickets leave.
The first ticket is sold by 13, 15 tickets leave.
The second ticket is sold by 13, 14 tickets leave.
The first ticket is sold by 12, 13 tickets leave.
The second ticket is sold by 12, 12 tickets leave.
The first ticket is sold by 11, 11 tickets leave.
The second ticket is sold by 11, 10 tickets leave.
The first ticket is sold by 11, 9 tickets leave.
The second ticket is sold by 11, 8 tickets leave.
The first ticket is sold by 13, 7 tickets leave.
The second ticket is sold by 13, 6 tickets leave.
The first ticket is sold by 13, 5 tickets leave.
The second ticket is sold by 13, 4 tickets leave.
The first ticket is sold by 13, 3 tickets leave.
The second ticket is sold by 13, 2 tickets leave.
The first ticket is sold by 13, 1 tickets leave.
The second ticket is sold by 13, 0 tickets leave.
如果我们不对售票操作进行锁定,则会有以下几个问题:
- 出售第一张票后其他机器出了另一张票,导致票没有成对卖。
- 已经无票后仍有机器出票造成混乱。
显然,本题的情景用synchronized也可以很容易的实现,实际上Lock有别于synchronized的主要点是lockInterruptibly()和tryLock()这两个可以对锁进行控制的方法。
4. ReadWriteLock读写锁
4.1. ReadWriteLock接口
回到开头synchronized缺陷的介绍,实际上,Lock接口的重要衍生接口ReadWriteLock即是解决这一问题。ReadWriteLock定义很简单,仅有两个接口:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
即是它只提供了readLock()和writeLock()两个操作,这两个操作均返回一个Lock类的实例。两个操作一个获取读锁,一个获取写锁,将读写分开进行操作。ReadWriteLock将读写的锁分开,可以让多个读操作并行,这就大大提高了效率。使用ReadWriteLock时,用读锁去控制读操作,写锁控制写操作,进而实现了一个可以在如下的大量读少量写且读者优先的情景运行的锁。
4.2. ReentrantReadWriteLock可重入读写锁
ReentrantReadWriteLock是ReadWriteLock的唯一实例。同时提供了很多操作方法。ReentratReadWriteLock接口实现的读锁写锁进入有如下要求:
4.2.1. 线程进入读锁的要求
- 没有其他线程的写锁。
- 没有锁请求 或 调用写请求的线程正是该线程。
4.2.2. 线程进入写锁的要求
- 没有其他线程的读锁。
- 没有其他线程的写锁。
4.2.3. 读写锁使用示例
private SomeClass someClass;//资源
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//创建锁
private final Lock readLock = readWriteLock.readLock();//读锁
private final Lock writeLock = readWriteLock.writeLock();//写锁
//读方法
readLock.lock();
try {
result = someClass.someMethod();
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
//写方法,产生新的SomeClass实例tempSomeClass
writeLock.lock();
try{
this.someClass = tempSomeClass;//更新
}catch (Exception e) {
e.printStackTrace();
} finally{
writeLock.unlock();
}
5. 公平锁
公平锁即当多个线程等待的一个资源的锁释放时,线程不是随机的获取资源而是等待时间最久的线程获取资源(FIFO)。Java中,synchronized是一个非公平锁,无法保证锁的获取顺序。ReentrantLock和ReentrantReadWriteLock默认也是非公平锁,但可以设置成公平锁。我们前面的实例中初始化ReentrantLock和ReentrantReadWriteLock时都是无参数的。实际上,它们提供一个默认的boolean变量fair,为true则为公平锁,为false则为非公平锁,默认为false。因此,当我们想将其实现为公平锁时,仅需要初始化时赋值true。即:
Lock lock = new ReentrantLock(true);
考虑前面卖票的实例,如果改为公平锁(尽管这和情景无关),则结果输出非常整齐如下:
The first ticket is sold by 11, 19 tickets leave.
The second ticket is sold by 11, 18 tickets leave.
The first ticket is sold by 12, 17 tickets leave.
The second ticket is sold by 12, 16 tickets leave.
The first ticket is sold by 13, 15 tickets leave.
The second ticket is sold by 13, 14 tickets leave.
The first ticket is sold by 11, 13 tickets leave.
The second ticket is sold by 11, 12 tickets leave.
The first ticket is sold by 12, 11 tickets leave.
The second ticket is sold by 12, 10 tickets leave.
The first ticket is sold by 13, 9 tickets leave.
The second ticket is sold by 13, 8 tickets leave.
The first ticket is sold by 11, 7 tickets leave.
The second ticket is sold by 11, 6 tickets leave.
The first ticket is sold by 12, 5 tickets leave.
The second ticket is sold by 12, 4 tickets leave.
The first ticket is sold by 13, 3 tickets leave.
The second ticket is sold by 13, 2 tickets leave.
The first ticket is sold by 11, 1 tickets leave.
The second ticket is sold by 11, 0 tickets leave.
6. Lock和synchronized的选择
- synchronized是内置语言实现的关键字,Lock是为了实现更高级锁功能而提供的接口。
- Lock实现了tryLock等接口,线程可以不用一直等待。
- synchronized发生异常时自动释放占有的锁,Lock需要在finally块中手动释放锁。因此从安全性角度讲,既可以用Lock又可以用synchronized时(即不需要锁的更高级功能时)使用synchronized更保险。
- Lock可以通过lockInterruptibly()接口实现可中断锁。
- 由于Lock提供了时间限制同步,可被打断同步等机制,线程激烈竞争时Lock的性能远优于synchronized,即有大量线程时推荐使用Lock。在竞争不激烈时,由于synchronized的编译器优化更好,性能更佳。
- ReentrantReadWriteLock实现了封装好的读写锁用于大量读少量写读者优先情景解决了synchronized读写情景难以实现问题。
7. 参考文章
Java多线程:线程间通信之Lock的更多相关文章
- Java多线程——线程间通信
Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...
- Java并发——线程间通信与同步技术
传统的线程间通信与同步技术为Object上的wait().notify().notifyAll()等方法,Java在显示锁上增加了Condition对象,该对象也可以实现线程间通信与同步.本文会介绍有 ...
- 多线程-线程间通信-多生产者多消费者问题(JDK1.5后Lock,Condition解决办法及开发中代码范例)
1 package multithread4; 2 3 import java.util.concurrent.locks.Condition; 4 import java.util.concurre ...
- java多线程-线程间协作
大纲: wait.notify.notifyAll Condition 生产消费者 一.wait.notify.notifyAll wait.notify.notifyAll是Object的本地fin ...
- 多线程-线程间通信-多生产者多消费者问题解决(notifyAll)
1 package multithread4; 2 3 /* 4 * 生产者,消费者. 5 * 6 * 多生产者,多消费者的问题. 7 * 8 * if判断标记,只有一次,会导致不该运行的线程运行了. ...
- 多线程 线程间通信 wait,notify
1. 方法wait锁释放,notify()锁不释放
- Java多线程:线程间通信之volatile与sychronized
由前文Java内存模型我们熟悉了Java的内存工作模式和线程间的交互规范,本篇从应用层面讲解Java线程间通信. Java为线程间通信提供了三个相关的关键字volatile, synchronized ...
- Java 里如何实现线程间通信
正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点:thread.join(), object.w ...
- Java 如何实现线程间通信
正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点: thread.join(), object. ...
随机推荐
- Linux多线程实践(1) --线程理论
线程概念 在一个程序里的一个执行路线就叫做线程(thread).更准确的定义是:线程是"一个进程内部的控制序列/指令序列"; 一切进程至少有一个执行线程; 进程 VS. 线程 ...
- BottomSheet底部动作条使用
底部动作条 底部动作条(Bottom Sheets)是一个从屏幕底部边缘向上滑出的一个面板,使用这种方式向用户呈现一组功能.底部动作条呈现了简单.清晰.无需额外解释的一组操作. 使用环境 底部动作条( ...
- 【翻译】在Sencha应用程序中使用插件和混入
原文:Using Plugins and Mixins in Your Sencha Apps 概述 当扩展一个框架类的功能的时候,通常都会直接将新功能写入派生类,然而,如果所需的同一功能存在于多个组 ...
- (五十八)NSObject实现多线程、自动释放池的补充
模拟一个图片下载的场景,图片的下载需要2s,在这期间为了保证程序的流畅,应该把图片的下载放在子线程中进行. 使用NSObject的方法performSelectorInBackground方法即可实现 ...
- MySQL学习笔记_5_SQL语言的设计与编写(上)
SQL语言的设计与编写(上) 一.SQL语句分类 数据定义语言(DDL): 用于定义和管理数据对象,包括数据库.数据表.视图.索引等.例如:CREATE.DROP.ALTER等语句. 数据操作语言(D ...
- MySQL学习笔记_4_MySQL创建数据表(下)
MySQL创建数据表(下) 五.数据表类型及存储位置 1.MySQL与大多数数据库不同,MySQL有一个存储引擎概念.MySQL可以针对不同的存储需求选择不同的存储引擎. 2. showengines ...
- 仿滴滴抢单倒计时的Demo
滴滴里面有一个下单完成之后等待界面的倒计时转圈的视图... 就是这个... 原理: 通过CAShapeLayer层添加到自己自定义的视图layer上... 设置ShapeLayer的path... 他 ...
- 01_MUI之Boilerplate中:HTML5示例,动态组件,自定义字体示例,自定义字体示例,图标字体示例
1安装HBuilder5.0.0,安装后的界面截图如下: 2 按照https://www.muicss.com/docs/v1/css-js/boilerplate-html中的说明,创建上图的 ...
- android驱动例子(LED灯控制)
本例子,讲述在android2.1上完全自已开发一个驱动去控制硬件口并写应用测试该驱动,通过这样一个例子,解析android下的驱动开发流程的应用调用流程,可以说是很好的入门引导 要达到的效果:通过a ...
- 滑动UITableViewCell出现多个按钮
iOS > = 5.0使用第三方效果图 iOS> = 8.0使用系统方法效果图 MGSwipeTableCell(Github上的三方库)- iOS >= 5.0 直接使用比较简单 ...