ITPub博客

首页 > 应用开发 > Java > java并发编程--互斥锁, 读写锁及条件

java并发编程--互斥锁, 读写锁及条件

Java 作者:Tim_java 时间:2015-09-07 15:23:34 0 删除 编辑
      java.util.concurrent.locks包提供了锁和等待条件的接口和类, 可用于替代JDK1.5之前的同步(synchronized)和监视器机制(主要是Object类的wait(), notify(), notifyAll()方法).

一、互斥锁--Lock接口及其实现类ReentrantLock

   

      所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作, Lock和synchronized机制的主要区别:

  • synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放. 而Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 以下代码演示了在不同的块结构中获取和释放锁:

    点击(此处)折叠或打开

    1. public class LockTest {
    2.     private static Lock lock = new ReentrantLock();
    3.     public static void main(String[] args) {
    4.         lock.lock();
    5.         invokeMethod();
    6.     }

    7.     private static void invokeMethod() {
    8.         lock.unlock();
    9.     }
    10. }
    为了确保锁被释放, 通常会采用如下的代码形式:

点击(此处)折叠或打开

  1. Lock lock = new ReentrantLock();
  2. // 获取锁
  3. lock.lock();
  4.     try {
  5.         // access the resource protected by this lock
  6.     } finally {
  7.         // 释放锁
  8.         lock.unlock();
  9.     }

|--void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.

|--void unlock(): 执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.

  • Lock提供了一个非块结构的获取锁尝试--tryLock(), 一个获取可中断锁的尝试--lockInterruptibly()和一个获取超时失效锁的尝试--tryLock(long time, TimeUnit unit).

|--boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 通常采用如下的代码形式调用tryLock()方法:


点击(此处)折叠或打开

  1. Lock lock = new ReentrantLock();
  2.       if (lock.tryLock()) {
  3.           try {
  4.               // manipulate protected state
  5.           } finally {
  6.               lock.unlock();
  7.           }
  8.       } else {
  9.           // perform alternative actions
  10.       }

此用法可确保如果获取了锁, 则会释放锁; 如果未获取锁, 则不会试图将其释放.

  • Lock的newCondition()方法可以获得与该锁绑定的Condition对象, Condition的详细介绍如下. 

二、条件--Condition

    调用Condition对象的相关方法, 可以方便的挂起和唤醒线程. Object对象的wait(), notify(), notifyAll()方法当然也可以做到这一点, 但是Object对象的这些方法存在很不方便的地方--如果多个线程调用了obj的wait()方法而挂起, 那么我们无法做到调用obj的notify()和notifyAll()方法唤醒其中特定的一个线程. 而Condition对象就可以做到这一点. 

  • void await(): 调用Condition对象的await()方法将导致当前线程被挂起, 并释放该Condition对象所绑定的锁. Condition对象只能通过Lock类的newCondition()方法获取, 因此一个Condition对象必然有一个与其绑定的Lock锁. 调用Condition对象的await()方法的前提是: 当前线程必须持有与该Condition对象绑定的锁, 否则程序可能抛出异常.
  • void signal(): 唤醒一个在该Condition对象上挂起的线程. 如果存在多个线程等待这个Condition对象的唤醒, 则随机选择一个. 线程被唤醒之前, 必须重新获取到锁(与该Condition对象绑定的Lock对象).
  • void signalAll(): 唤醒所有在该Condition对象上挂起的线程. 所有被唤醒的线程将竞争与该Condition对象绑定的锁, 只有获取到锁的线程才能恢复到运行状态.

问题的描述

启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5, 然后是线程2打印6,7,8,9,10, 然后是线程3打印11,12,13,14,15. 接着再由线程1打印16,17,18,19,20....以此类推, 直到打印到75. 程序的输出结果应该为:

 

线程1: 1

线程1: 2

线程1: 3

线程1: 4

线程1: 5

 

线程2: 6

线程2: 7

线程2: 8

线程2: 9

线程2: 10

...

 

线程3: 71

线程3: 72

线程3: 73

线程3: 74

线程3: 75



解法一: 采用原始的synchronized, wait(), notify(), notifyAll()等方式控制线程.

点击(此处)折叠或打开

  1. public class NumberPrintDemo {
  2.     // n为即将打印的数字
  3.     private static int n = 1;
  4.     // state=1表示将由线程1打印数字, state=2表示将由线程2打印数字, state=3表示将由线程3打印数字
  5.     private static int state = 1;

  6.     public static void main(String[] args) {
  7.         final NumberPrintDemo pn = new NumberPrintDemo();
  8.         new Thread(new Runnable() {
  9.             public void run() {
  10.                 // 3个线程打印75个数字, 单个线程每次打印5个连续数字, 因此每个线程只需执行5次打印任务. 3*5*5=75
  11.                 for (int i = 0; i < 5; i++) {
  12.                     // 3个线程都使用pn对象做锁, 以保证每个交替期间只有一个线程在打印
  13.                     synchronized (pn) {
  14.                         // 如果state!=1, 说明此时尚未轮到线程1打印, 线程1将调用pn的wait()方法, 直到下次被唤醒
  15.                         while (state != 1)
  16.                             try {
  17.                                 pn.wait();
  18.                             } catch (InterruptedException e) {
  19.                                 e.printStackTrace();
  20.                             }
  21.                         // 当state=1时, 轮到线程1打印5次数字
  22.                         for (int j = 0; j < 5; j++) {
  23.                             // 打印一次后n自增
  24.                             System.out.println(Thread.currentThread().getName()
  25.                                     + ": " + n++);
  26.                         }
  27.                         System.out.println();
  28.                         // 线程1打印完成后, 将state赋值为2, 表示接下来将轮到线程2打印
  29.                         state = 2;
  30.                         // notifyAll()方法唤醒在pn上wait的线程2和线程3, 同时线程1将退出同步代码块, 释放pn锁.
  31.                         // 因此3个线程将再次竞争pn锁
  32.                         // 假如线程1或线程3竞争到资源, 由于state不为1或3, 线程1或线程3将很快再次wait, 释放出刚到手的pn锁.
  33.                         // 只有线程2可以通过state判定, 所以线程2一定是执行下次打印任务的线程.
  34.                         // 对于线程2来说, 获得锁的道路也许是曲折的, 但前途一定是光明的.
  35.                         pn.notifyAll();
  36.                     }
  37.                 }
  38.             }
  39.         }, "线程1").start();

  40.         new Thread(new Runnable() {
  41.             public void run() {
  42.                 for (int i = 0; i < 5; i++) {
  43.                     synchronized (pn) {
  44.                         while (state != 2)
  45.                             try {
  46.                                 pn.wait();
  47.                             } catch (InterruptedException e) {
  48.                                 e.printStackTrace();
  49.                             }
  50.                         for (int j = 0; j < 5; j++) {
  51.                             System.out.println(Thread.currentThread().getName()
  52.                                     + ": " + n++);
  53.                         }
  54.                         System.out.println();
  55.                         state = 3;
  56.                         pn.notifyAll();
  57.                     }
  58.                 }
  59.             }
  60.         }, "线程2").start();

  61.         new Thread(new Runnable() {
  62.             public void run() {
  63.                 for (int i = 0; i < 5; i++) {
  64.                     synchronized (pn) {
  65.                         while (state != 3)
  66.                             try {
  67.                                 pn.wait();
  68.                             } catch (InterruptedException e) {
  69.                                 e.printStackTrace();
  70.                             }
  71.                         for (int j = 0; j < 5; j++) {
  72.                             System.out.println(Thread.currentThread().getName()
  73.                                     + ": " + n++);
  74.                         }
  75.                         System.out.println();
  76.                         state = 1;
  77.                         pn.notifyAll();
  78.                     }
  79.                 }
  80.             }
  81.         }, "线程3").start();
  82.     }
  83. }

解法二: 采用JDK1.5并发包提供的Lock, Condition等类的相关方法控制线程.

总结: 对比解法一和解法二, 显然解法二是更好的解决方案. 解法一的问题在于无法进行精确唤醒, 比如线程1执行完打印任务并调用pn.notifyAll()方法后, 3个线程将再次竞争锁, 而不是精确唤醒线程2. 虽然线程2最终将赢得锁, 下一次的打印任务也肯定会由线程2执行, 但是竞争的持续时间是不可预知的, 只能看线程2的人品.

最糟糕的情形可以是: 线程3竞争到了锁, 紧接着wait. 接下来线程1也竞争到了锁, 然后线程1也wait. 此时就再也没有其他线程跟线程2竞争了, 线程2终于艰难的赢得了锁...



点击(此处)折叠或打开

  1. public class NumberPrint implements Runnable {
  2.     private int state = 1;
  3.     private int n = 1;
  4.     // 使用lock做锁
  5.     private ReentrantLock lock = new ReentrantLock();
  6.     // 获得lock锁的3个分支条件
  7.     private Condition c1 = lock.newCondition();
  8.     private Condition c2 = lock.newCondition();
  9.     private Condition c3 = lock.newCondition();

  10.     @Override
  11.     public void run() {
  12.         new Thread(new Runnable() {
  13.             public void run() {
  14.                 for (int i = 0; i < 5; i++) {
  15.                     try {
  16.                         // 线程1获得lock锁后, 其他线程将无法进入需要lock锁的代码块.
  17.                         // 在lock.lock()和lock.unlock()之间的代码相当于使用了synchronized(lock){}
  18.                         lock.lock();
  19.                         while (state != 1)
  20.                             try {
  21.                                 // 线程1竞争到了lock, 但是发现state不为1, 说明此时还未轮到线程1打印.
  22.                                 // 因此线程1将在c1上wait
  23.                                 // 与解法一不同的是, 三个线程并非在同一个对象上wait, 也不由同一个对象唤醒
  24.                                 c1.await();
  25.                             } catch (InterruptedException e) {
  26.                                 e.printStackTrace();
  27.                             }
  28.                         // 如果线程1竞争到了lock, 也通过了state判定, 将执行打印任务
  29.                         for (int j = 0; j < 5; j++) {
  30.                             System.out.println(Thread.currentThread().getName()
  31.                                     + ": " + n++);
  32.                         }
  33.                         System.out.println();
  34.                         // 打印完成后将state赋值为2, 表示下一次的打印任务将由线程2执行
  35.                         state = 2;
  36.                         // 唤醒在c2分支上wait的线程2
  37.                         c2.signal();
  38.                     } finally {
  39.                         // 打印任务执行完成后需要确保锁被释放, 因此将释放锁的代码放在finally中
  40.                         lock.unlock();
  41.                     }
  42.                 }
  43.             }
  44.         }, "线程1").start();

  45.         new Thread(new Runnable() {
  46.             public void run() {
  47.                 for (int i = 0; i < 5; i++) {
  48.                     try {
  49.                         lock.lock();
  50.                         while (state != 2)
  51.                             try {
  52.                                 c2.await();
  53.                             } catch (InterruptedException e) {
  54.                                 e.printStackTrace();
  55.                             }
  56.                         for (int j = 0; j < 5; j++) {
  57.                             System.out.println(Thread.currentThread().getName()
  58.                                     + ": " + n++);
  59.                         }
  60.                         System.out.println();
  61.                         state = 3;
  62.                         c3.signal();
  63.                     } finally {
  64.                         lock.unlock();
  65.                     }
  66.                 }
  67.             }
  68.         }, "线程2").start();

  69.         new Thread(new Runnable() {
  70.             public void run() {
  71.                 for (int i = 0; i < 5; i++) {
  72.                     try {

  73.                         lock.lock();
  74.                         while (state != 3)
  75.                             try {
  76.                                 c3.await();
  77.                             } catch (InterruptedException e) {
  78.                                 e.printStackTrace();
  79.                             }
  80.                         for (int j = 0; j < 5; j++) {
  81.                             System.out.println(Thread.currentThread().getName()
  82.                                     + ": " + n++);
  83.                         }
  84.                         System.out.println();
  85.                         state = 1;
  86.                         c1.signal();
  87.                     } finally {
  88.                         lock.unlock();
  89.                     }
  90.                 }
  91.             }
  92.         }, "线程3").start();
  93.     }
  94.     
  95.     public static void main(String[] args) {
  96.         new NumberPrint().run();
  97.     }
  98. }

    总结: 对比解法一和解法二, 显然解法二是更好的解决方案. 解法一的问题在于无法进行精确唤醒, 比如线程1执行完打印任务并调用pn.notifyAll()方法后, 3个线程将再次竞争锁, 而不是精确唤醒线程2. 虽然线程2最终将赢得锁, 下一次的打印任务也肯定会由线程2执行, 但是竞争的持续时间是不可预知的, 只能看线程2的人品.

最糟糕的情形可以是: 线程3竞争到了锁, 紧接着wait. 接下来线程1也竞争到了锁, 然后线程1也wait. 此时就再也没有其他线程跟线程2竞争了, 线程2终于艰难的赢得了锁...

 

三、读写锁--ReadWriteLock接口及其实现类ReentrantReadWriteLock

ReentrantReadWriteLock中定义了2个内部类, ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock, 分别用来代表读取锁和写入锁. ReentrantReadWriteLock对象提供了readLock()和writeLock()方法, 用于获取读取锁和写入锁. 
 
读取锁允许多个reader线程同时持有, 而写入锁最多只能有一个writter线程持有.
读写锁的使用场合: 读取共享数据的频率远大于修改共享数据的频率. 在上述场合下, 使用读写锁控制共享资源的访问, 可以提高并发性能.
如果一个线程已经持有了写入锁, 则可以再持有读写锁. 相反, 如果一个线程已经持有了读取锁, 则在释放该读取锁之前, 不能再持有写入锁.
可以调用写入锁的newCondition()方法获取与该写入锁绑定的Condition对象, 此时与普通的互斥锁并没有什么区别. 但是调用读取锁的newCondition()方法将抛出异常. 


 

使用读写锁的一个例子:


点击(此处)折叠或打开

  1. public class ReadWriteLockTest {
  2.     private static ReadWriteLock lock = new ReentrantReadWriteLock();
  3.     private static Person person = new Person("David Beckham", true);

  4.     public static void main(String[] args) {
  5.         new Thread() {
  6.             public void run() {
  7.                 while(true) {
  8.                     try {
  9.                         lock.readLock().lock();
  10.                         System.out.print("name = " + person.getName());
  11.                         System.out.println(", isMan = " + person.isMan());
  12.                     } finally {
  13.                         lock.readLock().unlock();
  14.                     }
  15.                 }
  16.             };
  17.         }.start();
  18.         new Thread() {
  19.             public void run() {
  20.                 boolean state = true;
  21.                 while(true) {
  22.                     try {
  23.                         lock.writeLock().lock();
  24.                         if (state) {
  25.                             person.setName("Lady GaGa");
  26.                             person.setMan(false);
  27.                             state = false;
  28.                         } else {
  29.                             person.setName("David Beckham");
  30.                             person.setMan(true);
  31.                             state = true;
  32.                         }
  33.                         
  34.                     } finally {
  35.                         lock.writeLock().unlock();
  36.                     }
  37.                 }
  38.             };
  39.         }.start();
  40.     }
  41. }

  42. class Person {
  43.     private String name;
  44.     private boolean isMan;

  45.     public Person(String name, boolean isMan) {
  46.         this.name = name;
  47.         this.isMan = isMan;
  48.     }

  49.     public String getName() {
  50.         return name;
  51.     }

  52.     public void setName(String name) {
  53.         this.name = name;
  54.     }

  55.     public boolean isMan() {
  56.         return isMan;
  57.     }

  58.     public void setMan(boolean isMan) {
  59.         this.isMan = isMan;
  60.     }
  61. }
    

     不管程序运行多久, 也不会有人妖的出现。
    这里获取写锁的线程在写的时候,其它的线程如果没有使用读锁的话,读到的数据可能会有中间态数据。所以读锁和写锁要同时使用。

参考:
http://coolxing.iteye.com/blog/1236909
和
http://coolxing.iteye.com/blog/1236696

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/11627468/viewspace-1793388/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: dubbo连接池爆满
请登录后发表评论 登录
全部评论

注册时间:2015-05-26

  • 博文量
    12
  • 访问量
    89865