多线程篇 之 ReentrantLock 与 Condition

一、 ReentrantLock

ReentrantLock是一个互斥锁,也是一个可重入的互斥锁。ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是相比功能更加丰富,添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。

先来看构造:

//默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

// 可选是否公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

为什么会存在公平锁和非公平锁

公平锁: 保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总能按照请求的顺序获得锁,通俗讲就是 先start()启动的线程先获得锁。

非公平锁: 后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。

通常情况下挂起的线程重新开始与它真正开始运行之间会产生严重的延时,而非公平锁就可以利用这段时间完成操作,这是非公平锁在某些时候比公平锁性能要好的原因之一。

而一般默认构造也是非公平锁。

ReentrantLock 的锁

  /**加锁
  1)如果当前线程未被中断,则获取锁。 
  2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。 
  3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。 */
  public void lock() 
  
  // 如果当前线程未被中断,则获取锁。
  void lockInterruptibly()
      
  //马上返回,拿到lock就返回true,不然返回false  
  boolean tryLock()
      
  // 拿不到lock,就等一段时间,超时返回false
  boolean tryLock(long timeout, TimeUnit unit)
      
   //试图释放该锁
  public void unlock()
   

案例1 ReentrantLock 用法

public class ReentrantLock2 {
	Lock lock = new ReentrantLock();

	void m1() {
		try {
			lock.lock(); //synchronized(this)
			for (int i = 0; i < 5; i++) {
				TimeUnit.SECONDS.sleep(1);
				System.out.println(i);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	void m2() {
		try {
			lock.lock();
			System.out.println("m2 ...");
		} finally {
			lock.unlock();
		}

	}

	public static void main(String[] args) throws InterruptedException {
		T02_ReentrantLock2 rl = new T02_ReentrantLock2();
		new Thread(rl::m1).start();
		TimeUnit.SECONDS.sleep(1);
		new Thread(rl::m2).start();
	}
}

输出,当m1执行释放锁后, m2才能拿到锁得以执行。

0
1
2
3
4
m2 ...

等同于直接加 synchronized 关键字。

synchronized void m1() {
    for(int i=0; i<5; i++) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(i);
    }

}

synchronized void m2() {
	System.out.println("m2 ...");
}

ReentrantLock必须要手动释放锁。使用synchronized锁定如果遇到异常,jvm会自动释放锁,但是Lock必须手动释放,因此常常在finally中释放锁。


案例二 lockInterruptibly()与 lock()的区别

lock():默认处理了中断请求,一旦监测到中断状态,则中断当前线程;

lockInterruptibly(): 直接抛出中断异常,由上层调用者区去处理中断。

再看代码:

/**
 *
 *@author Saiuna
 *@date 2020/7/13 9:02
 */
public class LockInterruptiblyDemo {
		
	public static void main(String[] args) throws InterruptedException {
		Lock lock = new ReentrantLock();

		Thread t1 = new Thread(()->{
			try {
				lock.lock();
				System.out.println("t1 start");
				TimeUnit.SECONDS.sleep(5);
				System.out.println("t1 end");
			} catch (InterruptedException e) {
				System.out.println(Thread.currentThread().getName() +  "  interrupted!");
			} finally {
				lock.unlock();
			}
		},"t1");
		t1.start();
		
		Thread t2 = new Thread(()->{
			try {
				//lock.lock();
				lock.lockInterruptibly(); //可以对interrupt()方法做出响应
				System.out.println("t2 start");
				TimeUnit.SECONDS.sleep(2);
				System.out.println("t2 end");
			} catch (InterruptedException e) {
				System.out.println(Thread.currentThread().getName() +  "  interrupted!");
			} finally {
				lock.unlock();
			}
		}, "t2");
		t2.start();
		
		TimeUnit.SECONDS.sleep(1);
//		Interrupts this thread
		t1.interrupt();
		t2.interrupt();
	}
}

这输出 就不言而喻了吧。 (注意抛出异常的是t2 )

t1 start
t1  interrupted!
t2  interrupted!
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.feature.multThreadDemo.ReentrantLock.LockInterruptiblyDemo.lambda$main$1(LockInterruptiblyDemo.java:42)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

Lock() 在阻塞的时候是不响应中断的。 lock获取锁过程中,忽略了中断,在成功获取锁之后,再根据中断标识处理中断.


二、Condition

Condition 可替代Object的wait()、notify()实现线程间的协作,相比下,使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。

Condition类能实现synchronized和wait、notify搭配的功能,另外比后者更灵活,Condition可以实现多路通知功能,也就是在一个Lock对象里可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活

而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在这个对象上。线程开始notifyAll时,需要通知所有的WAITING线程,没有选择权,会有相当大的效率问题。


举例:

实现一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。

/**
 * 消费者只叫醒生产者 而生产者只叫醒消费者。而不是谁都叫醒所有人,浪费资源
 * @param <T>
 * @author Saiuna
 * @date 2020/7/13 9:40
 */
public class MyContainer2<T> {
	final private LinkedList<T> lists = new LinkedList<>();
	final private int MAX = 10; //最多10个元素
	private int count = 0;
	
	private Lock lock = new ReentrantLock();
	private Condition producer = lock.newCondition();
	private Condition consumer = lock.newCondition();
	
	public void put(T t) {
		try {
			lock.lock();
			//满了则不再生成,等待被消费
			while(lists.size() == MAX) { 
				producer.await();
			}
			
			lists.add(t);
			++count;
			consumer.signalAll(); //通知消费者线程进行消费
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public T get() {
		T t = null;
		try {
			lock.lock();
			//没得消费了, 等待生产
			while(lists.size() == 0) {
				consumer.await();
			}
			t = lists.removeFirst();
			count --;
			producer.signalAll(); //通知生产者进行生产
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
		return t;
	}
	
	public static void main(String[] args) throws InterruptedException {
		MyContainer2<String> c = new MyContainer2<>();
		//启动消费者线程
		for(int i=0; i<10; i++) {
			new Thread(()->{
				for(int j=0; j<5; j++) System.out.println(Thread.currentThread().getName() +
						" 消费: "  +c.get());
			}, "c" + i).start();
		}

		TimeUnit.SECONDS.sleep(2);

		//启动生产者线程
		for(int i=0; i<2; i++) {
			new Thread(()->{
				for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + "_产出_" + j);
			}, "p" + i).start();
		}
	}
}

这里使用Lock和Condition来实现,也可以用Object 的 wait()、notifyAll() 来实现,此处不再展示,可自行替换对应 的await() 和 signalAll()即可。

对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒,生产时只需要唤醒消费者来消费,而消费时 只唤醒生产者 去生产。


三、ReentrantLock 与 synchronized 的区别

相同: ReentrantLock提供了synchronized类似的功能和内存语义。

不同:

(1)与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。

(2)ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合。

(3)ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。

(4)ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。

(5)ReentrantLock支持中断处理,且性能较synchronized会好些。

(6)ReentrantLock的锁必须在 finally 块中释放, 而使用synchronized,JVM 将确保锁会获得自动释放。

(7)当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。

synchroized 是个重点,下期写



四、 抛弃 synchronied ?

虽然ReentrantLock 看上去很丰富很好,但我的前辈建议还是用 synchronied,然后找到了下文。

下文为直接引用 JDK 5.0 中更灵活、更具可伸缩性的锁定机制 ,写得很nice

处处都好?

看起来 ReentrantLock 无论在哪方面都比 synchronized 好 —— 所有 synchronized 能做的,它都能做,它拥有与 synchronized 相同的内存和并发性语义,还拥有 synchronized 所没有的特性,在负荷下还拥有更好的性能。那么,我们是不是应当忘记 synchronized ,不再把它当作已经已经得到优化的好主意呢?或者甚至用 ReentrantLock 重写我们现有的 synchronized 代码?实际上,几本 Java 编程方面介绍性的书籍在它们多线程的章节中就采用了这种方法,完全用 Lock 来做示例,只把 synchronized 当作历史。但我觉得这是把好事做得太过了。

还不要抛弃 synchronized

虽然 ReentrantLock 是个非常动人的实现,相对 synchronized 来说,它有一些重要的优势,但是我认为急于把 synchronized 视若敝屣,绝对是个严重的错误。 java.util.concurrent.lock 中的锁定类是用于高级用户和高级情况的工具 。一般来说,除非您对 Lock的某个高级特性有明确的需要,或者有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。

为什么我在一个显然“更好的”实现的使用上主张保守呢?

因为对于 java.util.concurrent.lock 中的锁定类来说,synchronized 仍然有一些优势。比如,在使用 synchronized 的时候,不能忘记释放锁;在退出 synchronized 块时,JVM 会为您做这件事。

您很容易忘记用finally 块释放锁,这对程序非常有害。您的程序能够通过测试,但会在实际工作中出现死锁,那时会很难指出原因(这也是为什么根本不让初级开发人员使用 Lock 的一个好理由。)

另一个原因是因为,当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源

Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。而且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前,使用 Lock类将意味着要利用的特性不是每个 JVM 都有的,而且不是每个开发人员都熟悉的。

什么时候选择用 ReentrantLock 代替 synchronized

既然如此,我们什么时候才应该使用 ReentrantLock 呢?

答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。

我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。

一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。

结束语

Lock 框架是同步的兼容替代品,它提供了 synchronized 没有提供的许多特性,它的实现在争用下提供了更好的性能。

但是,这些明显存在的好处,还不足以成为用 ReentrantLock 代替 synchronized 的理由。相反,应当根据您是否 需要 ReentrantLock 的能力来作出选择。

大多数情况下,您不应当选择它 —— synchronized 工作得很好,可以在所有 JVM 上工作,更多的开发人员了解它,而且不太容易出错。

只有在真正需要 Lock 的时候才用它。在这些情况下,您会很高兴拥有这款工具。

参考:

https://www.cnblogs.com/xiaoxi/p/7651360.html

https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

今天不学习,明天变辣鸡。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值