java多线程:synchronized和lock比较浅析

简介: 转载:http://www.toutiao.com/a6392135944652587266/?tt_from=weixin&utm_campaign=client_share&app=news_article&utm_source=weixin&iid=7704173001&utm_medium=toutiao_ios&wxshare_count=1     synchronized是基于jvm底层实现的数据同步,lock是基于Java编写,主要通过硬件依赖CPU指令实现数据同步。

转载:http://www.toutiao.com/a6392135944652587266/?tt_from=weixin&utm_campaign=client_share&app=news_article&utm_source=weixin&iid=7704173001&utm_medium=toutiao_ios&wxshare_count=1

 

 

synchronized是基于jvm底层实现的数据同步,lock是基于Java编写,主要通过硬件依赖CPU指令实现数据同步。下面一一介绍

一、synchronized的实现方案

1.synchronized能够把任何一个非null对象当成锁,实现由两种方式:

a.当synchronized作用于非静态方法时,锁住的是当前对象的事例,当synchronized作用于静态方法时,锁住的是class实例,又因为Class的相关数据存储在永久带,因此静态方法锁相当于类的一个全局锁。

b.当synchronized作用于一个对象实例时,锁住的是对应的代码块。

2.synchronized锁又称为对象监视器(object)。

3.当多个线程一起访问某个对象监视器的时候,对象监视器会将这些请求存储在不同的容器中。

>Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中

>Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中

>Wait Set:哪些调用wait方法被阻塞的线程被放置在这里

>OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck

>Owner:当前已经获取到所资源的线程被称为Owner

> !Owner:当前释放锁的线程

下图展示了他们之前的关系

4.synchronized在jdk1.6之后提供了多种优化方案:

>自旋锁

jdk1.6 之后默认开启,可以使用参数-XX:+UseSpinning控制,自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换 的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时候很长,那么自旋的线程只会白白消 耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。

自旋锁的本质:执行几个空方法,稍微等一等,也许是一段时间的循环,也许是几行空的汇编指令。

>锁消除

即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持,那么是什么是逃逸分析?对于虚拟机来说需要使用数据流分析来确定是否消除变量底层框架的同步代码,因为有许多同步的代码不是自己写的。

例1.1

public static String concatString(String s1, String s2, String s3) {  
    return s1 + s2 + s3;  
}  

由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append 操作,这里的stringBuilder.append是线程不同步的(假设是同步)。

Javac 转化后的字符串连接代码为:

public static String concatString(String s1, String s2, String s3) {  
    StringBuffer sb = new StringBuffer;  
    sb.append(s1);  
    sb.append(s2);  
    sb.append(s3);  
    return sb.toString;  
}  

此时的锁对象就是sb,虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 到concatString 方法之外,其他线程无法访问到它,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

>锁粗化

将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

>轻量级锁

加 锁过程:在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,这时候线程堆栈与对象头的状态如图 13-3 所示

然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位 (Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态,这时线程堆栈与对象头的状态如图13-4

如果上述更新操作失败,则说明这个锁对象被其他锁占用,此时轻量级变为重量级锁,标志位为“10”,后面等待的线程进入阻塞状态。

解 锁过程:也是由CAS进行操作的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。

轻 量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

>偏向锁

偏向锁也 是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。

实质就是设置一个变量,判断这 个变量是否是当前线程,是就避免再次加锁解锁操作,从而避免了多次的CAS操作。坏处是如果一个线程持有偏向锁,另外一个线程想争用偏向对象,拥有者想释 放这个偏向锁,释放会带来额外的性能开销,但是总体来说偏向锁带来的好处还是大于CAS的代价的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。

二、lock的实现方案

与 synchronized不同的是lock是纯java手写的,与底层的JVM无关。在java.util.concurrent.locks包中有很多 Lock的实现类,常用的有ReenTrantLock、ReadWriteLock(实现类有ReenTrantReadWriteLock)

,其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类(简称AQS),实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。

分 析之前我们先来花点时间看下AQS。AQS是我们后面将要提到的CountDownLatch/FutureTask/ReentrantLock /RenntrantReadWriteLock/Semaphore的基础,因此AQS也是Lock和Excutor实现的基础。它的基本思想就是一个 同步器,支持获取锁和释放锁两个操作。

要支持上面锁获取、释放锁就必须满足下面的条件:

1、 状态位必须是原子操作的

2、 阻塞和唤醒线程

3、 一个有序的队列,用于支持锁的公平性

场景:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。

主要从以下几个特点介绍:

1.可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

2.可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

3.公平锁和非公平锁

公 平锁以请求锁的顺序来获取锁,非公平锁则是无法保证按照请求的顺序执行。synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对 于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

参数为true时表示公平锁,不传或者false都是为非公平锁。

ReentrantLock lock = new ReentrantLock(true);

4.读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock获取读锁,通过writeLock获取写锁。

三、总结

1.synchronized

优点:实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更广泛

缺点:悲观的排他锁,不能进行高级功能

2.lock

优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁

缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪

3.相同点

都是可重入锁

参考文章:http://www.cnblogs.com/longshiyVip/p/5213771.html

相关文章
|
10天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
4天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
9天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
|
9天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
38 1
|
17天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
17天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
51 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
21 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
32 2