synchronized关键字

 一、synchronized的基本用法

  1. synchronized修饰的是实例方法时,线程获取的锁是该对象的锁。当synchronized修饰的是静态方法时,线程获取的锁是该对象对应的Class对象的锁。
  2. 当一个对象拥有多个由synchronized修饰的是实例方法时,那么只有一个线程能够获取该对象的锁,其他的线程就会等待,注意我们实例化出来的对象一定是同一个对象。
  3. 案例:
    public class MyThreadTest2 {
    
        public static void main(String[] args) {
            MyClass myClass = new MyClass();
    
            Thread t1 = new Thread1(myClass);
            Thread t2 = new Thread2(myClass);
    
            t1.start();
    
            try {
                Thread.sleep(700);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            t2.start();
        }
    }
    
    class MyClass {
    
        public synchronized void hello () {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("hello");
        }
    
        public synchronized void world() {
            System.out.println("world");
        }
    
        public static synchronized void say() {
            System.out.println("say");
        }
    }
    
    class Thread1 extends Thread {
    
        private MyClass myClass;
    
        public Thread1(MyClass myClass) {
            this.myClass = myClass;
        }
    
        @Override
        public void run() {
            myClass.hello();
        }
    }
    
    class Thread2 extends Thread {
    
        private MyClass myClass;
    
        public Thread2(MyClass myClass) {
            this.myClass = myClass;
        }
    
        @Override
        public void run() {
            myClass.say();
        }

    运行结果:

当我们把代码修改一下: 

class Thread2 extends Thread {

    private MyClass myClass;

    public Thread2(MyClass myClass) {
        this.myClass = myClass;
    }

    @Override
    public void run() {
        myClass.world();
    }
}

运行结果:

 二、synchronized字节码分析

使用javap 对以下代码的Class文件进行反编译后:

1.synchronized关键字对应的字节码指令是monitorenter和monitorexit,monitor翻译过来的意思是监视器,也就是我们经常说的锁。其实我们会发现Thread类中的方法很多都是由native修饰的,这些方法的底层代码是由c++编写的,说白了学到多线程的底层还是java虚拟机,线程是由我们的操作系统创建和执行的。

2.为什么会出现两次monitorexit,这个是我们后面要讨论的问题?

为了防止异常情况的出现,编译器会生成一条monitorexit指令来保证线程会释放该对象的锁。下面的程序有可能正常退出,就是会执行17行的指令直接return了,还有一种情况是打印的时候出现了异常,此时就需要一个monitorexit来释放对象的锁。

public class MyTest1 {

    private Object object = new Object();

    public void method() {
        synchronized (object) {
            System.out.println("hello world");
        }
    }

}

当我们将代码修改后然后再反编译:

此时的synchronized对应一个monitorexit关键字,因为无论是打印时出现异常还是抛出异常,此时只有一个monitorexit,所以synchronized关键字和monitorenter和monitorexit并不是一对一的关系。

public class MyTest1 {

    private Object object = new Object();

    public void method() {
        synchronized (object) {
            System.out.println("hello world");
            throw new RuntimeException("出错了");
        }
    }
}

 

 3.JVM使用了ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC_SYNCHRONIZED标志。所以synchronized修饰方法和使用代码块的底层实现是不同的。

三、自旋对synchronized的意义

synchronized底层是基于Monitor对象实现的,Monitor是依赖于底层操作系统的mutex lock来实现互斥的。假如a线程和b线程同时去竞争某一对象的Monitor,当a线程拿到了Monitor时,b线程不会立即进行等待状态,它首先会进行自旋,因为进入到等待状态会涉及到用户态和内核态之间的切换。而用户态和内核态之间的切换会极大的影响锁的性能。如果a线程在很短的时间内执行完并释放了对象的锁后,b线程很可能会立即获取到该对象的锁,这样就避免了线程在用户态和内核态之间的切换。如果a线程执行的时间比较长,那么自旋就失去了意义,因为自旋它会一直占用CPU的资源。所以总体的思想是:先自旋,不成功再进行阻塞。最后解释一下用户态和内核态,

内核态:运行操作系统程序,操作硬件。

用户态:运行用户程序。

四、synchronized锁升级的过程

synchronized是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这个才是synchronized锁升级的目的。这种锁的优化实际上是通过Java对象头中的一些标志位来去实现的;对于锁的访问与改变,实际上都与Java对象头息息相关。所以想学会多线程还是得先学会jvm。

对象头主要也是由3块内容来构成:

1. Mark Word

2. 指向类的指针

3. 数组长度

其中Mark Word(它记录了对象、锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit)的位信息包括了如下组成部分:

    1. 无锁标记

    2. 偏向锁标记

    3. 轻量级锁标记

    4. 重量级锁标记

    5. GC标记

偏向锁:针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其Mark Word中将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个synchronized方法时,它会检查这个对象的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进入管程(Monitor)了,而是直接进入到该方法体中。

轻量级锁若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁,而第二个线程在争抢时,会发现该对象头中的Mark Word已经是偏向锁,但里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap),从而获取到锁,这里面存在两种情况:

1. 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。这种情况说明竞争不是很激烈,所以还是偏向锁。

2. 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行升级,升级为轻量级锁。这种情况说明锁竞争有点激烈,所以锁就由偏向锁升级为轻量级锁。

自旋锁:上面我们提到了自旋的意义,总体的思想就是:先自旋,如果失败,线程就进入等待状态。若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)自旋最大的一个特点就是避免了线程从用户态进入到内核态。

    

重量级锁:线程最终从用户态进入到了内核态。

以上就是我对synchronized的总结,欢迎交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值