并发编程之synchronized详解

设计同步器的意义

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源
这种资源可能是: 对象、变量、文件等。

  • 共享:资源可以由多个线程同时访问
  • 可变:资源可以在其生命周期内被修改

引出的问题: 由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

如何解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。

即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。

加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题

synchronized原理详解

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。
当然,JVM内置锁在1.5 之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与 Lock持平。

我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?
答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局

对象的内存布局

在这里插入图片描述
对象头:

在这里插入图片描述

偏向锁

为了更好的弄清楚,引入org.openjdk.jol.info.ClassLayout,我们看代码:

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.10</version>
</dependency>
 public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

运行结果:
在这里插入图片描述
我们看标红的就是markWord。
00000001 00000000 00000000 00000000 这后面的是0。但是我们要反过来看
因为在操作系统分为2种模式:大端模式和小端模式。

在这里插入图片描述

我们在这里用的是小端模式:
00000000 00000000 00000000 00000001
这里new出来的对象是无锁状态。这里解释下对象的hashcode为什么是0
hashcode是懒加载方式,所以高25位一开始没打印出来。
那我们开始升级锁,看代码:

public static void main(String[] args) throws InterruptedException {
      Object o = new Object();
      System.out.println(ClassLayout.parseInstance(o).toPrintable());
      synchronized (o){
          System.out.println(ClassLayout.parseInstance(o).toPrintable());
      }
}
运行结果:

在这里插入图片描述
那为什么会直接升级为轻量级锁,不应该是先升级成偏向锁吗?
JVM会延迟去启动偏向锁,大概是4秒钟时间。原因
JVM在启动的时候会加载hashmap,class等等,这里面也会有大量的同步块。而且还有10几个线程。JVM内部本身有一些竞争,为了减少锁升级带来的开销,把偏向锁推迟启动了。
我们延迟5秒去启动,再来看下。默认是开启偏向锁。

public static void main(String[] args) throws InterruptedException {
     TimeUnit.SECONDS.sleep(5);
     Object o = new Object();
     System.out.println(ClassLayout.parseInstance(o).toPrintable());
     synchronized (o){
         System.out.println(ClassLayout.parseInstance(o).toPrintable());
     }
 }
运行结果:

在这里插入图片描述
我们看见第一个打印没有加同步块的:00000101 00000000 00000000 00000000
第二个打印是加了同步块的:00000101 10111000 00111100 00000010
难道说我们启动了偏向锁后,即使没有同步块也会加锁吗?
我们称没加同步块的为匿名偏向00000101 00000000 00000000 00000000。后边的都是0。代表着当前是可偏向状态。可以理解为已经预先做好准备了,可以偏向,但是还没有偏向,至于偏向谁还不知道。

再给大家看个例子:

@Slf4j
public class T0_BasicLock {
    public static void main(String[] args) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object o = new Object();
        log.info(ClassLayout.parseInstance(o).toPrintable());

        new Thread(()->{
            synchronized (o){
                log.info(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o){
                log.info(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }
}
运行结果:线程交替执行最后会升级轻量级锁。

在这里插入图片描述
模拟下升级重量级锁:

public class T0_heavyWeightMonitor {

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object a = new Object();

        Thread thread1 = new Thread(){
            @Override
            public void run() {
                synchronized (a){
                    System.out.println("thread1 locking");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    try {
                        //让线程晚点儿死亡,造成锁的竞争
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                synchronized (a){
                    System.out.println("thread2 locking");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();
        thread2.start();
    }

}

竞争非常激烈:
在这里插入图片描述

锁的膨胀升级过程

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
从JDK 1.6 中默认是开启偏向锁和轻量级锁 的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

关于synchronized的使用可以看我的下篇博客 synchronize的8锁现象带你彻底了解

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值