同步区域有点像拜访你的公公婆婆。你当然是希望待的时间越短越好。说到锁的话情况也是一样的,你希望获取锁以及进入临界区域的时间越短越好,这样才不会造成瓶颈。
synchronized关键字是语言层面的加锁机制,它可以用于方法以及代码块。这个关键字是由HotSpot JVM来实现的。我们在代码中分配的每一个对象,比如String, Array或者一个JSON文档,在GC的层面的对象头部,都内建了一个加锁的机制。JIT编译器也是类似的,它在进行字节码的编译和反编译的时候,都取决于特定的某个锁的具体的状态和竞争级别。
同步块的一个问题在于——进入临界区域内的线程不能超过一个。这对生产者消费者场景是一个很大的打击,尽管这里有些线程会尝试进行独占式的数据编辑,而另外一些线程只是希望读取一下数据,这个是可以和别的线程同时进行的。
读写锁(ReadWriteLock)是一个理想的解决方案。你可以指定哪些线程会阻塞别的操作(写线程),哪些线程可以和别人共同进行内容的消费(读线程)。一个美满的结局?恐怕不是。
读写锁不像同步块,它并不是JVM中内建支持的,它只不过是段普通的代码。同样的,要想实现锁机制,它得引导CPU原子地或者按某个特定的顺序来执行某些特定的操作,以避免竞争条件。这通常都是通过JVM预留的一个后门来实现的——unsafe类。读写锁使用的是CAS操作来将值直接设置到内存中去,这是它们线程排队算法中的一部分。
尽管这样,读写锁也还不够快,有时候甚至会表现得非常慢,慢到你压根儿就不应该使用它。然而JDK的这帮家伙们没有放弃治疗,现在他们带来了一个全新的StampedLock。这个读写锁使用了一组新的算法以及Java 8 JDK中引入的内存屏障的特性,这使得这个锁更高效也更健壮。
它兑现了自己的诺言了吗?让我们拭目以待。
使用锁
StampedLock的用法 更为复杂。它使用了一个戳(stamp)的概念,这是一个long值,它用作加锁解锁操作的一个标签。这意味着想要解锁一个操作你得将它对应的戳给传递进去。如果你传入的戳是错误的,那么可能会抛出一个异常,或者更糟糕的是,无法预知的行为。
另外一个值得关注的重要问题是,StampedLock并不像ReadWriteLock,它不是可重入的。因此它虽然更快,但也有一个坏处是线程可能会自己产生死锁。在实践中,这意味着你应该始终确保锁以及对应的戳不要逃逸出所在的代码块。
long stamp = lock.writeLock(); //blocking lock, returns a stamp
try {
write(stamp); // this is a bad move, you’re letting the stamp escape
}
finally {
lock.unlock(stamp);// release the lock in the same block - way better
}
这个设计还有个让人无法忍受的地方是这个long类型的戳对你而言没有任何意义。我还是希望锁操作能返回一个描述这个戳的对象——包括它的类型,锁的时间,owner线程,等等。这让调试和跟踪日志变得更简单。不过这么做很有可能是故意的,以便阻止开发人员不要将这个戳在代码里传来传去,同时也减少了分配对象的开销。
乐观锁
这个锁最重要的一个新功能就是它的乐观锁模式。研究和实践表明,读操作占了绝大数,并且很少和写操作竞争 。因此,使用一个成熟的读锁有点浪费。更好的方式是直接去读,结束之后再看一下这段时间内这个值有没有被改动过。如果有的话,你再进行重试,或者升级成一个更重的锁。
long stamp = lock.tryOptimisticRead(); // non blocking
read();
if(!lock.validate(stamp)){ // if a write occurred, try again with a read lock
long stamp = lock.readLock();
try {
read();
}
finally {
lock.unlock(stamp);
}
}
使用锁最大的麻烦在于,它在生产环境的实际表现取决于应用的状态。这意味着你不能凭空选择使用何种锁,而是得将代码执行的具体环境也考虑进来 。
并发的读写线程数会影响到你具体使用哪种锁——同步区还是读写锁。如果这些线程数在JVM的执行生命周期内发生改变的话,这个问题就更棘手了,这取决于应用的状态以及线程的竞争级别。
为了能说明这点,我对四种模式下的锁进行了压测——synchronized,读写锁,StampedLock的读写锁,以及读写乐观锁,分别使用了不同的竞争级别以及不同读写线程数的组合。读线程会去消费一个计数器的值,而写线程会将它从0增加到1M。
5个读线程,5个写线程
5个读写线程分别在并发地执行,可以看到StampedLock明显胜出了,它的性能要比synchronized高出3倍。读写锁的性能也不错。奇怪的是,乐观锁,表面看起来应该是最快的,实际上这里却是最慢的。
10个读线程,10个写线程
下面,我将竞争级别提高到10个写线程和10个读线程。现在情况开始发生变化了。读写锁要比StampedLock以及synchronized慢了一个数量级。说到乐观锁还是很让人意外,它比StampedLock的读写锁还要慢。
16个读线程,4个写线程
下面,我保持同样的竞争级别,不过将读线程的比重调整了下:16个读,4个写。读写锁再说次说明了为什么它要被替换掉了——它慢了百倍以上。Stamped以及乐观锁都表现得不错,synchronized也紧随其后。
19个读,1个写
最后,我只留了一个写线程,剩下19个全是读。注意到结果更慢了,因为单个线程完成任务的时间会更长。这里的结果非常有意思。读定锁看起来像是完成不了了。StampedLock的话也不太理想——乐观锁在这里明显胜出,百倍于读写锁。需要记住的是这个模式下它可能会失败,因为写操作可能会在你读的时候出现。synchronized,我们忠实的老伙伴,依旧保持着很稳定的表现。
完整的结果可以在
这里下载。硬件:Macbook Pro i7
测试代码见
这里。
结论
看起来平均表现最佳的还是内部实现的synchronized锁。尽管如此,并不是说它在所有情况下都是表现得最好的。主要是想告诉你,采用哪种锁,应该在你的代码上线前在不同的竞争级别下,并且使用不同的读写线程数来进行详细的测试,根据结果来选择。否则你会面临线上故障的风险。
原创文章转载请注明出处:
http://it.deepinmind.com
英文原文链接
分享到:
相关推荐
- **StampedLock**:Java 8引入的锁,支持读、写和乐观读三种模式,提供更细粒度的锁控制。 **并发工具类**: - **CountDownLatch**:用于计数的同步辅助类,允许一个或多个线程等待其他线程完成操作。 - **...
第一章:Java并发简介 1.1 什么是并发编程 1.2 Java中的并发编程模型 1.3 线程的生命周期 第二章:基础同步工具 2.1 synchronized关键字 2.2 volatile关键字 第三章:锁机制 3.1 ReentrantLock 3.2 ...
3. **锁机制**:书中可能详细介绍了传统的synchronized关键字,以及Java 5引入的显式锁,如ReentrantLock、ReadWriteLock(读写锁)和StampedLock。读者可以学习到如何在不同场景下选择合适的锁策略。 4. **原子类*...
- **ReentrantLock**:可重入互斥锁,与synchronized相比,具有更细粒度的控制,支持公平性和非公平性,以及可中断和尝试获取锁的特性。 - **ReadWriteLock**:读写锁,允许多个读取线程同时访问,但在写操作时会...
并发编程涉及的主要目标是确保线程安全、解决活跃性问题以及优化性能。 **1. 线程的三大性质** - **可见性**:当一个线程修改了共享变量,其他线程能立即看到这个变化。由于缓存的存在,不同线程可能拥有各自的副本...
【美团】Java 岗 154 道面试题涵盖了Java集合、JVM调优和并发编程等核心领域,下面将详细解释其中的部分知识点。 1. **ArrayList 和 Vector 的区别**:ArrayList 是基于动态数组...56. **ReadWriteLock 和 StampedLock
Java多线程是Java开发中的核心技能之一,尤其在面试中,对于一线大厂的面试者来说,深入理解和掌握多线程的相关知识点至关重要。以下是一些关键的Java多线程面试知识点: 1. **自旋锁**:自旋锁是一种等待机制,当...
55. ReadWriteLock和StampedLock用于读写并发控制,ReadWriteLock允许多个读取者,StampedLock提供了读写锁和乐观锁。 56. run()直接执行线程体,start()创建新线程并执行run()。 57. start()由系统调度执行run()...
这份资深程序员的Java面试题总结涵盖了多个关键领域,包括Java基础、集合、多线程、反射以及对象拷贝等,这些都是Java程序员需要掌握的核心知识。 一、Java基础 1. JDK是Java Development Kit,包含了JRE(Java ...
J.U.C中的锁不仅包括传统的synchronized和ReentrantLock,还有诸如ReadWriteLock、StampedLock等高级锁。这些锁具有不同的特性,如读写锁允许多个读操作同时进行,但写操作时必须独占,这样就大大提高了并发度。 ...
3. **StampedLock**:一种更先进的锁,提供了乐观读锁和写锁,以及独占写锁。乐观读锁在读取时不需要获取锁,只有在更新时才检查是否需要获取写锁,从而提高了性能。 Condition接口是Lock接口的一个重要补充,它...
5. **显式封锁**:如java.util.concurrent.locks.StampedLock,它提供了乐观读、独占写以及共享读写的能力,进一步提高了并发性能。 封锁的使用需要谨慎,过度的封锁可能会导致线程饥饿或死锁问题。死锁是指两个或...
│ 高并发编程第一阶段19讲、结合jconsole,jstack以及汇编指令认识synchronized关键字.mp4 │ 高并发编程第一阶段20讲、同步代码块以及同步方法之间的区别和关系.mp4 │ 高并发编程第一阶段21讲、通过实验分析...
│ 高并发编程第一阶段19讲、结合jconsole,jstack以及汇编指令认识synchronized关键字.mp4 │ 高并发编程第一阶段20讲、同步代码块以及同步方法之间的区别和关系.mp4 │ 高并发编程第一阶段21讲、通过实验分析...