详解Java多线程锁之synchronized

synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

synchronized的四种使用方式

  1. 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用于调用对象
  2. 修饰方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用于调用对象

    注意:synchronized修饰方法时必须是显式调用,如果没有显式调用,例如子类重写该方法时没有显式加上synchronized,则不会有加锁效果。

  3. 修饰静态方法:其作用的范围是整个静态方法,作用于所有对象
  4. 修饰类:其作用的范围是synchronized后面括号括起来的部分(例如:test.class),作用于所有对象

对象锁和类锁是否会互相影响么?

  • 对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
  • 类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。

类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

对应的实验代码如下:

@Slf4j
public class SynchronizedExample {

    // 修饰一个代码块
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修饰一个方法
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    // 修饰一个类
    public static void test3(int j) {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test3 {} - {}", j, i);
            }
        }
    }

    // 修饰一个静态方法
    public static synchronized void test4(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test4 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample example1 = new SynchronizedExample();
        SynchronizedExample example2 = new SynchronizedExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
    }
}

在JDK1.6之前,synchronized一直被称呼为重量级锁(重量级锁就是采用互斥量来控制对资源的访问)。通过反编译成字节码指令可以看到,synchronized会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计算器减1,当计数器为0时,锁就被释放,然后notify通知所有等待的线程。
Java的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要用户态和内核态切换,大量的状态转换需要耗费很多处理器的时间。

synchronized的优化

在JDK1.6中对锁的实现引入了大量的优化:

  1. 锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。
  2. 锁消除(Lock Elimination):JVM即时编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸出去从来被其他线程访问到,就可以去除这个锁。
  3. 偏向锁(Biased Locking):目的是消除数据无竞争情况下的同步原语。使用CAS记录获取它的线程。下一次同一个线程进入则偏向该线程,无需任何同步操作。
  4. 适应性自旋(Adaptive Spinning):为了避免线程频繁挂起、恢复的状态切换消耗。线程会进入自旋状态。JDK1.6引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,可以能减少自旋的时间。
  5. 轻量级锁(Lightweight Locking):在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠一条CAS原子指令就可以完成锁的获取及释放。

在JDK1.6之后,synchronized不再是重量级锁,锁的状态变成以下四种状态:
无锁->偏向锁->轻量级锁->重量级锁

锁的状态

自适应自旋锁

大部分时候,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。这项技术就是所谓的自旋锁。
自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有获取到锁,则该线程应该被挂起。在JDK1.6中引入了自适应的自旋锁,自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

所谓自旋,不是获取不到就阻塞,而是在原地等待一会儿,再次尝试(当然次数或者时长有限),他是以牺牲CPU为代价来换取内核状态切换带来的开销。借助于适应性自旋,可以在CPU时间片的损耗和内核状态的切换开销之间相对的找到一个平衡,进而能够提高性能

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,如果失败则进行轻量锁的升级。

轻量级锁

如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:

  1. 线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的MarkWord复制到该锁记录中,官方称之为DisplacedMarkWord。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,则获得锁,进入步骤3)。如果失败执行步骤2)
  2. 线程自旋,自旋成功则获得锁,进入步骤3)。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤3)
  3. 锁的持有线程执行同步代码,执行完CAS替换MarkWord成功释放锁,如果CAS成功则流程结束,CAS失败执行步骤4)
  4. CAS执行失败说明期间有线程尝试获得锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁之后,还要唤醒等待的线程

重量级锁

自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败则进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己,需要从用户态切换到内核态实现。(当竞争竞争激烈时,线程直接进入阻塞状态。不过在高版本的JVM中不会立刻进入阻塞状态而是会自旋一小会儿看是否能获取锁如果不能则进入阻塞状态。)

总结

可以简单总结是如下场景:

  1. 只有一个线程进入加锁区,锁状态是偏向锁
  2. 多个线程交替进入加锁区,锁状态可能是轻量级锁
  3. 多线程同时进入加锁区,锁状态可能是重量级锁

最后,限于笔者经验水平有限,欢迎读者就文中的观点提出宝贵的建议和意见。如果想获得更多的学习资源或者想和更多的技术爱好者一起交流,可以关注我的公众号『全菜工程师小辉』后台回复关键词领取学习资料、进入后端技术交流群和程序员副业群。同时也可以加入程序员副业群Q群:735764906 一起交流。

原文地址:https://www.cnblogs.com/xichji/p/11633479.html

时间: 2024-08-26 23:45:43

详解Java多线程锁之synchronized的相关文章

牛刀小试 - 详解Java多线程

线程与多线程的概念 关于线程与多线程的较详细的理解可以参考:线程的解释 和多线程的解释. 而我们要做的是,对其进行"精炼".我们每天都在和电脑.手机打交道,每天都在使用各种各样的应用软件. 打开上电脑的任务管理器,就可以看到有一项名为"进程"的栏目,点击到里面可能就会发现一系列熟悉的名称:QQ,360等等. 所以首先知道了,QQ.360之类的应用软件在计算机上被称为一个进程. 而一个应用程序都会有自己的功能,用以执行这些进程当中的个别功能的程序执行流就是所谓的线程.

synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解

本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁,源码剖析 第一部分:synchronized与static synchronized的差别 1.synchronized与static synchronized 的差别 synchronized是对类的当前实例进行加锁,防止其它线程同一时候訪问该类的该实例的全部synchronized块.注意这里

synchronized与static synchronized 的区别、synchronized在JVM底层的实现原理及Java多线程锁理解

本Blog分为如下部分: 第一部分:synchronized与static synchronized 的区别 第二部分:JVM底层又是如何实现synchronized的 第三部分:Java多线程锁,源代码剖析 第一部分:synchronized与static synchronized的区别 1.synchronized与static synchronized 的区别 synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是"类

详解java垃圾回收机制(转)及finalize方法(转)

详细介绍Java垃圾回收机制 垃圾收集GC(Garbage Collection)是Java语言的核心技术之一,之前我们曾专门探讨过Java 7新增的垃圾回收器G1的新特性,但在JVM的内部运行机制上看,Java的垃圾回收原理与机制并未改变.垃圾收集的目的在于清除不再使用的对象.GC通过确定对象是否被活动对象引用来确定是否收集该对象.GC首先要判断该对象是否是时候可以收集.两种常用的方法是引用计数和对象引用遍历. 引用计数收集器 引用计数是垃圾收集器中的早期策略.在这种方法中,堆中每个对象(不是

java 同步锁(synchronized)

java 同步锁(synchronized) 在java中,Synchronized就是一把锁,他可以锁定一个方法,也可以锁定一个方法,我擦,其实这两个东西就是一样的.块不就是一个没有名字的方法么,方法就是一个有名字的块.本文就用块来测试.所谓锁,就是原子操作,把这个锁定的块作为一个整体,就像你上厕所,拉了就要擦屁屁,当然你也可以不擦,如果你不在意出现的问题的话.信号量Semaphore和这个Synchronized 其实实现的功能差不多,不过效率不同,使用的方式也不同.Synchronized

Protocol Buffer技术详解(Java实例)

Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发团队中目前主要使用的开发语言就是C++.Java和Python,其中Python主要用于编写各种工具程序.然而为了保证该篇Blog的完整性和独立性,我仍然会将上一篇Blog中已经出现的内容再一次赘述,同时对于Java中特有的部分也会着重介绍.          一.生成目标语言代码.      下面

详解Java解析XML的四种方法

(1)DOM解析 DOM是html和xml的应用程序接口(API),以层次结构(类似于树型)来组织节点和信息片段,映射XML文档的结构,允许获取 和操作文档的任意部分,是W3C的官方标准 [优点] ①允许应用程序对数据和结构做出更改. ②访问是双向的,可以在任何时候在树中上下导航,获取和操作任意部分的数据. [缺点] ①通常需要加载整个XML文档来构造层次结构,消耗资源大. [解析详解] ①构建Document对象: DocumentBuilderFactory dbf = DocumentBu

详解java动态代理

生活中的代理: 比如一个明星成名了以后,是需要有一个代理的,因为太多人想找他签名,应付不来,那么这个时候代理的作用是拦截你对真正明星的访问,他可以拦截下来收点费用,再叫真正的明星过来为你签名. 程序中的代理: 1,要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如,异常处理.日志.计算方法的运行时间.事务管理.等等,你准备如何做? 2,编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码 下图显示了在程序中代理的调用原理(

详解Java中代码块和继承

本文发表于个人GitHub主页,原文请移步详解Java中代码块和继承 阅读. 概念 1.代码块 局部代码块 用于限定变量生命周期,及早释放,提高内存利用率 静态代码块 对类的数据进行初始化,仅仅只执行一次. 构造代码块 把多个构造方法中相同的代码可以放到这里,每个构造方法执行前,首先执行构造代码块. 2.继承 继承是已有的类中派生出新的类,新的类能够吸收已有类的数据属性和行为,并能扩展新的功能. 代码块的执行顺序 public class Test {    public String name