什么是Lock?什么是ReentrantLock?

一.什么是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相对灵活很多,可以实现”选择性通知”。

扫描二维码关注公众号,回复: 3838952 查看本文章

这是因为,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秒的时间,可以说明读写线程是互斥的。

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

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

猜你喜欢

转载自blog.csdn.net/striveb/article/details/83421107