从谈Java并发开始synchronized和锁就时常被谈到,上篇讲Java内存模型特点的时候,也说道用synchronized几乎可以同时满足原子性、可见性和有序性三点,那本篇就来说一下锁的概念、synchronized和API层面Lock锁框架的比较选择。后面也会讲到状态依赖与协同问题、条件队列和锁优化。
先说说synchronized。synchronized关键字可谓是并发里的常见词,但synchronized的用法可能这里还有很多大家不熟悉的细节,这里整理一下:
- synchronized保证当前块中代码内容(对外部)的原子性、可见性和happens-before规则的顺序
- 通常在非静态情况下,synchronized的是锁住对象,即以对象充当锁,所以有synchronized(Object){…}代码块编码方式,其中Object既可以是当前对象this也可以是其它对象
- synchronized修饰的非静态方法使用的锁是当前对象
- 需要注意的是一个对象只对应一个synchronized锁,即所有synchronized同一个对象的方法会竞争这同一个锁
- 未获得锁的线程需要阻塞等待,关于线程阻塞的解除前面线程终止的文章提到过
- 可冲入性,已经进入锁定的线程可直接获得当前锁,无须阻塞等待,重入采用计数机制
- 修饰静态方法的synchronized关键字锁住的是当前类对应的class对象,静态代码块需要锁住对应类.class
- synchronized是JVM提供的锁保证,实现机制无须再编码层面关心
1. 相应的,再说下Lock接口及其对应的一个实现ReentrantLock。ReentrantLock顾名思义,至少保证了synchronized的可重入性,实际上Lock的实现基本上保证了synchronized的在并发开发中线程安全的所有特性,需要注意,也是在两者选择中需要注意的特点,整理如下:
- Lock(及其ReentrantLock实现)锁提供了lock()和unlock()方法,和synchronized的代码块进入和退出对应。为了保证锁在发生异常的情况下锁得到解除,通常采用try-finally代码块的形式,并在finally中执行unlock()方法的调用
- Lock(及其ReentrantLock实现)支持非阻塞等待,提供了tryLock()方法和按时间尝试的方法,获得锁失败则可以按需要直接返回,而不是死等下去,这在一定程度上避免了死锁和饿死的情况
- 不像Java提供的synchronized内在锁,Lock(及其ReentrantLock实现)还支持可中断锁,提供了lockInterruptibly()方法,在等待锁阻塞的时候,可以通过中断使其放弃等待
- 和内在锁以及内在锁对应的监视器等待队列机制不同,Lock和其对应的Condition接口都支持一个对象多个锁和条件对应,而非固有的一对一
- 还有,ReentrantLock的实现区分了公平锁和非公平锁,更灵活地适应需求场景
特点上,先整理这几点。更多的,就像《Java Concurrency in Practice》中所说,JavaSE5的java.util.concurrent中这套Java代码实现的锁框架,不是用来彻底取代synchronized的固有内在锁的,而是给开发者提供了特定需求场景下更灵活更方便的选择,Lock本身也有其缺点,比如语法上需要try-finally支持,用法复杂,提高了使用的学习成本和门槛,需要开发者维护,使用风险高,而且性能上靠JUC代码实现来维护等,而内在锁保持了原有代码的兼容并且性能可以随着JVM实现的改进优化提高,可谓各有所长,而且在能够满足需求的情况下,应当优先选择synchronzied内在锁。
对于Java的java.util.concurrent.locks中的详细锁实现,我会在后面的文章详细给出源码要点分析。
2. 有了锁,我们解决了线程安全方面的问题,但并不是所有的并发需求仅仅用锁就能得到解决,下面我们说说状态依赖和条件队列。
比如在线程协作方面经典的案例“生产者-消费者”。在生产者和消费者之间有一个循环的传送带,生产者生产出产品消费者才能消费,消费者将传送带上的产品有消费,生产者才能生产新产品并放到传送带上。对于传送带来讲,它是生产者和消费者共享的,而且有满和空两种状态,如果满了则生产者需要停下来,如果空了消费者也没有可消费的产品。实际上,传送带既然是两者共享,则需要加锁使用保证线程安全和状态一致性,而生产者和消费者又同时依赖于传送带的状态。那么生产者或者消费者发现传送带状态不满足的情况下,需要释放锁,因为只有这样才能让对方来处理,只有这样才能使不满足的状态得到改变,有机会满足所需的状态。
一个简单的解决方案就是循环检查状态是否满足。假如有两个线程P和C,分别代表生产者和消费者,而一个数据结构Q代表传送带。对于P,需要获得Q的锁,循环判断Q是否为满,未满则可以继续执行,否则释放锁,继续循环判断。C也如此,只不过条件是Q未空。
对于处理器来说,循环的判断是十分消耗计算资源的。为了解决这个问题,我们可以让线程P和C每次尝试失败后释放锁并等待一段时间,等计时器到了之后再重新尝试获取锁并判断。这样至少看起来比前一种更有效果,但有至少有如下两个问题:
- 在通常的实现中,线程不断的重复切换和睡眠唤醒状态的调度是对性能有损耗的
- 睡眠(等待)时间的长短设定是一个问题,过短则和前一种情况没什么区别,徒增了一些调度开销,而过长会出现响应度低,延迟过长
那么有没有一种更有效的解决方案使得这装状态依赖的线程间协作更有效率呢?那就是条件队列。当一个线程发现自己不满足条件时,将其挂载到某个条件下的队列中,直到条件满足时得到系统的通知。这样的方式就避免了对线程不必要的唤醒、锁获取和检查。当然,要做到这些需要底层的支持。在java.lang.Object中,采用native的方式实现了wait()/notify()/notifyAll()方法。这三个方法结合操作系统的实现给我们使用条件队列提供了方便。wait()所做的就是释放锁,等待条件发生,当前线程阻塞。notify()/notifyAll()则是通知满足条件的队列中的线程,将其唤醒。
类似的java.util.concurrent.locks中也给出了对应的实现,就是Condition,提供了更灵活的方法await()/signal()/signalAll()。下面一起说下线程协同需要注意的地方:
- wait()/notify()/notifyAll()和await()/signal()/signalAll()都是会自动释放锁的,这就要求他们的执行必需在已经获得锁的代码块中
- 为了不错过通知信号,通常wait()会包含在一个循环中,而循环的条件往往就是可以继续向下执行的条件
- 对于JVM内在实现的条件队列,是和synchronized内在锁绑定的,只能对一个对象wait(),但条件可能是各种各样,这个就需要在while循环的条件中做不同的判断
- notify()/notifyAll()的对比,后者更安全,保证能够被通知到,前者更有效率,不做不必要的唤醒。通常的建议是,除非保证等待条件和后续任务处理只有一类并且每次只需要一个线程被唤醒,否则优先考虑notifyAll(),惯例这样做是为了保证逻辑的正确性
- Condition的await()/signal()和Object的wait()/notify()相比,是Java代码实现的框架,解开了一个对象只有一个条件队列的对应关系,可以根据需求调用Lock的newCondition()方法,每次调用开启一个新的条件队列,提高了队列的精确性
- synchronized和wait()/notify()都是JVM的内在实现,是绑在一起的;Condition及其队列操作是基于AQS实现的,在SunJDK实现中用到了sun.misc.Unsafe类的park()和unpark()方法;内在条件队列与j.u.c的Condition实现的选择理由和synchronized和Lock的对比基本一致
线程协同和条件队列就先说到这里。
3. 回头再看看锁的实现,简要比较和分析一下内在锁和Lock框架的实现原理和理念。对于通常情况下的锁,我们把一个对象锁住,使得其他线程没有机会获得锁从而不能做加锁才能进行的操作,更重要的,对这些线程进行阻塞,这种锁我们可以称其为“悲观锁”,就是不能背弃锁而做锁后的操作,只能阻塞。我们知道,Java的线程实现最终都是基于硬件和操作系统平台之上的,这种阻塞和唤醒开销都是非常大的。
与其对应,有一种所叫做“乐观锁”,基于冲突检测的道理。我们尝试不考虑加锁直接去做锁后的操作,操作修改时做一个对比,如果没有问题直接就改了,没有明显的加锁过程,如果对比发生了变化,也就意味着其他线程做了修改,则这个操作失败,做失败后的处理,可以考虑循环尝试。其实在ReentrantLock的tryLock()中,就是用sun.misc.Unsafe的compareAndSwapInt()方法调用,这里的实现就有了“乐观锁”的味道。另外,在java.util.concurrent.atomic中的大部分类都是基于乐观锁的思路做出实现。
显然考虑到系统底层的阻塞和唤醒的成本考虑,乐观锁通常会比悲观锁效率更好一些。
另外,对于synchronized和java.util.concurrent.locks包中的Lock实现的性能对比,在JDK1.5之前,并发量较大的时候,后者明显优于前者。但JavaSE6之后,JDK的实现对内在锁做了很大优化,单纯在性能方面的考虑,两种锁实现已经没有绝对的优劣差异了。
4. 下面就说说JavaSE6中的一些锁优化方案。
- 自旋锁。其实自旋简单理解就是线程不停地自己循环尝试获得锁,而非自旋锁未能获得的情况下一般是阻塞掉,之后再唤醒尝试获得锁。上面提到阻塞和唤醒是基于底层实现的,是有成本的。那在一定程度上,自旋虽然有计算资源上的损失但综合考虑这个成本,在多次自选后未能获得锁后再为避免浪费处理器计算资源将其阻塞掉。在JDK1.6之后,引入了自适应的概念,这点得到了一定优化。
- 锁消除。锁可能会带来阻塞等成本,那么没有锁自然就没有这些成本,在JDK判断得到对象只被单线程安全使用的情况下会把锁消除掉,以削减锁成本。
- 锁粗化。通常我们认为锁的粒度越小越好,以减少对不必要锁住的资源锁住,但有些情况刚好相反,锁过于细会导致一系列操作反复加锁解锁加锁解锁,在这种情况下,我们可以统一加锁一次解锁一次,削减了不必要的加锁解锁带来的成本。
- 轻量级锁和偏向锁则是在考虑仅仅在无竞争条件下的特殊优化处理,在发现竞争的时候又恢复到“重量级”锁状态,要结合情况考虑是否采用。更详细的原理这里不展开介绍。
5. 在这篇文章的最后,重申一下对于线程协同需求场景的处理。从前文状态依赖和线程协同介绍中,大家可以看到条件队列的实际使用细节还是蛮多的,很容易出现问题。
“工欲善其事,必先利其器”,我们实际上站在“巨人的肩膀”。java.util.concurrent中已经有很好的工具,比如各类BlockingQueue实现。另外也可以考虑用管道等机制来解决我们的需求,这样就免去了我们在使用条件队列面临的各类细节技术问题,提高解决问题的效率。
相关推荐
《实战Java虚拟机——JVM故障诊断与性能优化》内容简介:随着越来越多的第三方语言(Groovy、Scala、JRuby等)在Java虚拟机上运行,Java也俨然成为一个充满活力的生态圈。本书将通过200余示例详细介绍Java虚拟机中的...
在Java编程语言中,线程是程序执行流的最小单元,一个...理解线程的基础概念、创建方式、状态转换以及线程间的同步与通信机制对于编写高质量的Java应用程序至关重要。希望本文能为你理解和运用Java线程提供一定的帮助。
在这个未完成的案例中,我们可能正在探讨如何在Java中创建和管理线程,以及处理多线程环境下的并发问题。下面是对Java多线程基础知识的详细解释: 1. **线程的创建方式**: - 继承`Thread`类:自定义一个新的类,...
本教程将深入讲解Java线程的相关知识,包括进程与线程的基本概念、线程的创建和启动、多线程的互斥与同步、线程状态和线程控制以及死锁的概念。 首先,我们要理解进程与线程的区别。进程是操作系统资源分配的基本...
在Java多线程编程中,线程安全是一个关键概念,特别是在并发访问共享资源时。"线程八锁案例分析"文档中的示例着重展示了`synchronized`关键字如何在控制线程同步方面发挥作用。以下是对这些案例的详细分析: 案例1...
Java 中的多线程编程需要充分考虑线程安全和锁机制的问题,否则可能会导致程序的执行不稳定和崩溃。Lock 机制是 Java 中的一种重要的线程同步机制,它可以用来实现线程安全和提高程序的执行效率。
本文将详细介绍 Java 线程状态转换图,包括初始状态、可运行状态、运行状态、阻塞状态、锁池状态、等待队列状态和终止状态七种状态的定义、特点和转换关系。 初始状态(Newborn) * 线程的实现有两种方式,一是...
Java线程安全与锁是多线程编程中的关键概念,主要涉及到并发环境下对共享资源的访问控制。在Java中,线程安全问题主要是由于多个线程同时访问并修改同一份数据,导致数据不一致或者出现意外的行为。为了解决这个问题...
很好的JAVA多线程实例,方便初学都学习。
它教导我们如何通过条件等待和信号机制实现线程间的协同工作,以及如何利用锁对象来保证数据的正确性和线程安全。通过学习这个示例,开发者能够更好地理解和运用Java的并发编程技术,提高多线程应用程序的设计和性能...
3. Lock接口与ReentrantLock类:提供比synchronized更细粒度的锁控制,支持公平锁和非公平锁,以及可中断和定时等待的获取锁机制。 4. wait(), notify()和notifyAll():在synchronized代码块内使用,用于线程间的...
### JAVA线程安全及性能优化的关键知识点 ...通过以上讨论,我们可以看出Java线程安全与性能优化是一个复杂而重要的主题,需要开发者深入理解Java内存模型,并灵活运用各种工具和技术来确保程序既正确又高效地运行。
线程,同步与锁————Lock你到底锁住了谁?.htm
《实战Java虚拟机——JVM故障诊断与性能优化》是一本深入探讨Java开发中的关键环节——Java虚拟机(JVM)的专著。本书聚焦于实际应用中的问题解决和性能调优,对于Java开发者和系统管理员来说,是提升技术水平的重要...
Java 多线程编程应用场景 —— 电影院售票系统设计 本资源摘要信息将对 Java 多线程编程在电影院售票系统设计中的应用进行详细介绍。该系统模拟了电影院三个售票窗口同时出售电影票的过程,通过 Java 多线程编程...
这个名为"Java多线程的小例子——吃包子"的示例,旨在帮助开发者直观地理解多线程的工作原理。下面我们将深入探讨该示例所涉及的核心知识点。 首先,多线程通常涉及到以下几个关键概念: 1. **线程(Thread)**:...
在Java编程语言中,多线程是并发执行多个任务或操作的一种方式,这对于优化系统性能、提高资源利用率具有重要意义。...对于开发者来说,理解和熟练掌握`synchronized`是编写高效、安全的多线程Java程序的基础。
本主题聚焦于“java线程应用——排序过程动态显示”,通过源码和示例程序来阐述如何利用线程来实时展示排序算法的动态过程。 首先,排序过程动态显示意味着我们将使用线程来逐个步骤地执行排序算法,同时更新用户...
虽然Java提供了线程优先级(`Thread.setPriority()`),但其具体行为依赖于操作系统的实现,通常并不推荐过度依赖优先级来控制线程执行顺序。 总之,理解和熟练运用这些Java多线程操作对于编写高效、稳定的并发...
总的来说,理解和掌握Java线程的创建、状态管理、同步机制和线程安全是进行多线程编程的基础,这对于开发高效、稳定的并发程序至关重要。在实际编程中,应充分利用Java提供的工具和机制,避免潜在的并发问题,提升...