synchronized

synchronized

在这里插入图片描述

1. 基本介绍

synchronized块是Java提供的一种原子性内置锁,也叫监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这个时候其他线程访问该同步代码块时,会被阻塞挂起。

拿到内部锁的线程会在正常退出同步块 或者 抛出异常后 或者在同步块内调用了 wait系列方法时 释放该内置锁

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorentermonitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。

img

monitor对象分为三个部分。waitSetEntryListOwner。当执行到Synchronized代码时,每个obj对象可以拥有一个Monitor对象,Owner(只能拥有一个值)属性记录当前线程Thread2,代表当前线程Thread2拥有这个锁对象,obj锁的其他线程会被记录到EntryList阻塞队列中去,当当前线程Thread2释放掉锁后,objMonitor会唤醒EntryList中记录的线程,不公平的再次争夺这个锁对象。

1.1 synchronized 保证线程安全

public class SafeDemo {
    static int count = 0;
    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        count++;
                    }
//                    System.out.println(Thread.currentThread().getName()+" already end");
                }
            },"thread:"+i);
            thread.start();
        }
        //等待所有线程执行完毕
        while (Thread.activeCount()>2){

        }

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

如同上面的案例,创建了20个线程,每个线程累加count一万次。理想状态下,得到的count值应该为200000。但是我们可以看到每次程序执行得到的结果永远都是小于200000。这种现象造成的原因是因为多个线程竞争同一个共享变量引发的线程安全问题。通过synchronized加锁的方式可以解决这个问题,如下。

public class SafeDemo {
    static int count = 0;
    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    //保证线程安全
                    synchronized (SafeDemo.class){
                        for (int j = 0; j < 10000; j++) {
                            count++;
                        }
                    }
//                    System.out.println(Thread.currentThread().getName()+" already end");
                }
            },"thread:"+i);
            thread.start();
        }
        //等待所有线程执行完毕
        while (Thread.activeCount()>2){

        }

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

仅仅只需要改变run(),使用synchronized关键字。保证同一时间内只有一个线程操作共享变量;

1.2 JVM中保证线程安全的原理

package com.wddong.testmoduleone.demoResp.synchronizedPackage;

public class SyncDemo {
    public static void main(String[] args) {

        synchronized (Object.class){
            System.out.println("synchronized jvm");
        }
    }
}
cd D:\ideaworkspace\wddongProject\testmoduleone\target\classes\com\wddong\testmoduleone\de
moResp\synchronizedPackage
javap -v SyncDemo.class

通过javap命令得到源文件的字节码,我们可以看到synchronized关键字,在JVM中是通过monitorentermonitorexit指令来保证线程同步的;

在这里插入图片描述

2. 锁对象分析

synchronized 锁对象的分析:

  • 静态方法: 静态方法 synchronized 锁的是 Class
  • 实例方法: synchronized 锁的是 方法的调用对象
  • 代码块:可以任意的去指定是锁Class模板还是具体对象
public class SynchronizedDemo {
    public static void main(String[] args) {
        SynchronizedCase synchronizedCase = new SynchronizedCase();
//        可以任意的去指定是锁Class模板还是具体对象
//        synchronized (SynchronizedCase.class){
        synchronized (synchronizedCase){
        }
    }
}
public class SynchronizedCase{
    //synchronized修饰静态方法,锁的是Class
    public synchronized static void method1(){   
    }
    //synchronized修饰实例方法,锁的是方法的调用对象
    public synchronized void method2(){
    }
}

3. 锁升级

减少了获取锁和释放锁带来的性能损耗,Java 1.6synchronized进行了各种优化;引入偏向锁轻量级锁Java 1.6 一共有四种状态,从低到高依次是无锁态==>偏向锁==>轻量级锁==>重量级锁锁只可以升级不能降级;

4. 对象头

一个Java对象的组成分为对象头实例数据对齐填充对象头又分为Mark word类型指针两部分;

Mark Word的组成:对象自身的运行时数据,HashCodeGC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等;

32bit64bit虚拟机下的Mark Word的存储结构差异:

img

5. 偏向锁

大多数情况下,锁不仅不存在多线程竞争,且总是由同一线程多次获取。为了让线程获取锁的代价更低而引入了偏向锁;

1. 偏向锁的加锁

当一个线程访问同步块并获取锁时,会在对象头栈帧中的锁记录(Lock Record)中存储锁偏向的线程ID,以后该线程在进入和退出同步代码块时不需要进行CAS操作来加锁解锁;

2. 偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置为无锁状态。如果线程仍然存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的mark word 要么重新偏向其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程;

3. 偏向锁的取消

偏向锁在Java6和Java7 里是默认启用的。但是他在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:--XX:BiasedLockingStartupDelay=0

JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,程序默认会进入轻量级锁状态;

在这里插入图片描述

6. 轻量级锁

轻量级锁加锁

线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录(Lock record)的空间,将对象头中的Mark word复制到锁记录(Lock record)中,官方称为displaced Mark word,然后线程尝试使用CAS将对象头中的Mark word替换成指向锁记录的指针;如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;

轻量级锁解锁

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

如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁

在这里插入图片描述

7. 重量级锁

重量级锁:当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。

8. 锁升级过程

在这里插入图片描述

阶段1:线程1 进入同步代码块,此时没有其他线程竞争。成功通过CAS获取到锁,此时Mark Word栈的锁记录(Lock Record)记录下线程1的线程ID,同时Mark Word是否偏向锁锁标志位值为 1|01

阶段2:线程1再次进入到同步代码块,此时锁对象中的Mark word中记录的还是线程1的线程ID没有被其他线程修改,所以此时为偏向锁,无需CAS操作直接成功加锁。

阶段3:线程2尝试通过CAS获取对象锁,此时对象的Mark Word存储的还是线程1的线程ID。所以此处要先执行偏向锁的撤销(上面有讲到)。,最终栈中的锁记录和对象头的mark word 偏向线程2的线程ID

阶段4:线程2再次尝试获取锁对象,但是此时刚好也有线程3尝试来获取锁对象。此时就会存在锁竞争。偏向锁会升级称为轻量级锁(上面我们也有介绍到轻量级锁的加锁过程)。此时栈的锁记录(Lock Record)会将对象头中Mark Word复制过来,然后线程尝试CAS将对象头中的Mark Word替换成锁记录的指针,成功代表获取锁成功,失败则自旋再次获取;

阶段5:上面阶段4有提到失败自旋再次获取,当自旋超过一定的次数后,轻量级锁就会升级称为重量级锁;

阶段5中长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。synchronized允许短时间的盲等。忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。当自旋数超过限制,就会升级为重量级锁;


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王叮咚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值