您的位置:首页 > 编程语言 > Java开发

Java中的ReentrantLock和synchronized

2017-07-17 12:02 651 查看
多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言。核心类库包含一个 
Thread
 类,可以用它来构建、启动和操纵线程,Java
语言包括了跨线程传达并发性约束的构造 —— 
synchronized
 和 
volatile
 。在简化与平台无关的并发类的开发的同时,它决没有使并发类的编写工作变得更繁琐,只是使它变得更容易了。

synchronized 快速回顾

把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙;它要对付内存缓存和编译器优化的各种反常行为。一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,如下面的代码所示,那么运行库将确保某一线程对变量所做的更新先于对现有
synchronized
 块所进行的更新,当进入由同一监控器(lock)保护的另一个 
synchronized
 块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于 
volatile
 变量上。

[java] view
plain copy

synchronized (lockObject) {   

  // update object state  

}  

所以,实现同步操作需要考虑安全更新多个共享变量所需的一切,不能有争用条件,不能破坏数据(假设同步的边界位置正确),而且要保证正确同步的其他线程可以看到这些变量的最新值。通过定义一个清晰的、跨平台的内存模型(该模型在 JDK 5.0 中做了修改,改正了原来定义中的某些错误),通过遵守下面这个简单规则,构建“一次编写,随处运行”的并发类是有可能的:
不论什么时候,只要您将编写的变量接下来可能被另一个线程读取,或者您将读取的变量最后是被另一个线程写入的,那么您必须进行同步。

不过现在好了一点,在最近的 JVM 中,没有争用的同步(一个线程拥有锁的时候,没有其他线程企图获得锁)的性能成本还是很低的。(也不总是这样;早期 JVM 中的同步还没有优化,所以让很多人都这样认为,但是现在这变成了一种误解,人们认为不管是不是争用,同步都有很高的性能成本。)

对 synchronized 的改进

如此看来同步相当好了,是么?那么为什么 JSR 166 小组花了这么多时间来开发 
java.util.concurrent.lock
 框架呢?答案很简单-同步是不错,但它并不完美。它有一些功能性的限制 —— 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。

ReentrantLock 类

java.util.concurrent.lock
 中的 
Lock
 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 
Lock
 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 
ReentrantLock
 类实现了 
Lock
 ,它拥有与 
synchronized
 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM
可以花更少的时候来调度线程,把更多时间用在执行线程上。)

reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 
synchronized
 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) 
synchronized
 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 
synchronized
 块时,才释放锁。

在查看清单 1 中的代码示例时,可以看到 
Lock
 和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。

清单 1. 用 ReentrantLock 保护代码块。

[java] view
plain copy

Lock lock = new ReentrantLock();  

lock.lock();  

try {   

  // update object state  

}  

finally {  

  lock.unlock();   

}  

除此之外,与目前的 synchronized 实现相比,争用下的 
ReentrantLock
 实现更具可伸缩性。(在未来的 JVM 版本中,synchronized 的争用性能很有可能会获得提高。)这意味着当许多线程都在争用同一个锁时,使用 
ReentrantLock
 的总体开支通常要比 
synchronized
 少得多。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: