关键字 synchronized 简单介绍

本文概述

讲解下 Java 中和并发相关的个关键字:synchronizedvolatile

因为关键字已经是除了 JVM 外最底层的应用了。。。

所以除了分析在 JVM 中关键字的作用,其他的只能通过代码举例说明,这样就会显得比较得…枯燥。


synchronized

synchronized 关键字是为了解决共享资源竞争的问题,共享资源一般是以对象形式存在的内存片段。

所以,只有共享资源的读写访问才需要同步化,如果不是共享资源那么根本就没有必要同步。

对 synchronized 的基础使用其实比较简单:

  • 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
  • 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁;
  • synchronized 修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。

使用方法

synchronized 可修饰的对象如下:

修饰目标
实例方法当前实例对象(即方法调用者)
静态方法类对象
this当前实例对象(即方法调用者)
class 对象类对象
任意 Object 对象任意示例对象

简单的使用就不详细讲了。


原理分析

举个例子,如下 mian 方法代码:

   public static void main(String[] args) {
        synchronized (new Object()) {
            System.out.println(1);
        }
    }

使用 jclasslib 查看字节码,main 方法字节码如下:

 0 new #2 <java/lang/Object>
 3 dup
 4 invokespecial #1 <java/lang/Object.<init>>
 7 dup
 8 astore_1
 9 monitorenter
10 getstatic #3 <java/lang/System.out>
13 iconst_1
14 invokevirtual #4 <java/io/PrintStream.println>
17 aload_1
18 monitorexit
19 goto 27 (+8)
22 astore_2
23 aload_1
24 monitorexit
25 aload_2
26 athrow
27 return

可以发现,有关监视器的几行命令:

9 monitorenter
......
18 monitorexit
......
24 monitorexit

MonitorenterMonitorexit 指令,会让对象在执行,使其锁计数器加1或者减1。

每一个对象在同一时间只与一个 monitor(锁) 相关联,而一个 monitor 在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的 Monitor 锁的所有权的时候,monitorenter 指令会发生如下3中情况之一:

  • monitor 计数器为 0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个 monitor 已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  • 这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于 monitor 的所有权,释放过程很简单,就是讲 monitor 的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该 monitor 的所有权,即释放锁。

下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:

线程监视器示意图


特性描述

  1. 可重入特性:一个线程可以多次执行 synchronized ,重复获取同一把锁。

Synchronized 先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。


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

JDK6 的优化

JDK 1.5 之前都只有 监视器 这一个重量级锁。JDK和开发都会大量使用。

JDK 1.6 的时候进行大量的改进,锁升级的过程为:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁


CAS
  • CAS 的全名是:Compare And Swap ,比较再交换。它是现在 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
  • CAS 可以将比较和交换转化为原子操作,这个原子操作直接由 CPU 保证。
  • JAVA 中的 CAS 实现例如:AtomicInteger

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。

因此 synchronized 我们也将其称之为 悲观锁,JDK中的ReentrantLock也是一 种悲观锁。 性能较差!


乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁。

但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。

CAS这种机制我们也可以将其称之为乐观锁。 综合性能较好!


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

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。

  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。(可以阅读 AtomicInteger 源码获得解释)


偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

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

偏向锁,仅限在不存在竞争的情况下。


轻量级锁

轻量级锁是 JDK1.6 之中加入的新型锁机制。

它名字中的"轻量级”是相对于使用 monitor 的传统锁而言的,因此传统的锁机制就称为 “重量级” 锁。

首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

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


自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成 busy-waiting。

  • 使用自旋锁会有以下一个问题:
  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

  • 自旋锁的优点
  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

锁消除

锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。

锁消除可以节省毫无意义的请求锁的时间。

举个例子:

public static void main(String[] args) {
    appendStr("1", "2", "3");
}

public static String appendStr(String str1, String str2, String str3) {
    return new StringBuffer().append(str1).append(str2).append(str3).toString();
}

StringBuffer 的 append 方法是一个同步方法:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

appendStr 方法中,锁头是 this(new StringBuffer) ,同时,它是局部变量,不存在竞争。

所以,JVM 会自动消除锁。

再详细点,这个消除步骤是,即时编译器(JIT)在进行逃逸分析时,进行的优化。


锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,—直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

public static void main(String[] args) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 100; i++) {
        sb.append(i);
    }
    System.out.println(sb.toString());
}

简单讲就是:即时编译器将 append 内的锁消除,在 for 循环上加一个锁


代码优化

优化注意点原因
同步代码块内代码尽量少执行越快,单位时间内等待时间越短,竞争越少。
自旋锁或者轻量级锁就能满足要求,不需要升级。
将一个锁拆分为多个锁例如,HashTable 和 ConcurrenHashMap ,原理和上一条相似。
读写分离读取不加锁,写入和删除加锁

参考文章

https://www.pdai.tech/md/java/thread/java-thread-x-key-synchronized.html

https://www.pdai.tech/md/java/thread/java-thread-x-key-volatile.html

https://www.pdai.tech/md/java/thread/java-thread-x-key-final.html

https://www.bilibili.com/video/BV1QC4y1H7qd?p=11

https://blog.csdn.net/topdeveloperr/article/details/80485900

https://blog.csdn.net/qq_38011415/article/details/89047812

https://blog.csdn.net/weixin_42762133/article/details/103241439

https://blog.csdn.net/javazejian/article/details/72828483

Java面试热点问题,synchronized原理剖析与优化_哔哩哔哩 (゜-゜)つロ 干杯

https://www.jianshu.com/p/9d3660ad4358?utm_source=oschina-app

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 中的 synchronized 关键字是用来实现线程同步的机制。当多个线程访问共享资源时,会发生竞争条件(race condition),导致数据的不一致性和程序的错误行为。synchronized 关键字可以保证在同一时刻只有一个线程访问共享资源,从而避免竞争条件的出现。 Javasynchronized 关键字的使用有两种方式:synchronized 方法和 synchronized 代码块。 1. synchronized 方法 synchronized 方法是一种简单的方式,它可以保证同一时刻只有一个线程能够访问该方法。在 synchronized 方法中,synchronized 关键字修饰整个方法,表示该方法是一个同步方法。当一个线程进入该方法时,它会尝试获取该方法的锁,如果该锁已经被其他线程占用,则该线程会被阻塞,直到获取到该方法的锁为止。 例如: ``` public synchronized void method() { // 该方法是同步方法 // 在进入该方法前,线程会尝试获取该方法的锁 // 如果该锁已经被其他线程占用,则该线程会被阻塞 // 直到获取到该方法的锁为止 } ``` 2. synchronized 代码块 synchronized 代码块是另一种方式,它可以对指定的对象或类进行加锁。在 synchronized 代码块中,synchronized 关键字修饰一个对象或类,表示该对象或类是同步锁。当一个线程进入该代码块时,它会尝试获取该对象或类的锁,如果该锁已经被其他线程占用,则该线程会被阻塞,直到获取到该锁为止。 例如: ``` public void method() { synchronized (this) { // 该代码块是同步代码块 // 在进入该代码块前,线程会尝试获取 this 对象的锁 // 如果该锁已经被其他线程占用,则该线程会被阻塞 // 直到获取到该锁为止 } } ``` 需要注意的是,synchronized 代码块和 synchronized 方法都是可重入锁,即同一个线程可以多次获取同一个锁。 除了使用 synchronized 关键字Java 还提供了其他的线程同步机制,如 Lock 和 Condition。这些机制相比 synchronized 更加灵活,但同时也更加复杂。在实际开发中,应根据实际情况选择不同的线程同步机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值