JAVA多线程提高八:线程锁技术

时间:2023-02-03 23:04:51

前面我们讲到了synchronized;那么这节就来将lock的功效。

一、locks相关类

锁相关的类都在包java.util.concurrent.locks下,有以下类和接口:

|---AbstractOwnableSynchronizer
|---AbstractQueuedLongSynchronizer
|---AbstractQueuedSynchronizer
|---Condition
|---Lock
|---LockSupport
|---ReadWriteLock
|---ReentrantLock
|---ReentrantReadWriteLock

接口摘要:

接口 摘要
Condition Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。
Lock Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
ReadWriteLock ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。

类摘要:

摘要
AbstractOwnableSynchronizer 可以由线程以独占方式拥有的同步器。
AbstractQueuedLongSynchronizer 以 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。
AbstractQueuedSynchronizer 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。
LockSupport 用来创建锁和其他同步类的基本线程阻塞原语。
ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantReadWriteLock 支持与 ReentrantLock 类似语义的 ReadWriteLock 实现。
ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.readLock() 方法返回的锁。
ReentrantReadWriteLock.WriteLock ReentrantReadWriteLock.writeLock() 方法返回的锁。

二、synchronized与lock

synchronized对比lock:
1、synchronized是Java语言的关键字属于内置特性,Lock是一个类
2、使用synchronized不需要用户去手动释放锁,使用Lock需要在finally手动释放锁,不然容易造成线程死锁

详细对比见下面的表格:

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
性能 少量同步 大量同步

三、常用类

1.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()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

lock()

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){ }finally{
lock.unlock(); //释放锁
}

tryLock()、tryLock(long time, TimeUnit unit)

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

所以,一般情况下通过tryLock来获取锁时是这样使用的:

Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){ }finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}

lockInterruptibly()

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
}
finally {
lock.unlock();
}
}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。 
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。 
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

2.锁类型

Java中存在以下几种锁:

  • 可重入锁:在执行对象中所有同步方法不用再次获得锁(可看一个使用示例)

  • 可中断锁:在等待获取锁过程中可中断

  • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

  • 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

可重入锁ReentrantLock

ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。

例子1:lock()的使用 
可以类似于Synchronized的用法,定义一个类,新建一个该类的对象用于线程间同步,在类里面定义锁的对象。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class LockTest { public static void main(String[] args) {
new LockTest().init();
} private void init(){
final Outputer outputer = new Outputer();
new Thread(new Runnable(){
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
outputer.output("zhangxiaoxiang");
} }
}).start(); new Thread(new Runnable(){
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
outputer.output("lihuoming");
} }
}).start(); } static class Outputer{
Lock lock = new ReentrantLock();
public void output(String name){
lock.lock();
try{
System.out.println(name);
}finally{
lock.unlock();
}
}
}
}

输出:

zhangxiaoxiang
lihuoming
lihuoming
zhangxiaoxiang
zhangxiaoxiang
lihuoming
...

注意:输出的字符串顺序不定,个数也不定。

例子2:tryLock()的使用

这里相比例子1只修改了Outputer类,main方法一样。

static class Outputer{
Lock lock = new ReentrantLock();
public void output(String name){
if (lock.tryLock()) {
try{
System.out.println(name + "得到锁");
}finally{
lock.unlock();
System.out.println(name + "释放锁");
}
} else {
System.out.println(name + "获取锁失败");
}
}
}

输出:

lihuoming得到锁
zhangxiaoxiang获取锁失败
lihuoming释放锁
zhangxiaoxiang得到锁
zhangxiaoxiang释放锁
lihuoming得到锁
lihuoming释放锁
...

注意:输出的字符串顺序不定,个数也不定。

例子3:lockInterruptibly()的使用 

执行lockInterruptibly()方法的方法中,需要将异常InterruptedException抛出,在等待锁的线程可调用interrupt()方法中断,即可触发异常InterruptedException,然后可以在catch中执行相应的操作。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class LockTest { public static void main(String[] args) {
new LockTest().init();
} private void init(){
final Outputer outputer = new Outputer();
Thread thread1 = new Thread(new Runnable(){
public void run() {
String name = "zhangxiaoxiang";
try {
Thread.sleep(10);
outputer.output(name);
} catch (InterruptedException e) {
System.out.println(name + "被中断");
} }
});
thread1.start(); Thread thread2 = new Thread(new Runnable(){
public void run() {
String name = "lihuoming";
try {
Thread.sleep(10);
outputer.output(name);
} catch (InterruptedException e) {
System.out.println(name + "被中断");
} }
});
thread2.start(); try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt(); } static class Outputer{
Lock lock = new ReentrantLock();
//将InterruptedException抛出
public void output(String name) throws InterruptedException {
System.out.println(name + "试图执行output方法");
lock.lockInterruptibly();
try{
System.out.println(name + "得到锁");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break;
}
}finally{
System.out.println(name + "执行了finally");
lock.unlock();
System.out.println(name + "释放锁"); }
}
}
}

输出:

zhangxiaoxiang试图执行output方法
zhangxiaoxiang得到锁
lihuoming试图执行output方法
lihuoming被中断

运行之后,发现thread2能够被正确中断

在jdk源码中的一个运用就是类ArrayBlockingQueue的方法。该方法中有以下几点注意: 
1、使用lock.lockInterruptibly()需抛出异常InterruptedException 
2、使用了Condition 
3、在finally中关闭锁

/**
* Inserts the specified element at the tail of this queue, waiting
* up to the specified wait time for space to become available if
* the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
/** notFull是一个Condition对象,
** Condition for waiting puts
** private final Condition notFull;
*/
nanos = notFull.awaitNanos(nanos);
}
insert(e);
return true;
} finally {
lock.unlock();
}
}

3.读写锁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();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。ReentrantReadWriteLock实现了ReadWriteLock接口。

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

读写锁,分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,由JVM控制。

注意:此锁最多支持 65535 个递归写入锁和 65535 个读取锁。试图超出这些限制将导致锁方法抛出 Error。

下面给出构造函数和常用方法的简要说明:

类ReentrantReadWriteLock

  • ReentrantReadWriteLock(boolean fair): 使用给定的公平策略创建一个新的 ReentrantReadWriteLock。
  • ReentrantReadWriteLock():使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock

类ReentrantReadWriteLock的方法

返回类型 方法
ReentrantReadWriteLock.ReadLock readLock() 返回用于读取操作的锁
ReentrantReadWriteLock.WriteLock writeLock() 返回用于写入操作的锁

下面给出示例代码:

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockTest {
public static void main(String[] args) {
final Queue3 q3 = new Queue3();
for(int i=0;i<3;i++)
{
final Thread readThread = new Thread() {
public void run() {
while (true) {
q3.get();
}
}
};
readThread.setName("read-"+i);
readThread.start(); final Thread writeThread = new Thread(){
public void run(){
while(true){
q3.put(new Random().nextInt(10000));
}
} };
writeThread.setName("write-"+i);
writeThread.start();
} }
} class Queue3{
private Object data = null;
ReadWriteLock rwl = new ReentrantReadWriteLock();
public void get(){
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " be ready to read data!");
Thread.sleep((long)(Math.random()*1000));
System.out.println(Thread.currentThread().getName() + " have read data :" + data);
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
rwl.readLock().unlock();
}
} public void put(Object data){ rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " be ready to write data!");
Thread.sleep((long)(Math.random()*1000));
this.data = data;
System.out.println(Thread.currentThread().getName() + " have write data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
rwl.writeLock().unlock();
}
}
}

输出:

read-0 be ready to read data!
read-1 be ready to read data!
read-0 have read data :null
read-1 have read data :null
write-1 be ready to write data!
write-1 have write data: 3713
write-1 be ready to write data!
write-1 have write data: 3420

对输出结果进行分析:be ready to read data!have read data并不是先后出现的,中间可以夹着be ready to read data!说明读锁之间不互斥。

面试题: 
缓存系统:取数据,需调用public Object getData(String key)方法,先检查缓存有没有这个数据,如果有就直接返回,如果没有,就从数据库中查找这个数,然后写入缓存。 
如果使用synchronized对getData加锁,那么getData方法只能被一个读线程执行,其他读操作就得等待,这里可以使用一个读写锁,只有在写的时候才需要互斥

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class CacheDemo { private Map<String, Object> cache = new HashMap<String, Object>();
public static void main(String[] args) {
// TODO Auto-generated method stub } private ReadWriteLock rwl = new ReentrantReadWriteLock();
public Object getData(String key){
rwl.readLock().lock();
Object value = null;
try{
value = cache.get(key);
if(value == null){
rwl.readLock().unlock();
rwl.writeLock().lock();
try{
//再次进行判断,防止多个写线程堵在这个地方重复写
if(value==null){
value = "aaaa"; //设置新值
}
}finally{
rwl.writeLock().unlock();
}
//设置完成 释放写锁,恢复读写状态
rwl.readLock().lock();
}
}finally{
rwl.readLock().unlock();
}
return value;
}
}

其他更多有关ReentrantReadWriteLock后面补充。

4.Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,,阻塞队列实际上是使用了Condition来模拟线程间协作。

synchronized常与wait、notify等方法使用,Condition常与await、signal等方法使用。

  • Condition是个接口,基本的方法就是await()和signal()方法;
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

Conditon中的await()对应Object的wait(); 
Condition中的signal()对应Object的notify(); 
Condition中的signalAll()对应Object的notifyAll()。

Condition中的long awaitNanos(long nanosTimeout) throws InterruptedException方法传入一个等待的微秒时间,该方法返回了所剩毫微秒数的一个估计值,以等待所提供的 nanosTimeout 值的时间,如果超时,则返回一个小于等于 0 的值。可以用此值来确定在等待返回但某一等待条件仍不具备的情况下,是否要再次等待,以及再次等待的时间。此方法的典型用法采用以下形式(上面讲ArrayBlockingQueue的public E poll(long timeout, TimeUnit unit)方法中就用到这个方法):

synchronized boolean aMethod(long timeout, TimeUnit unit) {
long nanosTimeout = unit.toNanos(timeout);
while (!conditionBeingWaitedFor) {
if (nanosTimeout > 0)
nanosTimeout = theCondition.awaitNanos(nanosTimeout);
else
return false;
}
// ...
}

代码示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class ConditionCommunication { public static void main(String[] args) {
final Business business = new Business();
new Thread(
new Runnable() {
public void run() {
for (int i = 1; i <= 50; i++) {
business.sub(i);
}
}
}
).start(); for (int i = 1; i <= 50; i++) {
business.main(i);
}
} static class Business {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private boolean bShouldSub = true; public void sub(int i) {
lock.lock();
try {
while (!bShouldSub) {
try {
condition.await();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 10; j++) {
System.out.println("sub thread sequence of " + j + ",loop of " + i);
}
bShouldSub = false;
condition.signal();
} finally {
lock.unlock();
}
} public void main(int i) {
lock.lock();
try {
while (bShouldSub) {
try {
condition.await();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 100; j++) {
System.out.println("main thread sequence of " + j + ",loop of " + i);
}
bShouldSub = true;
condition.signal();
} finally {
lock.unlock();
}
} }
}

同样:await()方法需要放在while循环中。 
更多参考:

与传统的同步对比可参考:线程间协作的两种方式:wait、notify、notifyAll和Condition

参考

详解synchronized与Lock的区别与使用

Java并发编程:Lock

JAVA多线程提高八:线程锁技术的更多相关文章

  1. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep&lpar;&rpar;和wait&lpar;&rpar;方法的区别 为什么wait&lpar;&rpar;&comma;notify&lpar;&rpar;&comma;notifyAll&lpar;&rpar;等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  2. java多线程系列&lpar;八&rpar;---CountDownLatch和CyclicBarrie

    CountDownLatch 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 java多线 ...

  3. Java多线程6:Synchronized锁代码块(this和任意对象)

    一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...

  4. Java多线程系列--&OpenCurlyDoubleQuote;JUC线程池”02之 线程池原理&lpar;一&rpar;

    概要 在上一章"Java多线程系列--“JUC线程池”01之 线程池架构"中,我们了解了线程池的架构.线程池的实现类是ThreadPoolExecutor类.本章,我们通过分析Th ...

  5. Java多线程系列--&OpenCurlyDoubleQuote;JUC线程池”03之 线程池原理&lpar;二&rpar;

    概要 在前面一章"Java多线程系列--“JUC线程池”02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包括:线程池示例参考代 ...

  6. Java多线程5:Synchronized锁机制

    一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...

  7. Java多线程——进程和线程

    Java多线程——进程和线程 摘要:本文主要解释在Java这门编程语言中,什么是进程,什么是线程,以及二者之间的关系. 部分内容来自以下博客: https://www.cnblogs.com/dolp ...

  8. Java多线程并发02——线程的生命周期与常用方法,你都掌握了吗

    在上一章,为大家介绍了线程的一些基础知识,线程的创建与终止.本期将为各位带来线程的生命周期与常用方法.关注我的公众号「Java面典」了解更多 Java 相关知识点. 线程生命周期 一个线程不是被创建了 ...

  9. Java多线程系列--&OpenCurlyDoubleQuote;JUC线程池”06之 Callable和Future

    概要 本章介绍线程池中的Callable和Future.Callable 和 Future 简介示例和源码分析(基于JDK1.7.0_40) 转载请注明出处:http://www.cnblogs.co ...

随机推荐

  1. 导入HDFS的数据到Hive

    1. 通过Hive view CREATE EXTERNAL TABLE if not exists finance.json_serde_optd_table ( retCode string, r ...

  2. CSS3中的box-shadow

    语法: box-shadow: h-shadow v-shadow blur spread color inset; box-shadow 向框添加一个或多个阴影.该属性是由逗号分隔的阴影列表,每个阴 ...

  3. KVO&lpar;键-值观察&rpar;

    // 1.键-值观察 // 2.它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知. // 3.符合KVC(Key-ValuedCoding)机制的对象才可以使用KVO // 4.实现过 ...

  4. Java的常用包

    java.lang:  这个包下包含了Java语言的核心类,如String.Math.Sytem和Thread类等,使用这个包无需使用import语句导入,系统会自动导入这个包中的所有类. java. ...

  5. The openssl extension is required for SSL&sol;TLS protection but is not available

    今天使用composer update发现报错:The openssl extension is required for SSL/TLS protection but is not availabl ...

  6. Hanlp1&period;7版本的新增功能一览

    Hanlp1.7版本在去年下半年的时候就随大快的DKH1.6版本同时发布了,截至目前1.7大版本也更新到了1.7.1了.本篇分别就1.7.0和1.7.1中新增的功能做一个简单的汇总介绍. HanLP ...

  7. &lbrack;每天解决一问题系列 - 0013&rsqb; 如何修改WiX Burn内置的窗口

    问题描述: 我们产品的burn安装包仅支持.net 3.5 sp1以上,在只有.net 2.0的机器上会给用户弹一个窗口,告诉用户为什么不能够安装的原因.本来burn已经内置了,但是在日文操作系统下, ...

  8. 修改postfix smtp端口,防止公网扫描浪费你的服务器流量

    邮件服务器的默认发送邮件端口是25,一些ISP会*25端口防止垃圾邮件的发送,这样就导致不能使用Foxmail.outlook等邮件客户端发送邮件.修改默认smtp端口就可以解决这个问题.下面的方法 ...

  9. Service Mesh扫盲

    原文:http://www.infoq.com/cn/news/2017/12/why-service-mesh 摘要: 对 Service Mesh 的理解?它的出现最终是为了解决什么问题? Ser ...

  10. Python学习之环境搭建及模块引用

    这是我学习Python过程积累的经验和踩过的坑,希望学习Python的新手们能尽量避免,以免不必要的时间浪费.今天也是我第一次接触Python. 基础语法看了两个晚上,所以如果没看的朋友们,抽时间先看 ...