JAVA多线程学习十一-线程锁技术
前面我们讲到了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
参考
作者:guanbin —— 纵码万里千山
出处:https://www.cnblogs.com/guanbin-529/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。