zoukankan      html  css  js  c++  java
  • 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手动释放锁,不然容易造成线程死锁

    详细对比见下面的表格:

    类别synchronizedLock
    存在层次 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

  • 相关阅读:
    谈谈一些有趣的CSS题目(十二)-- 你该知道的字体 font-family
    谈谈一些有趣的CSS题目(十一)-- reset.css 知多少?
    【Web动画】SVG 实现复杂线条动画
    【Web动画】SVG 线条动画入门
    引人瞩目的 CSS 变量(CSS Variable)
    谈谈一些有趣的CSS题目(十)-- 结构性伪类选择器
    ROW_NUMBER() OVER函数的基本用法
    PL SQL笔记(三)
    pushState、replaceState、onpopstate 实现Ajax页面的前进后退刷新
    无聊的人用JS实现了一个简单的打地鼠游戏
  • 原文地址:https://www.cnblogs.com/pony1223/p/9287015.html
Copyright © 2011-2022 走看看