synchronized底层实现原理

2年前 (2022) 程序员胖胖胖虎阿
190 0 0

文章目录

  • 一、synchronized锁表现三种形势
  • 二、为什么说Synchronized是一个重量级锁
  • 三、Synchronized底层实现原理
  • 四、Synchronized锁存储位置
  • 五、Synchronized锁的升级过程

一、synchronized锁表现三种形势

Java中每个对象都可以作为锁。具体表现为以下3种方式:

  1. 对于普通方法,锁的是当前实例对象。
public class SynchronizedTest {
    public synchronized void test(String name){
        System.out.println(name+"开始执行");
        try {
            Thread.sleep(2000);
        }catch (Exception e){
        }
        System.out.println(name+"执行完毕");
    }
    public static void main(String[] args) throws  Exception {
        SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.test("线程1");
            }
        }).start();
        SynchronizedTest t1=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.test("线程2");
            }
        }).start();
    }
}

上面这个代码我们new了两个不同的对象。打印结果如下。线程2并没有等线程1执行完成后才执行,说明对于普通方法,如果是不同的对象实例锁是不起作用的

线程1开始执行
线程2开始执行
线程2执行完毕
线程1执行完毕

我们把上面的代码修改一下,改为同一个实例

public class SynchronizedTest {
    public synchronized void test(String name){
        System.out.println(name+"开始执行");
        try {
            Thread.sleep(2000);
        }catch (Exception e){
        }
        System.out.println(name+"执行完毕");
    }

    public static void main(String[] args) throws  Exception {
        SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.test("线程1");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.test("线程2");
            }
        }).start();
    }
}

打印结果如下。从打印结果可以看出同一个对象实例的时候,第二个线程只有等到第一个线程执行完成后才开始执行。

线程1开始执行
线程1执行完毕
线程2开始执行
线程2执行完毕
  1. 对于静态同步方法,锁的是当前类的Class对象。
public class SynchronizedTest {
    public synchronized static void test(String name){
        System.out.println(name+"开始执行");
        try {
            Thread.sleep(2000);
        }catch (Exception e){
        }
        System.out.println(name+"执行完毕");
    }
    public static void main(String[] args) throws  Exception {
        new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.test("线程1");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.test("线程2");
            }
        }).start();
    }
}

打印结果如下。第一个线程执行完成后才开始执行第二个线程。

线程1开始执行
线程1执行完毕
线程2开始执行
线程2执行完毕
  1. 对于同步方法快,锁的是synchonized括号里配置的对象。
public class SynchronizedTest {
    public  void test(String name){
       Object o=new Object();
        synchronized(o.getClass()){
            System.out.println(name+"开始执行");
            try {
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println(name+"执行完毕");
        }

    }
    public static void main(String[] args) throws  Exception {
        SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.test("线程1");
            }
        }).start();
        SynchronizedTest t1=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.test("线程2");
            }
        }).start();
    }
}

打印结果如下,第一个线程执行完成后才开始执行第二个线程。

线程1开始执行
线程1执行完毕
线程2开始执行
线程2执行完毕

二、为什么说Synchronized是一个重量级锁

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。

三、Synchronized底层实现原理

同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。

四、Synchronized锁存储位置

Synchronized用的锁是存在java的对象头里面的。一个对象在new出来之后再内存中主要分为4个部分:
synchronized底层实现原理
Mark Word:存储了对象的hashCode、GC信息、锁信息三部分。这部分占8字节。

Class Pointer:存储了指向类对象信息的指针。在64位JVM上有一个压缩指针选项-ClassPointer指针:-XX:+UseCompressedClassPointers 为4字节 不开启为8字节。默认是开启的。

实例数据(instance data):记录了对象里面的变量数据。引用类型:-XX:+UseCompressedOops 为4字节 不开启为8字节 Oops Ordinary Object Pointers

Padding:作为对齐使用,对象在64位服务版本中,规定对象内存必须要能被8字节整除,如果不能整除,那么久靠对齐来不。举个例子:new出了一个对象,内存只占用18字节,但是规定要能被8整除,所以padding=6
Mark Word存储结构如下:
32位虚拟机下:
synchronized底层实现原理

64位虚拟机下:
synchronized底层实现原理

五、Synchronized锁的升级过程

Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

  1. 偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。

  2. 轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  3. 重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。

版权声明:程序员胖胖胖虎阿 发表于 2022年11月3日 下午7:16。
转载请注明:synchronized底层实现原理 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...