java并发编程专题(五)----详解(JUC)ReentrantLock

上一节我们了解了Lock接口的一些简单的说明,知道Lock锁的常用形式,那么这节我们正式开始进入JUC锁(java.util.concurrent包下的锁,简称JUC锁)。下面我们来看一下Lock最常用的实现类ReentrantLock。

1.ReentrantLock简介

由单词意思我们可以知道这是可重入的意思。那么可重入对于锁而言到底意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

1.1公平锁与非公平锁

我们查看ReentrantLock的源码可以看到无参构造函数是这样的:

public ReentrantLock() {
 sync = new NonfairSync();
}

NonfairSync()方法为一个非公平锁的实现方法,另外Reentrantlock还有一个有参的构造方法:

public ReentrantLock(boolean fair) {
 sync = fair ? new FairSync() : new NonfairSync();
}

它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许直接获取锁,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。

为什么我们不让所有的锁都公平呢?毕竟,公平是好事,不公平是不好的,不是吗?(当孩子们想要一个决定时,总会叫嚷“这不公平”。我们认为公平非常重要,孩子们也知道。)在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为 false ,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。

下面我们先来看一个例子:

public class TestReentrantLock implements Runnable{

 ReentrantLock lock = new ReentrantLock();

 public void get() {
  lock.lock();
  System.out.println(Thread.currentThread().getId());
  set();
  lock.unlock();
 }

 public void set() {
  lock.lock();
  System.out.println(Thread.currentThread().getId());
  lock.unlock();
 }

 @Override
 public void run() {
  get();
 }

 public static void main(String[] args) {
  TestReentrantLock ss = new TestReentrantLock();
  new Thread(ss).start();
  new Thread(ss).start();
  new Thread(ss).start();
 }
 }

运行结果:

10
10
12
12
11
11

Process finished with exit code 0

由结果我们可以看出同一个线程进入了同一个ReentrantLock锁两次。

2.condition条件变量

我们知道根类 Object 包含某些特殊的方法,用来在线程的 wait() 、 notify() 和 notifyAll() 之间进行通信。那么为了在对象上 wait 或 notify ,您必须持有该对象的锁。就像 Lock 是同步的概括一样, Lock 框架包含了对 wait 和 notify 的概括,这个概括叫作 条件(Condition)。 Condition 的方法与 wait 、 notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和signalAll ,因为它们不能覆盖 Object 上的对应方法。

首先我们来计算一道题:
我们要打印1到9这9个数字,由A线程先打印1,2,3,然后由B线程打印4,5,6,然后再由A线程打印7,8,9. 这道题有很多种解法,我们先用Object的wait,notify方法来实现:

public class WaitNotifyDemo {
 private volatile int val = 1;

 private synchronized void printAndIncrease() {
  System.out.println(Thread.currentThread().getName() +"prints " + val);
  val++;
 }

 // print 1,2,3 7,8,9
 public class PrinterA implements Runnable {
  @Override
  public void run() {
  while (val <= 3) {
   printAndIncrease();
  }

  // print 1,2,3 then notify printerB
  synchronized (WaitNotifyDemo.this) {
   System.out.println("PrinterA printed 1,2,3; notify PrinterB");
   WaitNotifyDemo.this.notify();
  }

  try {
   while (val <= 6) {
   synchronized (WaitNotifyDemo.this) {
    System.out.println("wait in printerA");
    WaitNotifyDemo.this.wait();
   }
   }
   System.out.println("wait end printerA");
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  while (val <= 9) {
   printAndIncrease();
  }
  System.out.println("PrinterA exits");
  }
 }
 // print 4,5,6 after printA print 1,2,3
 public class PrinterB implements Runnable {

  @Override
  public void run() {
  while (val < 3) {
   synchronized (WaitNotifyDemo.this) {
   try {
    System.out
     .println("printerB wait for printerA printed 1,2,3");
    WaitNotifyDemo.this.wait();
    System.out
     .println("printerB waited for printerA printed 1,2,3");
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   }
  }
  while (val <= 6) {
   printAndIncrease();
  }

  System.out.println("notify in printerB");
  synchronized (WaitNotifyDemo.this) {
   WaitNotifyDemo.this.notify();
  }
  System.out.println("notify end printerB");
  System.out.println("PrinterB exits.");
  }
 }
 public static void main(String[] args) {
  WaitNotifyDemo demo = new WaitNotifyDemo();
  demo.doPrint();
 }

 private void doPrint() {
  PrinterA pa = new PrinterA();
  PrinterB pb = new PrinterB();
  Thread a = new Thread(pa);
  a.setName("printerA");
  Thread b = new Thread(pb);
  b.setName("printerB");
  // 必须让b线程先执行,否则b线程有可能得不到锁,执行不了wait,而a线程一直持有锁,会先notify了
  b.start();
  a.start();
 }
 }

运行结果为:

printerB wait for printerA printed 1,2,3
printerA prints 1
printerA prints 2
printerA prints 3
PrinterA printed 1,2,3; notify PrinterB
wait in printerA
printerB waited for printerA printed 1,2,3
printerB prints 4
printerB prints 5
printerB prints 6
notify in printerB
notify end printerB
wait end printerA
printerA prints 7
printerA prints 8
printerA prints 9
PrinterA exits
PrinterB exits.

Process finished with exit code 0

我们来分析一下上面的程序:

首先在main方法中我们看到是先启动了B线程,因为B线程持有wait()对象,而A线程则持有notify(),如果先启动A有可能会造成死锁的状态。
B线程启动以后进入run()方法:

 while (val < 3) {
 synchronized (WaitNotifyDemo.this) {
  try {
  System.out.println("printerB wait for printerA printed 1,2,3");
  WaitNotifyDemo.this.wait();
  System.out.println("printerB waited for printerA printed 1,2,3");
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
 }
 }
 while (val <= 6) {
 printAndIncrease();
 }

这里有一个while循环,如果val的值小于3,那么在WaitNotifyDemo的实例的同步块中调用WaitNotifyDemo.this.wait()方法,这里要注意无论是wait,还是notify,notifyAll方法都需要在其实例对象的同步块中执行,这样当前线程才能获得同步实例的同步控制权,如果不在同步块中执行wait或者notify方法会出java.lang.IllegalMonitorStateException异常。另外还要注意在wait方法两边的同步块会在wait执行完毕之后释放对象锁。

这样PrinterB就进入了等待状态,我们再看下PrinterA的run方法:

while (val <= 3) {
 printAndIncrease();
 }

// print 1,2,3 then notify printerB
synchronized (WaitNotifyDemo.this) {
 System.out.println("PrinterA printed 1,2,3; notify PrinterB");
 WaitNotifyDemo.this.notify();
}

try {
 while (val <= 6) {
 synchronized (WaitNotifyDemo.this) {
  System.out.println("wait in printerA");
  WaitNotifyDemo.this.wait();
 }
 }
 System.out.println("wait end printerA");
} catch (InterruptedException e) {
 e.printStackTrace();
}

这里首先打印了1、2、3,然后在同步块中调用了WaitNotifyDemo实例的notify方法,这样PrinterB就得到了继续执行的通知,然后PrinterA进入等待状态,等待PrinterB通知。

我们再看下PrinterB run方法剩下的代码:

while (val <= 6) {
 printAndIncrease();
}

System.out.println("notify in printerB");
synchronized (WaitNotifyDemo.this) {
 WaitNotifyDemo.this.notify();
}
System.out.println("notify end printerB");
System.out.println("PrinterB exits.");

PrinterB首先打印了4、5、6,然后在同步块中调用了notify方法,通知PrinterA开始执行。

PrinterA得到通知后,停止等待,打印剩下的7、8、9三个数字,如下是PrinterA run方法中剩下的代码:

while (val <= 9) {
 printAndIncrease();
}

整个程序就分析完了,下面我们再来使用Condition来做这道题:

public class TestCondition {
 static class NumberWrapper {
 public int value = 1;
 }

 public static void main(String[] args) {
 //初始化可重入锁
 final Lock lock = new ReentrantLock();

 //第一个条件当屏幕上输出到3
 final Condition reachThreeCondition = lock.newCondition();
 //第二个条件当屏幕上输出到6
 final Condition reachSixCondition = lock.newCondition();

 //NumberWrapper只是为了封装一个数字,一边可以将数字对象共享,并可以设置为final
 //注意这里不要用Integer, Integer 是不可变对象
 final NumberWrapper num = new NumberWrapper();
 //初始化A线程
 Thread threadA = new Thread(new Runnable() {
  @Override
  public void run() {
  //需要先获得锁
  lock.lock();
  try {
   System.out.println("threadA start write");
   //A线程先输出前3个数
   while (num.value <= 3) {
   System.out.println(num.value);
   num.value++;
   }
   //输出到3时要signal,告诉B线程可以开始了
   reachThreeCondition.signal();
  } finally {
   lock.unlock();
  }
  lock.lock();
  try {
   //等待输出6的条件
   reachSixCondition.await();
   System.out.println("threadA start write");
   //输出剩余数字
   while (num.value <= 9) {
   System.out.println(num.value);
   num.value++;
   }

  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
  }

 });

 Thread threadB = new Thread(new Runnable() {
  @Override
  public void run() {
  try {
   lock.lock();

   while (num.value <= 3) {
   //等待3输出完毕的信号
   reachThreeCondition.await();
   }
  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
  try {
   lock.lock();
   //已经收到信号,开始输出4,5,6
   System.out.println("threadB start write");
   while (num.value <= 6) {
   System.out.println(num.value);
   num.value++;
   }
   //4,5,6输出完毕,告诉A线程6输出完了
   reachSixCondition.signal();
  } finally {
   lock.unlock();
  }
  }

 });

 //启动两个线程
 threadB.start();
 threadA.start();
 }
}

基本思路就是首先要A线程先写1,2,3,这时候B线程应该等待reachThredCondition信号,而当A线程写完3之后就通过signal告诉B线程“我写到3了,该你了”,这时候A线程要等嗲reachSixCondition信号,同时B线程得到通知,开始写4,5,6,写完4,5,6之后B线程通知A线程reachSixCondition条件成立了,这时候A线程就开始写剩下的7,8,9了。

我们可以看到上例中我们创建了两个Condition,在不同的情况下可以使用不同的Condition,与wait和notify相比提供了更细致的控制。

3.线程阻塞原语–LockSupport

我们一再提线程、锁等概念,但锁是如果实现的呢?又是如何知道当前阻塞线程的又是哪个对象呢?LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。

java锁和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通过调用 LockSupport .park()和 LockSupport .unpark()实现线程的阻塞和唤醒 的。 LockSupport 很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继 续 执行;如果许可已经被占用,当前线 程阻塞,等待获取许可。
LockSupport是针对特定线程来进行阻塞和解除阻塞操作的;而Object的wait()/notify()/notifyAll()是用来操作特定对象的等待集合的。
LockSupport的两个主要方法是park()和Unpark(),我们来看一下他们的实现:

public static void park(Object blocker) {
 Thread t = Thread.currentThread();
 setBlocker(t, blocker);
 unsafe.park(false, 0L);
 setBlocker(t, null);
 }

public static void park() {
 unsafe.park(false, 0L);
 }

public static void unpark(Thread thread) {
 if (thread != null)
  unsafe.unpark(thread);
 }

由源码我们可见在park方法内部首先获得当前线程然后阻塞当前线程,unpark方法传入一个可配置的线程来为该线程解锁。以“线程”作为方法的参数, 语义更清晰,使用起来也更方便。而wait/notify的实现使得“线程”的阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。

下面我们来看一个例子:

public class TestLockSupport {

 public static Object u = new Object();
 static ChangeObjectThread t1 = new ChangeObjectThread("t1");
 static ChangeObjectThread t2 = new ChangeObjectThread("t2");

 public static class ChangeObjectThread extends Thread {
 public ChangeObjectThread(String name) {
  super.setName(name);
 }

 public void run() {
  synchronized (u) {
  System.out.println("in" + getName());
  LockSupport.park();
  }
 }
 }

 public static void main(String[] args) throws InterruptedException {
 t1.start();
 Thread.sleep(2000);
 t2.start();
 LockSupport.unpark(t1);
 LockSupport.unpark(t2);
 t1.join();
 t2.join();
 }
}

当我们把”LockSupport.unpark(t1);”这一句注掉的话我们会发现程序陷入死锁。而且我们看到再main方法中unpark是在t1和t2启动之后才执行,但是为什么t1启动之后,t2也启动了呢?注意,**unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行。**unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。

除了有定时阻塞的功能外,还支持中断影响,但是和其他接收中断函数不一样,他不会抛出
InterruptedException异常,他只会默默的返回,但是我们可以从Thread.Interrupted()等方法获得中断标记.
我们来看一个例子:

public class TestLockSupport {
 public static Object u = new Object();
 static ChangeObjectThread t1 = new ChangeObjectThread("t1");
 static ChangeObjectThread t2 = new ChangeObjectThread("t2");

 public static class ChangeObjectThread extends Thread {
 public ChangeObjectThread(String name) {
  super.setName(name);
 }

 public void run() {
  synchronized (u) {
  System.out.println("in " + getName());
  LockSupport.park();
  if (Thread.interrupted()) {
   System.out.println(getName() + " 被中断了!");
  }
  }
  System.out.println(getName() + " 执行结束");
 }
 }

 public static void main(String[] args) throws InterruptedException {
 t1.start();
 Thread.sleep(100);
 t2.start();
 t1.interrupt();
 LockSupport.unpark(t2);
 }
}

输出:

in t1
t1 被中断了!
t1 执行结束
in t2
t2 执行结束

Process finished with exit code 0

由run方法中的终端异常捕获我们可以看到线程在中断时并没有抛出异常而是正常执行下去了。

关于LockSupport其实要介绍的东西还是很多,因为这个类实现了底层的一些方法,各种的锁实现都是这个基础上发展而来的。以后会专门用一个篇章来学习jdk内部的阻塞机制。说前面我们讲到Object的wait和notify,讲到Condition条件,讲到jdk中不对外部暴露的LockSupport阻塞原语,那么在JUC包中还有另外一个阻塞机制—信号量机制(Semaphore),下一节我们一起探讨一下。

以上就是java并发编程专题(五)----详解(JUC)ReentrantLock的详细内容,更多关于java ReentrantLock的资料请关注我们其它相关文章!

(0)

相关推荐

  • java并发编程专题(三)----详解线程的同步

    有兴趣的朋友可以回顾一下前两篇 java并发编程专题(一)----线程基础知识 java并发编程专题(二)----如何创建并运行java线程 在现实开发中,我们或多或少的都经历过这样的情景:某一个变量被多个用户并发式的访问并修改,如何保证该变量在并发过程中对每一个用户的正确性呢?今天我们来聊聊线程同步的概念. 一般来说,程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价.如果程序并行化后, 连基本的执行结果的正确性都无法保证, 那么并行程序本身也就没有任何意义了.因此,

  • java并发编程专题(八)----(JUC)实例讲解CountDownLatch

    CountDownLatch 是一个非常实用的多线程控制工具类." Count Down " 在英文中意为倒计数, Latch 为门问的意思.如果翻译成为倒计数门阀, 我想大家都会觉得不知所云吧! 因此,这里简单地称之为倒计数器.在这里, 门问的含义是:把门锁起来,不让里面的线程跑出来.因此,这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束, 再开始执行. CountDown Latch 的构造函数接收一个整数作为参数,即当前这个计数器的计数个数. public Co

  • java并发编程专题(二)----如何创建并运行java线程

    实现线程的两种方式 上一节我们了解了关于线程的一些基本知识,下面我们正式进入多线程的实现环节.实现线程常用的有两种方式,一种是继承Thread类,一种是实现Runnable接口.当然还有第三种方式,那就是通过线程池来生成线程,后面我们还会学习,一步一个脚印打好基础. Runnable接口: public interface Runnable { public abstract void run(); } Thread类: public class Thread implements Runnab

  • java并发编程专题(十)----(JUC原子类)基本类型详解

    这一节我们先来看一下基本类型: AtomicInteger, AtomicLong, AtomicBoolean.AtomicInteger和AtomicLong的使用方法差不多,AtomicBoolean因为比较简单所以方法比前两个都少,那我们这节主要挑AtomicLong来说,会使用一个,其余的大同小异. 1.原子操作与一般操作异同 我们在说原子操作之前为了有个对比为什么需要这些原子类而不是普通的基本数据类型就能满足我们的使用要求,那就不得不提原子操作不同的地方. 当你在操作一个普通变量时,

  • java并发编程专题(一)----线程基础知识

    在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线程状态图: 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权. 3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码. 4

  • java并发编程专题(九)----(JUC)浅析CyclicBarrier

    上一篇我们介绍了CountDownlatch,我们知道CountDownlatch是"在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待",即CountDownLatch的作用是允许1或N个线程等待其他线程完成执行,而我们今天要介绍的CyclicBarrier则是允许N个线程相互等待. 1.CyclicBarrier简介 CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)

  • java并发编程专题(六)----浅析(JUC)Semaphore

    半路开始看的朋友可以回顾一下前几篇 java并发编程专题(一)----线程基础知识 java并发编程专题(二)----如何创建并运行java线程 java并发编程专题(三)----详解线程的同步 java并发编程专题(四)----浅谈(JUC)Lock锁 java并发编程专题(五)----详解(JUC)ReentrantLock Semaphore,从字面意义上我们知道他是信号量的意思.在java中,一个计数信号量维护了一个许可集.Semaphore 只对可用许可的号码进行计数,并采取相应的行动

  • java并发编程专题(四)----浅谈(JUC)Lock锁

    首先我们来回忆一下上一节讲过的synchronized关键字,该关键字用于给代码段或方法加锁,使得某一时刻它修饰的方法或代码段只能被一个线程访问.那么试想,当我们遇到这样的情况:当synchronized修饰的方法或代码段因为某种原因(IO异常或是sleep方法)被阻塞了,但是锁有没有被释放,那么其他线程除了等待以外什么事都做不了.当我们遇到这种情况该怎么办呢?我们今天讲到的Lock锁将有机会为此行使他的职责. 1.为什么需要Lock synchronized 是Java 语言层面的,是内置的关

  • java并发编程专题(十一)----(JUC原子类)数组类型详解

    上一节我们介绍过三个基本类型的原子类,这次我们来看一下数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray.其中前两个的使用方式差不多,AtomicReferenceArray因为他的参数为引用数组,所以跟前两个的使用方式有所不同. 1.AtomicLongArray介绍 对于AtomicLongArray, AtomicIntegerArray我们还是只介绍一个,另一个使用方式大同小异. 我们先来看看AtomicLong

  • java并发编程专题(七)----(JUC)ReadWriteLock的用法

    前面我们已经分析过JUC包里面的Lock锁,ReentrantLock锁和semaphore信号量机制.Lock锁实现了比synchronized更灵活的锁机制,Reentrantlock是Lock的实现类,是一种可重入锁,都是每次只有一次线程对资源进行处理:semaphore实现了多个线程同时对一个资源的访问:今天我们要讲的ReadWriteLock锁将实现另外一种很重要的功能:读写分离锁. 假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁.在没有写操作的时候,两个线

随机推荐