zoukankan      html  css  js  c++  java
  • 什么是Lock?什么是ReentrantLock?ReentrantReadWriteLock又是啥?

    ps:不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

    目录

    一.什么是Lock对象?

    二.使用ReentrantLock实现线程同步

    三.使用Lock对象实现线程间通信

    四. 使用Lock对象和Condition实现等待/通知实例

    五.使用Lock对象和多个Condition实现等待/通知实例

    六、公平锁和非公平锁

    七、使用ReentrantReadWriteLock实现并发

    八.ReentrantReadWriteLock实例代码


    一.什么是Lock对象?

    Lock其实是一个接口,在JDK1.5以后开始提供,其实现类常用的有ReentrantLock,这里所说的Lock对象即是只Lock接口的实现类,简称为Lock对象。

    在前面的synchronized这一篇博客中,可以讲了它可以实现线程间的同步互斥,从JDK1.5开始新增的 ReentrantLock类能够达到同样的效果,并且在此基础上还扩展了很多实用的功能,比 使用synchronized更佳的灵活。

    ReentrantLock的另一个称呼就是重入锁,下面就看看它怎么实现线程同步。

    二.使用ReentrantLock实现线程同步

    1. 首先,我们先来看一个例子:
    public class Run {
        public static void main(String[] args) {
            Lock lock = new ReentrantLock();
    //lambda写法
            new Thread(() ‐ > runMethod(lock), "thread1").start();
            new Thread(() ‐ > runMethod(lock), "thread2").start(); //常规写法
            new Thread(new Runnable() {
                @Override
                public void run() {
                    runMethod(lock);
                }
            }, "thread3").start();
        }
    
        private static void runMethod(Lock lock) {
            lock.lock();
            for (int i = 1; i <= 3; i++) {
                System.out.println("ThreadName:" + Thread.currentThread().getName()
                        + (" i=" + i));
            }
            System.out.println();
            lock.unlock();
        }
    }

    运行结果:

      ThreadName:thread1 i=1
      ThreadName:thread1 i=2
      ThreadName:thread1 i=3
      
      ThreadName:thread2 i=1
      ThreadName:thread2 i=2
      ThreadName:thread2 i=3
      
      ThreadName:thread3 i=1
      ThreadName:thread3 i=2
      ThreadName:thread3 i=3
    

    从代码和运行结果中我们可以发现,这段代码中的三个线程都是分组执行的,只有当前线程执行完之后,其他线程才可以获得锁,然后才可以执行。这也说明了当前线程只有执行完了才会释放它所持有的锁,但线程之间打印的顺序是随机的。

    下面再来看一个例子来更好地理解ReentrantLock是怎么实现线程同步的:

    public class Run {
        public static void main(String[] args) {
            Lock lock = new ReentrantLock();
            new Thread(() ‐ > runMethod(lock, 0), "thread1").start();
            new Thread(() ‐ > runMethod(lock, 5000), "thread2").start();
            new Thread(() ‐ > runMethod(lock, 1000), "thread3").start();
            new Thread(() ‐ > runMethod(lock, 5000), "thread4").start();
            new Thread(() ‐ > runMethod(lock, 1000), "thread5").start();
        }
    
        private static void runMethod(Lock lock, long sleepTime) {
            lock.lock();
            try {
                Thread.sleep(sleepTime);
                System.out.println("ThreadName:" +
                        Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    运行结果:

      ThreadName:thread1
      ThreadName:thread2
      ThreadName:thread3
      ThreadName:thread4
      ThreadName:thread5

     由此我们可以看出,在sleep指定的时间内,当调用了lock.lock()方法线程就持有了”对象监视器”,其他线程只能等待锁被释放后再次争抢,效果和使用synchronized关键字是一样的。

    三.使用Lock对象实现线程间通信

    上面了解了ReentrantLock是怎么实现线程间同步的,下面我们来看一下ReentrantLock是怎么实现线程间通信的。在线程间的通信这篇博客中我们可以看到synchronized与wait()方法和notify()方式结合实现线程间通信,也就是等待/通知模式。在ReentrantLock中,是借助Condition对象进行实现的。

    public class LockConditionDemo {
    	private Lock		lock		= new ReentrantLock();
    	private Condition	condition	= lock.newCondition();
    	public static void main( String[] args ) throws InterruptedException
    	{
    /* 使用同一个LockConditionDemo对象,使得lock、condition一样 LockConditionDemo demo = new LockConditionDemo(); */
    		new Thread( () ‐ > demo.await(), "thread1" ).start(); Thread.sleep( 3000 );
    		new Thread( () ‐ > demo.signal(), "thread2" ).start();
    	}
    
    
    	private void await()
    	{
    		try {
    			lock.lock();
    			System.out.println( "开始等待await! ThreadName:" + Thread.currentThread().getName() );
    			condition.await();
    			System.out.println( "等待await结束! ThreadName:" + Thread.currentThread().getName() );
    		} catch ( InterruptedException e ) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    
    
    	private void signal()
    	{
    		lock.lock();
    		System.out.println( "发送通知signal! ThreadName:" + Thread.currentThread().getName() );
    		condition.signal();
    		lock.unlock();
    	}
    }

    Condition的创建方式如下:

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    这里可以理解 Condition其实就是在创建条件,可以通过上面的创建的方式实现一个对象创建多个Condition条件,然后根据这些不同的条件实现不同的等待和通知。在使用关键字synchronized与wait()方法和 notify()方式结合实现线程间通信的时候,notify/notifyAll的通知等待的线程时是随机 的,显然使用Condition相对灵活很多,可以实现”选择性通知”。

    这是因为,synchronized关键字相当于整个Lock对象只有一个单一的Condition对象, 所有的线程都注册到这个对象上。线程开始notifAll的时候,需要通知所有等待的线程,让他们开始竞争获得锁对象,没有选择权,这种方式相对于Condition条件的方式 在效率上肯定Condition较高一些。

    四. 使用Lock对象和Condition实现等待/通知实例

    主要方法对比如下:

    (1)Object的wait()/wait(long timeout)方法相当于Condition类中的await()/await(long timeout)方法;

    (2)Object的notify()方法相当于Condition类中的signal()方法;

    (3)Object的notifyAll()方法相当于Condition类中的signalAll()方法;

    ReentrantLock结合Condition类可以实现选择性通知。synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的wait状态的线程,没有选择权,这样子比较消耗资源。

    下面看一个例子,跟之前一样,要先获取锁:

    public class LockConditionDemo {
    	private Lock lock = new ReentrantLock();
    	private Condition	condition	= lock.newCondition();
    	public static void main( String[] args ) throws InterruptedException
    	{
    /* 使用同一个LockConditionDemo对象,使得lock、condition一样 
    LockConditionDemo demo = new LockConditionDemo(); */
    		new Thread( () ‐ > demo.await(), "thread1" ).start(); 
            Thread.sleep( 3000 );
    		new Thread( () ‐ > demo.signal(), "thread2" ).start();
    	}
    
    
    	private void await()
    	{
    		try {
    			lock.lock();
    			System.out.println( "开始等待await! ThreadName:" + Thread.currentThread().getName() );
    			condition.await();
    			System.out.println( "等待await结束! ThreadName:" + Thread.currentThread().getName() );
    		} catch ( InterruptedException e ) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    
    
    	private void signal()
    	{
    		lock.lock();
    		System.out.println( "发送通知signal! ThreadName:" + Thread.currentThread().getName() );
    		condition.signal();
    		lock.unlock();
    	}
    }

    运行结果:

    开始等待await! ThreadName:thread1 
    发送通知signal! ThreadName:thread2 
    等待await结束! ThreadName:thread1
     

    首先,thread1先获得lock锁,然后调用Condition的wait方法,进入等待状态,然后执行thread2,调用Condition的signal方法,唤醒lock锁,然后继续执行thread1。

    五.使用Lock对象和多个Condition实现等待/通知实例

    实现多个Condition实现等待/通知的代码如下:

    public class LockConditionDemo {
    	private Lock lock = new ReentrantLock();
    	private Condition conditionA = lock.newCondition();
    	private Condition conditionB = lock.newCondition();
    	public static void main(String[] args) throws InterruptedException {
    		LockConditionDemo demo = new LockConditionDemo();
    		new Thread(() ‐> demo.await(demo.conditionA),
    		"thread1_conditionA").start();
    		new Thread(() ‐> demo.await(demo.conditionB),
    		"thread2_conditionB").start();
    		new Thread(() ‐> demo.signal(demo.conditionA),
    		"thread3_conditionA").start();
    		System.out.println("稍等5秒再通知其他的线程!");
    		Thread.sleep(5000);
    		new Thread(() ‐> demo.signal(demo.conditionB),
    		"thread4_conditionB").start();
    	}
    	private void await(Condition condition) {
    		try {
    			lock.lock();
    			System.out.println("开始等待await! ThreadName:" + Thread.currentThread().getName());
    			condition.await();
    			System.out.println("等待await结束! ThreadName:" + Thread.currentThread().getName());
    		}
    		catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		finally {
    			lock.unlock();
    		}
    	}
    	private void signal(Condition condition) {
    		lock.lock();
    		System.out.println("发送通知signal! ThreadName:" + Thread.currentThread().getName());
    		condition.signal();
    		lock.unlock();
    	}
    }

    执行结果:

    开始等待await! ThreadName:thread1_conditionA 
    开始等待await! ThreadName:thread2_conditionB 
    发送通知signal! ThreadName:thread3_conditionA 
    等待await结束! ThreadName:thread1_conditionA 
    稍等5秒再通知其他的线程!
    发送通知signal! ThreadName:thread4_conditionB 
    等待await结束! ThreadName:thread2_conditionB

    可以看出实现了分别通知。因此,我们可以使用Condition进行分组,可以单独的通知某一个分组,主要保证同一组的线程传的是同一组condition就可以,另外还可以使用signalAll()方法实现通知某一个分组的所有等待的线程。

    六、公平锁和非公平锁

    顾名思义,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配,即先进先出;非公平是一种抢占机制,是随机获得锁,并不是先来的一定能先得到锁。

    ReentrantLock提供了一个构造方法,可以很简单的实现公平锁或非公平锁,源代码构 造函数如下:

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

    fair为true表示是公平锁,反之为非公平锁。

    默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多

    七、使用ReentrantReadWriteLock实现并发

    上述的类ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行 ReentrantLock.lock()之后的任务。

    类似于我们集合中有同步类容器 和 并发类容器,HashTable(HashTable几乎可以等 价于HashMap,并且是线程安全的)也是完全排他的,即使是读也只能同步执行,而 ConcurrentHashMap就可以实现同一时刻多个线程之间并发。为了提高效率, ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。

    ReentrantReadWriteLock有两个锁:一个是读操作相关的锁,也称为“共享锁”;另一个是写操作相关的锁,称为“排它锁”。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。即一个是共享的,一个是排斥的,只有有排斥的锁,即为互斥。

    在没有线程进行写入操作时,进行读操作的多个线程都可以获取到读锁,而进行写入操作的线 程只有获取写锁后才能进行写入操作。即:多个线程可以同时进行读操作,但是同一 时刻只允许一个线程进行写操作,因为与写相关的锁是排它的。

    ReentrantReadWriteLock锁的特性:

    (1)读读共享; (2)写写互斥; (3)读写互斥; (4)写读互斥;

    八.ReentrantReadWriteLock实例代码

    (1)读读共享

    public class ReentrantReadWriteLockDemo {
    	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    	public static void main(String[] args) {
    		ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
    		new Thread(() ‐> demo.read(), "ThreadA").start();
    		new Thread(() ‐> demo.read(), "ThreadB").start();
    	}
    	private void read() {
    		try {
    			try {
    				lock.readLock().lock();
    				System.out.println("获得读锁" + Thread.currentThread().getName()
    				+ " 时间:" + System.currentTimeMillis());
    				//模拟读操作时间为5秒
    				Thread.sleep(5000);
    			}
    			finally {
    				lock.readLock().unlock();
    			}
    		}
    		catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }

    执行结果:

    获得读锁ThreadA 时间:1507720692022 
    获得读锁ThreadB 时间:1507720692022

    可以看出两个线程之间,获取锁的时间几乎同时,说明lock.readLock().lock()允许多 个线程同时执行lock()方法后面的代码。

    (2)写写互斥

    public class ReentrantReadWriteLockDemo {
    	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    	public static void main(String[] args) {
    		ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
    		new Thread(() ‐> demo.write(), "ThreadA").start();
    		new Thread(() ‐> demo.write(), "ThreadB").start();
    	}
    	private void write() {
    		try {
    			try {
    				lock.writeLock().lock();
    				System.out.println("获得写锁" + Thread.currentThread().getName()
    				+ " 时间:" + System.currentTimeMillis());
    				//模拟写操作时间为5秒
    				Thread.sleep(5000);
    			}
    			finally {
    				lock.writeLock().unlock();
    			}
    		}
    		catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }

    执行结果:

    获得写锁ThreadA 时间:1507720931662 
    获得写锁ThreadB 时间:1507720936662
    

    可以看出执行结果大致差了5秒的时间,可以说明多个写线程是互斥的。

    (3)读写互斥或写读互斥

    public class ReentrantReadWriteLockDemo {
    	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    	public static void main(String[] args) throws InterruptedException {
    		ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
    		new Thread(() ‐> demo.read(), "ThreadA").start();
    		Thread.sleep(1000);
    		new Thread(() ‐> demo.write(), "ThreadB").start();
    	}
    	private void read() {
    		try {
    			try {
    				lock.readLock().lock();
    				System.out.println("获得读锁" + Thread.currentThread().getName()
    				+ " 时间:" + System.currentTimeMillis());
    				Thread.sleep(3000);
    			}
    			finally {
    				lock.readLock().unlock();
    			}
    		}
    		catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    	private void write() {
    		try {
    			try {
    				lock.writeLock().lock();
    				System.out.println("获得写锁" + Thread.currentThread().getName()
    				+ " 时间:" + System.currentTimeMillis());
    				Thread.sleep(3000);
    			}
    			finally {
    				lock.writeLock().unlock();
    			}
    		}
    		catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }

    执行结果:

    获得读锁ThreadA 时间:1507721135908 
    获得写锁ThreadB 时间:1507721138908

    可以看出执行结果大致差了3秒的时间,可以说明读写线程是互斥的。

    PS:注意lock()与lockInterruptibly()的区别

    lockInterruptibly()允许在等待时由其他线程的Thread.interrupt()方法来中断等待线程而直接返回,这时是不用获取锁的,而会抛出一个InterruptException。而ReentrantLock.lock()方法则不允许Thread.interrupt()中断,即使检测到了Thread.interruptted一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功之后在把当前线程置为interrupted状态。

    参照:《Java多线程编程的核心技术》

    https://blog.csdn.net/column/details/17790.html

  • 相关阅读:
    Java--笔记(4)
    Java--笔记(3)
    Java--笔记(2)
    Java--笔记(1)
    Java--剑指offer(10)
    Java--剑指offer(9)
    网络基础面试常考知识点
    Java--剑指offer(8)
    Keil C51 的printf
    C语言中以十六进制输出字符型变量会出现'ffffff"的问题
  • 原文地址:https://www.cnblogs.com/baichendongyang/p/13235525.html
Copyright © 2011-2022 走看看