Java之synchronized详解

前言

本文将对常用的synchronized围绕常见的一些问题进行展开。以下为我们将围绕的问题:

  • 乐观锁和悲观锁?
  • synchronized的底层是怎么实现的?
  • synchronized可重入是怎么实现的?
  • synchronized锁升级?
  • synchronized是公平锁还是非公平锁?
  • synchronized和volatile对比有什么区别?
  • synchronized在使用时有何需要注意事项?

注意:下面都是在JDK1.8中进行的。

乐观锁和悲观锁?

关于乐观锁和悲观锁的定义和使用场景,可以看《Mysql InnoDB之各类锁》中,本质都是一样的,这里就不再赘述。

关于悲观锁,下面再进行介绍,synchronized和Lock都属于悲观锁,下面我们来具体看看乐观锁。

乐观锁的实现-CAS

乐观锁的核心就是CAS(Compare And Swap-比较与交换,是一种不抢占的同步方式),是一种无锁算法。CAS算法涉及三个操作数:

  • 需要读写的内存值V。
  • 进行比较的值A。
  • 要写入的新值B。

当前仅当当前内存值V等于值A时,才进行写入新值B,有人会问我在比较相等后的同时更新了值V咋办?写入的新值B不是覆盖了别人刚写入的值吗?是的比较和写入需要保证是一个原子操作,这里通过CPU的cmpxchg指令,去比较寄存器中的A和内存中的值V,如果相等的话就写入B,如果不等的话就值V赋值给寄存器中的值A,如果想继续自旋就继续不想继续可以抛出相应错误。

下面我们来看看常见的AtomicInteger是如何自旋的。

AtomicInteger

一个可以被原子更新的int值,关于原子变量属性描述具体可以参考{@link java.util.concurrent.automic}包。AutomicInteger用于原子递增计数器等应用程序中,不能被使用替代Integer。然而这个类扩展了Number允许被一些处理数值的工具或者公共代码统一访问。

字段和构造函数

package java.util.concurrent.atomic;

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 设置使用Unsafe.compareAndSwapInt进行更新。
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    // 获得value对象内存分布中的偏移量用于找到value
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 保证内存可见效和禁止指令重排。
    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * 初始值为0
     */
    public AtomicInteger() {
    }

incrementAndGet

/**
 * 以原子方式将当前值递增1
 * @return 更新后的值
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
<span class="copy-code-btn">&#x590D;&#x5236;&#x4EE3;&#x7801;</span>

var1:为AtomicInteger对象,用于unsafe结合valueOffset获得对象中的最新的value。
var2:value值在AtomicInteger对象中偏移量。
var4:增加的值为1。

package sun.misc;
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //&#x83B7;&#x5F97;AutomicInteger&#x7684;value&#x503C;
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

乐观锁的缺点

  • 如果并发比较高,CAS一直比较自旋,将会一直占用CPU,如果自旋的线程多了CPU就会飙升。
  • 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能保证原子操作,但是对多个共享变量操作时,CAS时无法保证操作的原子性的。
  • java从1.5开始JDK提供了AtomicReference类来保证引用之间的原子性,可以把多个变量放在一个对象中来进行CAS操作。

synchronized的底层是怎么实现的?

sychronized是通过对象头部的Mark Word中的锁标识+monitor实现的。

java对象头

Java之synchronized详解

对象头由Mark Word和Klass组成,在没有压缩指针的时候都占8个字节。

  • Mark Word:标记字段-运行时数据,如哈希码、GC信息以及锁信息。
  • Klass:对象锁代表的类的元数据指针。

Java之synchronized详解

锁标志位+是否是偏向锁(biased_lock)共同表示对象的几种状态

Java之synchronized详解

monitor

synchronized通过Monitor来实现线程同步和协作。

  • 同步依赖的是操作系统的Mutex(互斥锁量)只有拥有互斥量的线程才能进入临界区,不拥有的只能阻塞等待,会维护一个阻塞的队列。

Java之synchronized详解
  • 协作依赖的是synchronized持有的对象,对象可以让多个线程方便同步,还可以通过对象调用wait方法释放锁让线程进入等待队列,等其他线程调用对象的notify和notifyAll方法进行唤醒可以重新获取锁。

Java之synchronized详解

Monitor用来进行监听锁的持有和调度锁的持有的。持有的对象可以理解为锁的一个媒介,可以使用它方便操作同步和协作。

具体例子可以参考《Thread源码阅读及相关问题》中的例子。

monitor这套监听锁和调度锁包括使用的互斥量其实都是比较消耗资源的,所以使用它的成为”重量级锁”。JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了”偏向锁”和”轻量级锁”,下面我们分别会进行分配介绍。

无锁

当对象头中锁标志位为01,是否偏向锁为0时表示使无锁的状态。想要在无锁的时候实现同步可以使用上面乐观锁中实现-CAS。

偏向锁

偏向锁是一个锁优化的产物,在对象头中进行标记,表示有线程进入了临界区,在只有一个线程访问的时候既不用使用CAS也不用引入较重的monitor。

线程不会主动释放偏向锁,只有遇到其他线程进尝试竞争偏向锁时,需要等待全局安全点(在这个时间点上没有执行的字节码),它会首先暂停拥有偏向锁的的线程,判断它是否还活着,如果死亡了就恢复到无锁状态其他线程就可以占用,如果还在临界区就对锁进行升级成”轻量锁”。

Java之synchronized详解

每个线程进入临界区的时候都会查看对象头锁标识是否是偏向锁,是偏向锁的话,会判断当前线程和对象头中的线程id是同一个线程则直接进入,如果不是则进行CAS看是不是能比较替换成功(防止马上就释放了),如果没成功就会暂停持有偏向锁的线程,看线程是否已经不再用锁了,如果没用就释放,给新进来的线程占用,如果在用就进行锁升级生成轻量锁”。

可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序会默认进入轻量级锁状态。 笔者思考

可以发现在只有一个线程进入临界区的时候确实能避免使用互斥量带来的开销,但是可以发现线程不会主动释放偏向锁。为啥不当有偏向锁的时候离开临界区进行释放?还要等其他线程来的时候要等全局点的时候尝试对线程暂停之后再看该线程持有锁的状态?这些疑问我们考虑不全,可能是设计的问题,也可能是因为一些其他原因,我们对JVM源码不够熟悉的情况下会比较费解。或许后面迭代会进行优化。

所以我们只需要明白一点:偏量锁是一种锁的优化,它本质上不是锁,只是对象头中进行了标记,如果没有多线程并发访问临界区的时候可以减少开销,如果出现多并发的时候会进行升级。

轻量级锁

轻量锁发生在偏向锁升级或者 -XX:-UseBiasedLocking=false的时候,线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaces Mark Word。然后线程尝试使用CAS将对头像中的Mark Word替换为之指向锁记录的指针。如果成功,当前线程获得锁,如果失败将通过自旋来进行同步。

重量级锁

重量锁的锁标志位为10,就是上面介绍的monitor机制,开销最大。

Java之synchronized详解

synchronized锁升级?

如上面的synchronized的底层实现章节。

synchronized可重入是怎么实现的?

我们先用代码证明下:

public class SynchronizedReentrantTest extends Father {
    public synchronized void doSomeThing1() {
        System.out.println("doSomeThing1");
        doSomeThing2();
    }

    public synchronized void doSomeThing2() {
        System.out.println("doSomeThing2");
        super.fatherDoSomeThing();
    }

    public static void main(String[] args) {
        SynchronizedReentrantTest synchronizedReentrantTest = new SynchronizedReentrantTest();
        synchronizedReentrantTest.doSomeThing1();
    }
}

class Father {
    public synchronized void fatherDoSomeThing() {
        System.out.println("fatherDoSomeThing");
    }
}
<span class="copy-code-btn">&#x590D;&#x5236;&#x4EE3;&#x7801;</span>

输出:

doSomeThing1
doSomeThing2
fatherDoSomeThing

说明synchronized是可重入的。

重量级锁使用的是monitor对象中的计数字段来实现的,偏向锁应该没有只有表示当前被那个线程持有,轻量锁在每次进入的时候都会添加一个Lock Record来表示锁的重入次数。

笔者思考

为啥偏向锁不记录重入次数,重入的时候只需要看是否是当前线程,对象头中没有地方存放次数,所以偏向锁不会主动释放(应该是判断嵌套临界区比较麻烦),需要另外一个线程来判断当前线程是否活跃死亡了才释放还会尝试暂停持有的线程。这点其实不如轻量级锁和重量级锁。

synchronized是公平锁还是非公平锁?

非公平的,直接下面打饭的例子:

import lombok.SneakyThrows;

public class SyncUnFairLockTest {
    //&#x98DF;&#x5802;
    private static class DiningRoom {
        //&#x83B7;&#x53D6;&#x98DF;&#x7269;
        @SneakyThrows
        public void getFood() {
            System.out.println(Thread.currentThread().getName() + ":&#x6392;&#x961F;&#x4E2D;");
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + ":@@@@@@&#x6253;&#x996D;&#x4E2D;@@@@@@@");
                Thread.sleep(200);
            }
        }
    }

    public static void main(String[] args) {
        DiningRoom diningRoom = new DiningRoom();
        //&#x8BA9;5&#x4E2A;&#x540C;&#x5B66;&#x53BB;&#x6253;&#x996D;
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                diningRoom.getFood();
            }, "&#x540C;&#x5B66;&#x7F16;&#x53F7;:00" + (i + 1)).start();
        }
    }
}

输出:

&#x540C;&#x5B66;&#x7F16;&#x53F7;:001:&#x6392;&#x961F;&#x4E2D;
&#x540C;&#x5B66;&#x7F16;&#x53F7;:001:@@@@@@&#x6253;&#x996D;&#x4E2D;@@@@@@@
&#x540C;&#x5B66;&#x7F16;&#x53F7;:005:&#x6392;&#x961F;&#x4E2D;
&#x540C;&#x5B66;&#x7F16;&#x53F7;:003:&#x6392;&#x961F;&#x4E2D;
&#x540C;&#x5B66;&#x7F16;&#x53F7;:004:&#x6392;&#x961F;&#x4E2D;
&#x540C;&#x5B66;&#x7F16;&#x53F7;:002:&#x6392;&#x961F;&#x4E2D;
&#x540C;&#x5B66;&#x7F16;&#x53F7;:002:@@@@@@&#x6253;&#x996D;&#x4E2D;@@@@@@@
&#x540C;&#x5B66;&#x7F16;&#x53F7;:004:@@@@@@&#x6253;&#x996D;&#x4E2D;@@@@@@@
&#x540C;&#x5B66;&#x7F16;&#x53F7;:003:@@@@@@&#x6253;&#x996D;&#x4E2D;@@@@@@@
&#x540C;&#x5B66;&#x7F16;&#x53F7;:005:@@@@@@&#x6253;&#x996D;&#x4E2D;@@@@@@@

注意到这里我加了sleep,因为对于公平锁来说无所谓,先来的肯定先执行,但是非公平锁时后面来的线程会先进行尝试获得锁,获取不到再进入队列,这样就能避免同一进入队列再被CPU唤醒,能提高效率,但是非公平锁会出现饿死的情况。

synchronized和volatile对比有什么区别?

都能保证可见效,synchronized因为是锁所以能保证原子性。

可见效主要指的是线程共享时工作内存和主内存能否及时同步。

Java之synchronized详解

JMM关于synchronized的两个规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。
  • 线程加锁时,将清空工作内存中共享变量的值,从而使变量共享时,需要从主内存中重新读取最新的值。

Original: https://www.cnblogs.com/pxza/p/15994888.html
Author: 人生的激活码
Title: Java之synchronized详解

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/580090/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • OptaPlanner 发展方向与问题

    ​ 最近一段时间,因为忙于【易排(EasyPlan)规划平台】的设计与开发工作,平台的一些功能设计,需要对OptaPlanner的各种特性作更深入的研究与应用。慢慢发现,OptaP…

    Java 2023年6月16日
    094
  • 在项目中如何直接使用hystrix?

    一、背景 最近由于一些背景原因,需要在项目中需要对接口进行限流。所以就考虑到了直接使用Hystrix。但是呢,又不想直接使用SpringCloud,而是直接引入原生,现在发现挺好用…

    Java 2023年6月15日
    071
  • Java基础面试题(1)

    个人总结,仅自己学习用。愿与大家一起分享!如有错误请指正。 一、String,StringBuffer, StringBuilder 的区别是什么?String为什么是不可变的? …

    Java 2023年5月29日
    0155
  • vue+element-ui后台管理系统模板

    vue+element-ui后台管理系统模板 前端:基于vue2.0+或3.0+加上element-ui组件框架 后端:springboot+mybatis-plus写接口 通过A…

    Java 2023年6月15日
    046
  • Markdown笔记

    Markdown笔记 二级标题 三级标题 四级标题 六级标题 加粗 hello Hello Hello Hello Hello 引用 这是一个引用 分割线 图片 超链接 列表 无序…

    Java 2023年6月15日
    069
  • JVM详解

    一、JVM的位置及体系结构 JVM作用在操作系统之上,而Java程序作用在jvm之上,其他的程序则与jvm并列 二、类加载器,及双亲委派机制 1.类加载器 作用:加载Class文件…

    Java 2023年6月13日
    060
  • java中ftpClient.listFiles()结果为空问题解决方案

    问题描述 连接ftp读取路径下文件列表为空 查到方案 java项目中用到ftpClient.listFiles()函数时,总是返回null。网上乱七八糟的解决方案感觉都是拷来拷去。…

    Java 2023年5月29日
    075
  • 堆排序(java)

    目录 基础堆排序 一、概念及其介绍 二、适用说明 三、过程图示 四、Java 实例代码 优化堆排序 Java 实例代码 基础堆排序 一、概念及其介绍 堆排序(Heapsort)是指…

    Java 2023年6月5日
    062
  • 快捷键

    常用快捷键 文档操作通用快捷键• ctrl + c 复制• ctrl + v 粘贴• ctrl + x 剪切• ctrl + s 保存• ctrl + z 撤销• ctrl + y…

    Java 2023年6月7日
    068
  • fastposter v2.7.1 紧急发布 电商海报编辑器

    fastposter v2.7.1 紧急发布 电商海报编辑器 fastposter海报生成器,电商海报编辑器,电商海报设计器,fast快速生成海报 海报制作 海报开发。二维码海报,…

    Java 2023年6月5日
    071
  • Spring Cloud Gateway 路由谓词工厂

    Spring Cloud Gateway 包含许多内置的Route Predicate Factories。所有这些谓词都匹配HTTP请求的不同属性。多个 Route Predic…

    Java 2023年5月30日
    078
  • Java中使用java.awt.geom.Point2D进行坐标相关的计算(距离、平方等)

    在Java中需要对坐标点进行一些计算和判断。 比如计算两点之间的距离、距离的平方、两点是否相等、坐标赋值、克隆等。 可以使用Java自带的java.awt.Point2D的相关AP…

    Java 2023年5月29日
    062
  • 测试文章1

    Hive安装到配置 前言 (一)Hive集群规划 (二)安装MySql + * – + 官网下载需要的包 + 把他们下载到 CentOS 的 /usr/local/sr…

    Java 2023年6月5日
    087
  • 设计模式实战(二)(单例模式)

    设计模式实战 👾设计模式demo实战。 项目地址:https://github.com/bearbrick0/designpattern 🫑创建型设计模式 创建型的设计模式包括:单…

    Java 2023年6月5日
    078
  • Eclipse启动Tomcat后无法访问项目

    Eclipse中的Tomcat可以正常启动,不过发布项目之后,无法访问,包括http://localhost:8080/的小猫页面也无法访问到,报404错误。这是因为Eclipse…

    Java 2023年6月8日
    067
  • Spring 概述

    1. 什么是 spring? 1.Spring &#x662F;&#x4E2A;Java&#x4F01;&#x4E1A;&#x7EA7;&a…

    Java 2023年6月5日
    093
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球