Java锁 synchronized

作者: Java耕耘者 | 来源:发表于2019-12-23 19:20 被阅读0次

    Synchronized 关键字
    喜欢底层源码的朋友可以来交流探讨。交流群:818491202 验证:88

    在java中,相信大家都用过 synchronized 来解决多线程安全问题,下面简单描述一下 synchronized 的相关特性.
    被 synchronized 包含的代码块具有以下特性

    原子性 同步的代码块操作不可中断
    可见性 同步代码块里的数据都是最新的,也就是主存数据,并且操作完会立即刷新主存

    这两个特性将在下面的代码示例中展示(更底层的留到更后面说)

    synchronized 原子性的实现就是: 加锁

    java中的对象锁有四种状态: 无锁、偏向锁、轻量级锁、重量级锁(monitor).从左往右逐渐升级.

    根据对象锁的竞争,锁会逐渐升级,最后可能 升级成重量级锁也就是 monitor.并且锁只能升级,不能降级
    原子性
    synchronized 保证了同步代码块里面的操作的原子性
    代码示例
    public class SyncTest2 {
    // 计数
    static CountDownLatch cdl = new CountDownLatch(2);

    public static void main(String[] args) throws Exception{
        LockObject lockObject = new LockObject();
        new Thread(new TestSyncRunnable(lockObject)).start();
        new Thread(new TestSyncRunnable(lockObject)).start();
        cdl.await();
    }
    
    static class LockObject{
        private int cnt;
    }
    
    static class TestSyncRunnable implements Runnable{
    
        private LockObject lockObject;
    
        TestSyncRunnable(LockObject lockObject){
            this.lockObject = lockObject;
        }
    
        @Override
        public void run() {
            synchronized(this.lockObject) {
                System.out.println(String.format("线程%s开始执行任务",Thread.currentThread()));
                for(int i = 0;i<10000000;i++) {
                    this.lockObject.cnt++;
    

    // System.out.println(String.format("线程%s执行了第%s次",Thread.currentThread(),i));
    }
    System.out.println(String.format("线程%s执行完毕了",Thread.currentThread()));
    }
    cdl.countDown();
    }
    }
    上面这个结果,不管运行几次,结果都是一个执行完毕了,另一个才开始执行,因为 synchronized 是包括了整个循环操作.

    线程Thread[Thread-0,5,main]开始执行任务

    线程Thread[Thread-0,5,main]执行完毕了

    线程Thread[Thread-1,5,main]开始执行任务

    线程Thread[Thread-1,5,main]执行完毕了
    所有线程执行完毕了,结果: cnt = 20000000
    此时我们 将 synchronized 关键字去掉 ,run方法变成了如下
    @Override
    public void run() {
    // synchronized(this.lockObject) {
    System.out.println(String.format("线程%s开始执行任务",Thread.currentThread()));
    for(int i = 0;i<10000000;i++) {
    this.lockObject.cnt++;
    }
    System.out.println(String.format("线程%s执行完毕了",Thread.currentThread()));
    // }
    cdl.countDown();
    }
    下面是输出的结果,不管运行了多少次,因为循环次数比较多,所以 能很明显的能看出来,在第一个线程执行完毕前,都会被第二个线程打断
    线程Thread[Thread-0,5,main]开始执行任务

    线程Thread[Thread-1,5,main]开始执行任务

    线程Thread[Thread-1,5,main]执行完毕了

    线程Thread[Thread-0,5,main]执行完毕了

    可见性

    可见性表明在 synchronized 代码块中的对象,都会从主存中获取最新数据,并且在同步代码块结束后,会将最新数据写入主存.

    本来想了一个例子,但是这个例子似乎不太恰当,由于i++这个操作并不是原子性并且java中似乎没有 有原子性但是没有可见性的东西.
    在验证东西的时候,都是单一变量原则,我觉得无法完全证明,所以例子就不贴出来了.(如果各位有什么好的想法或者建议可以偷偷告诉我,真的可行的话我会偷偷在这加上个例子~~)

    JVM 对象模型
    这篇是讲 synchronized 的,如果连模型都讲了会不会太多(会的)?但是 synchronized 跟对象模型根本离不开,so就一起放在这里介绍吧~
    当我们申请一个java对象的时候,jvm将会构造一个 包含三部分数据的对象

    对象头 header - 包含对象的各种标识

    实例数据 - java对象中拥有的具体字段

    对齐填充 - 对象字段4字节对齐,对象地址必须是8字节的倍数,不满则补.

    Header
    JVM 对象的 Header 由三部分组成

    Mark Word 用于存储对象的各种 标志信息 (thread ID、 epoch、age、biasable、lock、hashCode 等)

    Class Metadata Address 指向class地址的指针(class要加载到内存中,既然是内存,那就肯定有个地址),用来标记该对象的类型-class

    Array length 如果该对象是数组的话,则会多4个字节(32bit)来存储数组的长度

    image.png

    Mark Word


    image.png

    可以看到有几个属性值,从左往右看:

    thread ID (线程ID,记录持有偏向锁的线程ID最开始是0,这个判断偏向锁是否要升级成轻量级锁)

    epoch (纪元,可以理解为用来记录偏向锁的 identifier )

    age (对象的年龄,存活过了多少次gc)

    biasable (是否开启偏向锁,默认开启,在某些已经确定会发生竞争的场景,关闭偏向锁能提高效率 .开启的话值为1,否则为0,如下图
    就是开启的)

    pointer to lock record (指向栈中锁记录的指针)

    pointer to heavyweight monitor (指向监视器的指针)

    锁标识位 (这个东西是用来标记当前对象锁状态,下面列出几种状态值)

    01 无锁、偏向锁
    00 轻量级锁
    10 重量级锁
    11 表明对象要被回收了, 标记GC

    锁升级流程
    1.开始的时候对象是无锁的, 如果开启了偏向锁,此时的 ThreadID 为0 ,表示没有线程持有锁.(禁用了偏向锁的话,则从轻量级锁开始)
    2.当一个线程第一次给对象加锁对时候,此时 thread ID 会 从默认的0改为该加锁线程的ID,并且同一个线程可以多次获取该锁,也就是可重入.
    3.重入流程: 当还是偏向锁并且获取锁的时候,根据 尝试加锁的线程ID和对象中存储的 thread ID 比较 ,如果是同一个线程,则允许重入,即获取锁.
    4.当尝试加锁的线程ID跟当前对象存储的 thread ID不同 , 则表明有第二个线程来争抢锁(在变成轻量级锁之前, threadID 是会一直保存着). 一旦(划重点: 一旦)有第二个线程来争抢锁的时候,就会转变成轻量级锁的结构,不管当前对象是不是正被锁着.(升级成轻量级锁的时候 thread ID 已经不存在)
    5.当变成轻量级锁后,会进行一定次数的自旋(实际上就是 循环CAS操作 ),自旋一定次数锁都失败后,锁最后会升级成 重量级锁(monitor) .
    一些参数
    禁用偏向锁
    偏向锁的话,可以用以下参数关闭
    // 禁用偏向锁
    -XX:-UseBiasedLocking
    自旋次数
    // 设置自旋次数
    -XX:PreBlockSpin
    // 禁用偏向锁
    -XX:-UseBiasedLocking
    重量级锁 - monitor
    锁竞争严重的话,最后对象锁会升级为重量级锁-monitor
    关于 monitor的结构 ,先来一张图吧~

    image.png

    属性说明
    _owner: 当前锁的持有者
    EntryList: 正在阻塞争抢锁锁的线程集合,没有争抢到锁就会进入该队列
    WaitSet: 调用 wait() 被挂起的线程集合
    流程
    (咳咳,这里简单的描述一下流程)

    文章最后的参考链接有一个较详细的 synchronized 源码解读链接

    当线程争抢锁失败的时候,会进入 EntryList 进行阻塞.
    当持有锁的线程调用 wait() 方法的时候,实际上是执行了 monitorexit 放弃了锁, 然后挂起线程,线程进入 waitSet.
    当持有锁的线程调用 notify()/notifyAll() 并且在同步代码块结束的时候 ,也就是调用了 monitorexit 的时候在 waitSet 中的线程才能获取到锁.

    来个🌰
    public class SyncDemo {
    int i;

    public void test1() {
        synchronized (this) {
            i++;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        SyncDemo syncDemo = new SyncDemo();
        for (int i = 0; i < 100; i++) {
            syncDemo.test1();
        }
    }
    

    }
    使用javap查看字节码
    // 地址太长就省略前面的,知道是class就好了
    javap -v -l -c /xxxxx/SyncDemo.class
    java字节码
    public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=3, locals=3, args_size=1
    0: aload_0
    1: dup
    2: astore_1
    3: monitorenter // 这个就是 synchronized 的 monitor, 先获取锁
    4: aload_0
    5: dup
    6: getfield #2 // Field i:I , 获取i的数值并入栈
    9: iconst_1 // 将1(这里是int)入栈
    10: iadd // 将栈顶2个int数值相加,结果入栈
    11: putfield #2 // Field i:I , 从栈顶弹出并赋值给i
    14: aload_1
    15: monitorexit // 操作结束后释放锁
    16: goto 24
    19: astore_2
    20: aload_1
    21: monitorexit
    22: aload_2
    23: athrow
    24: return

    通过字节码可以看到,使用的 monitorenter 进行加锁 , 操作结束后 monitorexit 释放锁.
    那如果有多个线程进行锁的争抢呢?
    像下面这样的代码
    public static void main(String[] args) throws InterruptedException {
    SyncDemo syncDemo = new SyncDemo();
    // 启动两个线程进行争抢
    new Thread(new SyncDemoRunnable(syncDemo)).start();
    new Thread(new SyncDemoRunnable(syncDemo)).start();
    }
    }
    public class SyncDemoRunnable implements Runnable{
    SyncDemo syncDemo;

    SyncDemoRunnable(SyncDemo syncDemo){
        this.syncDemo = syncDemo;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10000000; i++) {
            synchronized (this.syncDemo) {
                this.syncDemo.i++;
            }
        }
    }
    

    }
    这时候看javap的代码甚至会发现 monitorentry 都不见了( 可能是使用的姿势不对? ).
    最后通过查看汇编代码会发现,实际上都有lock 前缀的指令,证明其是原子操作.

    喜欢底层源码的朋友可以来交流探讨。交流群:818491202 验证:88

    相关文章

      网友评论

        本文标题:Java锁 synchronized

        本文链接:https://www.haomeiwen.com/subject/qmrtoctx.html