总结synchronized

synchronized的特性

synchronized具有原子性、可见性、有序性、可重入性

原子性(Atomicity)

在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。【一次操作,要么完全成功,要么完全失败】

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

可见性(Visibility)

是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值。

synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。

有序性(Ordering)

是指程序中代码的执行顺序,Java在编译时运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

ynchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。

可重入性:

synchronized的可重入性就是当一个线程调用synchronized代码持有对象锁的时候,如果调用了该对象的其他synchronized代码,那么可以重新持有该锁,即同一个线程可以获取同一把锁多次,所以synchronized具有可重入性。

原理

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,知道计数器的数量为0,就释放锁。

好处

  • 可以避免死锁
  • 可以让我们更好的来封装代码

不可中断特性 

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。


synchronized的使用

synchronized关键字最主要的三种使用方式:

// 加锁方式 ,当前实例 ,当前class , 自定义object
> synchronized(this)
> synchronized(object)
> synchronized(class) 或者静态代码块
            

修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

修饰静态方法

作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。

也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。
所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。


修饰代码块

指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

  • 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。
  • synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。

这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。
另外需要注意的是:尽量不要使用 synchronized(String a), 部分字符串常量会缓冲到常量池里面, 不过可以试试 new String(“a”)

synchronized的锁机制

CAS

CAS:Compare And Swap(比较相同再交换),CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。


CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  1. 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

注意: 该指令是是原子性的, 也就是说 CPU 执行该指令时, 是不会被中断执行其他指令的
先修知识 3: “CAS”实现的"无锁"算法常见误区

  • 误区一: 通过简单应用 “比较后再赋值” 的操作即可轻松实现很多无锁算法

CAS 指令的一个不可忽略的特征是原子性。 在 CPU 层面, CAS 指令的执行是有原子性语义保证的, 如果 CAS 操作放在应用层面来实现, 则需要我们自行保证其原子性。 否则就会发生如下描述的问题:

// 下列的函数如果不是线程互斥的, 是错误的 CAS 实现
function cas( p , old , new) returns bool {
    if *p ≠ old { // 此处的比较操作进行时, 可以同时有多个线程通过该判断
        return false
    }
    *p ← new // 多个线程的赋值操作会相互覆盖, 造成程序逻辑的错误
    return true
}
  • 误区二: CAS 操作的 ABA 问题
  •  大部分网络博文对 ABA 问题的常见描述是: 应用 CAS 操作时, 目标地址的值刚开始为 A, 工作线程/进程 读取后, 进行了一系列运算, 计算得出了新值 C, 在此期间, 目标地址的值被其他线程已经进行了不止一次修改, 其值已经从 A 被改为 B , 又改回 A, 此时便会发生同步问题。

  • 上面的描述是其实是错误的, 思考一下就会发现, 如果工作线程的操作目的是将目标地址的值从 A 改为 C, 那么即便在这期间目标地址的值经过了其他线程或进程的多次修改, 其语义依旧是正确的。
  • 例如目前要将某银行账号的余额扣除 50, 通过 CAS 保证同步 :
  1. 首先读取原有余额为 100 ,
  2. 计算余额应该赋值为 100 - 50 = 50
  3. 此时该线程被挂起, 该账户同时又发生了转入 150 和转出 150 的操作, 余额经历了 100 -》250 -》100 的变动
  4. 线程被唤醒, 进行 CAS 赋值操作 cas(p, 100, 50) , 正常得以执行。
  5. 该账户的余额依旧是正确的
  • 通过上述例子就可以发现, ABA 的问题并不在于多次修改。 查阅一下 CAS 的 wiki 解释, 就会发现, ABA 真正的问题是, 假如目标地址的内容被多次修改以后, 虽然从二进制上来看是依旧是 A, 但是其语义已经不是 A 。例如, 发生了整数溢出, 内存回收等等。 

每个线程都有自己独立的内存空间, 栈帧就是其中的一部分。里面可以存储仅属于该线程的一些信息。 

对象布局

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充

对象头由两部分组成:一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是 类型指针 ,及对象指向它的类元数据的指针。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。

64位虚拟机下,Mark Word结构

klass pointer
用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的

实例。。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;


实例数据
类中定义的成员变量。


对齐填充
仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

synchronized 关键字之锁的升级(偏向锁->轻量级锁->重量级锁)
Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制

  • 偏向锁(Biased Lock )
  • 轻量级锁( Lightweight Lock)
  • 重量级锁(Heavyweight Lock)
偏向锁

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁[1]、锁标志位[01]以及ThreadID即可。

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

偏向锁原理:
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下

虚拟机将会把对象头中的标志位设为“01”,即偏向模式。

同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

如果此时多个线程竞争,则需撤销偏向锁,流程如下:

  1. 偏向锁的撤销动作必须等待全局安全点

  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态 。

  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

小结

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以 提高带有同步但无竞争的程序性能。如果此时有不同线程访问,偏向模式不再适合。

轻量级锁

多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

轻量级锁原理:

关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧【一个进入栈中的方法】中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象。
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

如果此时,多个线程存在竞争关系,便要释放锁,流程如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。
  4. 在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。
小结

多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁。

锁粗化

  • 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗.

锁粗化:

  • JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。
重量级锁

当线程的自旋次数过长依旧没获取到锁,为避免CPU无端耗费,锁由轻量级锁升级为重量级锁。获取锁的同时会阻塞其他正在竞争该锁的线程,依赖对象内部的监视器(monitor)实现,monitor又依赖操作系统底层,需要从用户态切换到内核态,成本非常高。

为什么成本高?

 当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。


平时写代码如何对synchronized优化

(1)减少synchronized的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

(2)降低synchronized锁的粒度

将一个锁拆分为多个锁提高并发度。

(3)读写分离

读取时不加锁,写入和删除时加锁。如ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

参考

☆啃碎并发(七):深入分析Synchronized原理 - 简书

深入详解Synchronized同步锁的底层实现 - 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值