synchronized原理

1:问题

举例引出问题

public class test {
   
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
   
        Runnable runnable = new Runnable() {
   
            @Override
            public void run() {
   
                add();
            }
        };

        Thread thread = new Thread(runnable);
        Thread thread1 = new Thread(runnable);
        thread.start();
        thread1.start();

        thread.join();
        thread1.join();

        System.out.println("count:" + count);
    }

    public static void add() {
   
        for (int i = 0; i < 5000; i++) {
   
            count++;
        }
    }
}

在这里插入图片描述
上述代码中,启动两个线程,对变量count进行数值操作,两个线程分别执行5000次,按照预期结果来看,count的最终结果应该是10000,但是实际得到的数值却不是10000,并且实际得到的数值不是确定的。

那么为什么会出现这种情况呢?

因为JAVA中的++操作,它在语言层面虽然只是一行代码,看似是原子操作的,但是站在字节码的角度,它却不是原子操作的,所以两个线程在对count进行数值操作时,会出现线程之间的交错运行,导致count达不到预期的结果。

为什么线程之间交错运行就会导致count达不到预期结果呢?

因为两个线程进行计算的变量是属于共享变量,而不是线程私有的变量,所以两个线程进行计算时会出现一个线程将count复制到自己的空间进行计算了但是还没赋值给count,此时另一个线程读取count进行计算赋值,这种情况会导致count最终结果达不到预期。

如何解决这种情况?
由于count++操作在字节码的角度不是原子操作的,我们可以站在宏观的角度去解决,JAVA提供了synchronized关键字来帮助我们保证一组代码在执行时不会有别的线程来执行,这样子就可以保证在进行count++操作时只有一个线程在执行。

2:synchronized的使用方法

synchronized的使用可以作用于代码块或者是方法上

public class test {
   
    public void function(){
   
        synchronized (锁对象){
   
            ......
        }
    }
    
    public synchronized void function1(){
   
        ......
    }
}

在使用synchronized时要分清楚锁对象是谁,线程间所用的锁对象是否是同一个,如果线程间使用的锁对象不是同一个,则synchronized其实是没有意义的。

		synchronized (this){
   
            // 当前对象
        }

        Object lock = new Object();
        synchronized (lock){
   
            // lock对象
        }
        
        synchronized (XXX.class){
   
            // XXX类
        }
		public synchronized void function(){
   
        	// 当前对象
    	}

		public static synchronized void function(){
   
        	// 当前类
    	}

总结:

  1. 对于普通同步方法,锁就是当前实例
  2. 对于静态同步方法,锁就是当前类的Class对象
  3. 对于同步方法块,锁就是synchronized里配置的对象

3:synchronized原理

知道了synchronized的基本用法,synchronized需要与一个类或者对象进行关联,可以把类或者对象理解为一把锁,那么底层是怎么实现的呢?

在JVM规范中提到了synchronized在JVM中的实现原理,JVM是基于进入和退出Monitor对象来实现方法同步和代码块同步,但是两者的实现不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,方法同步的细节,JVM规范中没有详细说明,但是方法同步也是使用这两个指令进行实现的。

在这里插入图片描述

首先编译这段代码查看字节码

在这里插入图片描述
可以看到代码块同步中,monitorenter是在代码同步块开始的地方,monitorexit是在代码块方法结束的地方和异常处。

接下来查看同步方法的字节码

在这里插入图片描述
在这里插入图片描述
可以看到代码块同步可以看到monitorenter与monitorexit指令,但是方法块同步在编译后却没有看到。相关书籍中说明同步方法是使用另外一种方式进行实现的,但是本质都是使用monitorenter与monitorexit指令来进行实现。

4:Java对象头

synchronized用的锁是存在于java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型的,则用2个字宽来存储对象头。在32位虚拟机中,一个字宽等于4个字节,即32bit。

在这里插入图片描述
java对象头中的Mark Word里默认存储对象的HashCode、分代年龄、锁标记。32位的JVM的Mark Word默认存储如图所示。

在这里插入图片描述
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

在这里插入图片描述
上图中可以看到轻量级,重量级,偏向锁,GC与正常状态下的Mark Word的存储内容

5:轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以
使用轻量级锁。

5.1:轻量级锁加锁:

线程执行同步代码块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word信息复制到锁记录中,官方称之为 Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则当前线程获取到锁,如果失败则表示则表示有其他线程再竞争锁,当前线程尝试使用自旋来获取锁,当然 自旋已经是属于重量级锁的优化了。

在这里插入图片描述

上图是获取到轻量级锁的示意图,当前线程存储了对象头的Mark Word的信息,且CAS替换了对象头中的Mark Word,替换为指向锁记录的指针。

在这里插入图片描述
可重入锁的示意图,常见的情况就是一个线程执行A方法获取XXX锁成功,然后A方法中调用B方法,B方法中也需要获取XXX锁,由于当前线程已经在执行A方法时获取到XXX锁了,此时调用B方法获取XXX锁时也会在栈帧中创建一条锁记录。

5.2:轻量级锁解锁:

轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回对象头中,如果成功,则表示没有发生竞争,如果失败了,则表示当前锁存在竞争,锁就会膨胀为重量级锁,进行重量级锁的解锁操作。

在这里插入图片描述
上图中,Thread-0已经获取到了轻量级锁,此时Thread-1尝试获取锁对象,进行了Displaced Mark Word操作后进行CAS尝试替换对象头的Mark Word来指向Thread-1时会失败,因为此时的对象头的Mark Word信息指向了Thread-0,此时Thread-1会尝试 自旋 重试来获取锁,一定次数后如果获取不到锁,则会进行锁膨胀,将对象头(Object)申请Monitor锁,使对象头的Mark Word指向Monitor对象的地址,Monitor的Owner指向Thread-0,同时Thread-1进行Monitor的EntryList进行睡眠。

在这里插入图片描述
当Thread-0解锁后,会发现此时的锁已经不是轻量级锁,而升级位重量级锁,Thread-0会重置Monitor的Owner并唤醒EntryList中的线程来竞争锁。

在这里插入图片描述

  1. 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  2. 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
    高,就多自旋几次;反之,就少自旋甚至不自旋。

  3. Java 7 之后不能控制是否开启自旋功能

  4. 因为自旋是会消耗CPU资源的,为了避免无用的自旋,(比如获得锁的线程被阻塞住了),一旦锁升级为重量级锁,就不会再恢复到轻量级锁的状态,当锁处于这个状态下,其他线程尝试获取锁时,都会被阻塞住进入EntryList中,当持有锁的线程释放锁时会唤醒这些线程,被唤醒的线程开始锁的竞争。

6:偏向锁

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

关于轻量级锁,重量级锁,偏向锁,GC和正常状态下的Mark Word的格式,上文已经描述。

接下来使用jol来查看对象的Mark Word信息

<dependency>
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值