聊聊Synchronized、原理以及优化

Synchronized关键字

一、什么是Synchronized

synchronized是Java中的关键字,是一种同步锁。

二、为什么会出现Synchronized

在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatil

三、Synchronized怎么使用

Java中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:

  • 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
// 实例方法,锁的是该类的实例对象
public synchronized void function() {
    // ...
}
  • 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
// 静态方法,锁的是类对象
public static synchronized void function() {
    // ...
}
  • 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
// A. 同步代码块,锁的是该类的实例对象
synchronized (this) {
    // ...
}

// B. 同步代码块,锁的是该类的类对象
synchronized (Test.class) {
    // ...
}

// C. 同步代码块,锁的是自定义的实例对象
// String 对象作为锁
String lock = "";
synchronized (lock) {
    // ...
}
四、Synchronized原理

下面我们来看看为什么Synchronized可以实现同步,保证线程安全。

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现 。

对于执⾏同步代码块⾸先要先执⾏monitorenter指令,退出的时候monitorexit指令。使⽤Synchronized进⾏同步,其关键就是必须要对对象的监视器monitor进⾏获取,当线程获取 monitor后才能继续往下执行,否则就只能等待。⽽这个获取的过程是互斥的,即同⼀时刻只有⼀个线程能够获取到monitor。

一个字节码文件中包含⼀个monitorenter指令以及多个monitorexit指令。这是因为 Java虚拟机需要确保所获得的锁在正常执⾏路径,以及异常执⾏路径上都能够被解锁。

对于同步方法:当⽤ synchronized 标记⽅法时,你会看到字节码中⽅法的访问标记包括 ACC_SYNCHRONIZED。该标 记表示在进⼊该⽅法时,Java 虚拟机需要进⾏ monitorenter 操作。而在退出该方法时,不管是正常返回,还是向调⽤者抛异常,Java 虚拟机均需要进⾏ monitorexit 操作。

这里 monitorenter 和 monitorexit 操作所对应的锁对象是隐式的。对于实例⽅法来说,这两个操作对

应的锁对象是 this;对于静态⽅法来说,这两个操作对应的锁对象则是所在类的 Class 实例。

关于 monitorenter 和 monitorexit 的作⽤,我们可以抽象地理解为每个锁对象拥有⼀个锁计数器和⼀

个指向持有该锁的线程的指针。

当执⾏ monitorenter 时,如果⽬标锁对象的计数器为 0,那么说明它没有被其他线程所持有。在这个

情况下,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。

在⽬标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将

其计数器加 1,否则需要等待,直⾄持有线程释放该锁。

当执⾏ monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。当计数器减为 0 时,那便代表该锁已 经被释放掉了。

之所以采⽤这种计数器的⽅式,是为了允许同⼀个线程重复获取同⼀把锁。举个例⼦,如果⼀个 Java

类中拥有多个 synchronized ⽅法,那么这些⽅法之间的相互调⽤,不管是直接的还是间接的,都会涉

及对同⼀把锁的重复加锁操作。因此,我们需要设计这么⼀个可重⼊的特性,来避免编程⾥的隐式约

束。

五、Synchronized的优化

为什么需要优化?

因为在JDK1.5中,synchronized是性能低效的。因为这是⼀个重量级操作,它对性能最⼤的影响是阻塞的是 实现,挂起线程和恢复线程的操作都需要转⼊内核态中完成,这些操作给系统的并发性带来了很⼤的压 ⼒。相⽐之下使⽤Java提供的Lock对象,性能更⾼⼀些。

到了JDK1.6,发⽣了变化,对synchronize加⼊了很多优化措施,有⾃适应⾃旋,锁消除,锁粗化,轻 量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不⽐Lock差。官⽅也表示,他们也更⽀持 synchronized,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优 先考虑使⽤synchronized来进⾏同步。

现在我们对Synchronized应该有所印象了,它最⼤的特征就是在同⼀时刻只有⼀个线程能够获得对象的 监视器(monitor),从⽽进⼊到同步代码块或者同步⽅法之中,即表现为互斥性(排它性)。这种⽅ 式肯定效率低下,每次只能通过⼀个线程,既然每次只能通过⼀个,这种形式不能改变的话,那么我们 能不能让每次通过的速度变快⼀点呢。

在聊到锁的优化也就是锁的⼏种状态前,有两个知识点需要先关注:(1)CAS操作 (2)Java对象头, 这是理解下⾯知识的前提条件。

CAS

A : 什么是CAS

使⽤锁时,线程获取锁是⼀种悲观锁策略,即假设每⼀次执⾏临界区代码都会产⽣冲突,所以当前线程 获取到锁的时候同时也会阻塞其他线程获取该锁。⽽CAS操作(⼜称为⽆锁操作)是⼀种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突⾃然⽽然就不会阻塞其他线程的 操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?⽆锁操作是使 ⽤**CAS(compare and swap)**⼜叫做⽐较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到 没有冲突为⽌。

B : CAS的操作过程

CAS⽐较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O

预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没 有被其他线程更改过,即该旧值O就是⽬前来说最新的值了,⾃然⽽然可以将新值N赋值给V。反之,V 和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给 V,返回V即可。当多个线程使⽤CAS操作⼀个变量是,只有⼀个线程会成功,并成功更新,其余会失 败。失败的线程会重新尝试,当然也可以选择挂起线程。

CAS的实现需要硬件指令集的⽀撑,在JDK1.5后虚拟机才可以使⽤处理器提供的CMPXCHG指令实现。

元⽼级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁 带来的性能问题,因为这是⼀种互斥同步(阻塞同步)。⽽CAS并不是武断的将线程挂起,当CAS操作 失败后会进⾏⼀定的尝试,⽽⾮进⾏耗时的挂起唤醒的操作,因此也叫做⾮阻塞同步。这是两者主要的 区别。

C : CAS 出现的问题

  • ABA 问题:因为CAS会检查旧值有没有变化,这⾥存在这样⼀个有意思的问题。⽐如⼀个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发⽣了 变化。解决⽅案可以沿袭数据库中常⽤的乐观锁⽅式,添加⼀个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。

  • ⾃旋会浪费⼤量的处理器资源

    与线程阻塞相⽐,⾃旋会浪费⼤量的处理器资源。这是因为当前线程仍处于运⾏状况,只不过跑的是⽆ ⽤指令。它期望在运⾏⽆⽤指令的过程中,锁能够被释放出来。

    我们可以⽤等红绿灯作为例⼦。Java 线程的阻塞相当于熄⽕停⻋,⽽⾃旋状态相当于怠速停⻋。如果红灯的等待时间⾮常⻓,那么熄⽕停⻋相对省油⼀些;如果红灯的等待时间⾮常短,⽐如我们在同步代码块中只做了⼀个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停⻋更合适。

    然⽽,对于JVM来说,它并不能看到红灯的剩余时间,也就没法根据等待时间的⻓短来选择是⾃旋还是 阻塞。JVM给出的⽅案是⾃适应⾃旋,根据以往⾃旋等待时能否获取锁,来动态调整⾃旋的时间(循环数)。

    就我们的例⼦来说,如果之前不熄⽕等待了绿灯,那么这次不熄⽕的时间就⻓⼀点;如果之前不熄⽕没 等待绿灯,那么这次不熄⽕的时间就短⼀点。

  • 公平性

    ⾃旋状态还带来另外⼀个副作⽤,不公平的锁机制。处于阻塞状态的线程,⽆法⽴刻竞争被释放的锁。然⽽,处于⾃旋状态的线程,则很有可能优先获得这把锁。

Java虚拟机中synchronized关键字的实现,按照代价由⾼到低可以分为重量级锁、轻量锁和偏向锁三 种。

1、重量级锁会阻塞、唤醒请求加锁的线程它针对的是多个线程同时竞争同⼀把锁的情况。 JVM采⽤了⾃适应⾃旋,来避免线程在⾯对⾮常⼩的synchronized代码块时,仍会被阻塞、 唤醒的情况。

2、轻量级锁采⽤CAS操作,将锁对象的标记字段替换为⼀个指针,指向当前线程栈上的⼀块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同⼀把锁的情况。

3、偏向锁只会在第⼀次请求时采⽤CAS操作,在锁对象的标记字段中记录下当前线程的地址。 在之后的运⾏过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同⼀线程持有的情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值