栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java同步互斥访问一(synchronized)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java同步互斥访问一(synchronized)

Java同步互斥访问一(synchronized)
  • 1、前置概念
    • 1.1、什么是同步互斥访问?
    • 1.2、Java中实现同步互斥访问的方法
    • 1.3、Java中为什么要提供两种同步器(synchronized 和 Lock)
  • 2、synchronized
    • 2.1、静态方法上加锁和普通代码块加锁的区别
    • 2.2、synchronized锁的信息在对象的什么地方
    • 2.3、通过mark word看synchronized锁
    • 2.4、测试 synchronized 锁的升级
    • 2.5、synchronized锁的升级过程
  • 3、synchronized锁中的一些名词解释
    • 3.1、无锁状态
    • 3.2、偏向锁
    • 3.3、轻量级锁
    • 3.4、重量级锁
    • 3.5、自旋锁
    • 3.6、锁消除
    • 3.7、逃逸分析
  • 10、辅助知识
    • 10.1、synchronized 三个锁阶段的hashcode分别存储在哪

1、前置概念 1.1、什么是同步互斥访问?

在多线程编程中,通常会有多个线程同时访问一个资源的情况,同步互斥访问就是在同一时间只能有一个线程对同一资源进行访问。

1.2、Java中实现同步互斥访问的方法

同步互斥访问的解决办法是设计一个同步器,对多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是: 对象、变量、文件等
同步器采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临 界资源。
共享:资源可以由多个线程同时访问。
可变:资源可以在其生命周期内被修改。
Java中目前有 synchronized 和 Lock (ReentrantLock)。

1.3、Java中为什么要提供两种同步器(synchronized 和 Lock)

synchronized在1.5版本时的状况:这是因为在jdk1.5版本的时候,jdk官方就提供出了 synchronized 锁,但是在1.5版本的时候,synchronized 锁的加锁方式只有一个,就是通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,也就是比较消耗性能。
Lock锁的出现:由于 synchronized 锁的性能不大好,加的锁都是重要级别的锁,涉及到线程之间的状态切换,要从用户态切换到内核态,所以就有一个人设计了Lock锁,在当时,Lock锁的性能要比 synchronized 好很多。
synchronized锁的优化:后来jdk官方就对synchronized锁进行了优化,成了现在这个样子,性能基本和Lock差不多了。
如下图所示:

2、synchronized 2.1、静态方法上加锁和普通代码块加锁的区别

静态代码块加synchronized锁

相当于对实例化的 this 加上了锁
代码示例如下:

public class Juc_LockOnObject {
    public static Object object = new Object();
    private Integer stock = 10;
    public void decrStock(){
        //T1,T2
        synchronized (object){
            --stock;
            if(stock <= 0){
                System.out.println("库存售罄");
                return;
            }
        }
    }
}

上面是代码,我们编译程字节码文件文件之后就会出现下图的样子,Jvm会给我们加上 monitorenter 和 monitorexit ,monitorexit 有三个,这是Jvm在解锁时做一个容错(异常)处理,如下图所示:


静态方法上加 synchronized

相当对 类.class 文件加上了锁。
代码示例如下:

public class Juc_LockOnClass {
    static int stock;

    public static synchronized void decrStock(){
        System.out.println(--stock);
    }

    public static synchronized void cgg(){
        System.out.println();
    }

    public static void main(String[] args) {
        //Juc_LockOnClass.class对象
        Juc_LockOnClass.decrStock();
    }

}

当我们输出上面代码的字节码之后就可以看到,在同步的方法上加上了 ACC_SYNCHRonIZED 关键字,这个标识jvm底层识别到之后也就会给代码块加上monitornter 和 monitorexit,如下图所示:

2.2、synchronized锁的信息在对象的什么地方

synchronized 锁的信息一般存储在对象的对象头中,对象头里面有一个Mark Word,如果是32位系统的话,是占4个字节的,对象头如下图所示:

Mark Word在不同的锁中存储的东西也是不相同的,如下图所示:

2.3、通过mark word看synchronized锁

在这里我们需要先导入一个看mark word的包,pom文件如下:


    
        org.openjdk.jol
        jol-core
        0.10
    


1、在不加锁的情况下,观察,代码示例如下:

public class T0_ObjectSize {

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

我们执行代码,打印出下面的信息:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DEscriptION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

首先,offset[0,4]这块是我们的mark word,我们可以看到后面的二进制码如下所示:
00000001 00000000 00000000 00000000
注意:上面这个二进制码我们不能直接看,是因为我们的windows和linux都是小端模式,(这个东西分为大端模式和小端模式),所以当为小端模式的时候,应该把这四个二进制码反过来看,如下所示:
00000000 00000000 00000000 00000001
结论:所以我们看到了最后三位数字为 001 ,对照上面的Mark Word表格,说明现在对象是一个无锁态的状态。


2、(不加延迟时)在加锁的情况下,观察,代码示例如下:

public class T0_ObjectSize {

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

执行上面的代码之后会出现下面的结果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DEscriptION                               VALUE
      0     4        (object header)                           d8 f2 8c 02 (11011000 11110010 10001100 00000010) (42791640)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

注意:我们会惊奇的发现上面的锁状态是轻量级锁。
解释:这是因为Jvm在启动的过程中,也会启动十几个线程,这些线程之间会存在内部竞争,所以Jvm为了防止锁升级而消耗资源,就推迟了偏向锁的启动,会先启动轻量级锁,一般会有个4s左右的延迟。


3、(加延迟时)在加锁的情况下,观察,代码示例如下:

public class T0_ObjectSize {

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

执行程序后,显示如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DEscriptION                               VALUE
      0     4        (object header)                           05 28 4d 03 (00000101 00101000 01001101 00000011) (55388165)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从上面我们就可以看出,当延迟了5s之后,就加上了偏向锁。


4、这里有一个名词需要注意,那就是可偏向状态
可偏向状态指的是,预先做好准备,可以做偏向,但是现在还不是偏向锁的时候
代码如下:

public class T0_ObjectSize {
    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());
        }
    }
}

当我们的代码这样写时,就出现了可偏向状态(也叫匿名偏向),如下图所示;

2.4、测试 synchronized 锁的升级

1、偏向锁向轻量级锁的升级
代码示例如下:

public class T0_BasicLock {
    public static void main(String[] args) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object o = new Object();
        // 只有一个线程在用到对象 o,所以是偏向锁
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        new Thread(()->{
            synchronized (o){
                // 只有一个线程在用到对象 o,所以是偏向锁
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();

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

        // 只有一个线程在用到对象 o,所以是偏向锁
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o){
                // 有两个线程用了到对象 o,升级到了轻量级锁
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }
}

执行上面代码之后,我们会发现前三个都是偏向锁,最后一个当两个线程同时访问一个对象时,就变成了轻量级锁,如下图所示:


2、轻量级锁向重量级锁的升级
代码示例如下:

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();
    }

}

上面代码执行完成之后,我们会发现打印的两个锁都是重量级锁,这是因为轻量级锁在执行的过程中,如果有资源争抢的情况,会自己进行自旋(spin,就相当于执行空循环),当然这个自旋有一定的次数,我们在程序里面睡眠了2s,所以自旋的次数已经已经达到了,所以CPU认为就是抢占资源比较严重的情况,就自己将轻量级锁升级成了重量级锁。

2.5、synchronized锁的升级过程

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

3、synchronized锁中的一些名词解释 3.1、无锁状态

顾名思义,就是这个对象还没有加锁的状态。

3.2、偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。但当有多个线程同时访问对象时,并且竞争不是特别激烈(激烈意思就是其它线程不用等很久,比如说一两秒那种)时,就会升级成轻量级锁。
总结:也就是当一个对象只有一个线程进行访问时,它的锁就是偏向锁。
默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

3.3、轻量级锁

当有多个线程同时访问被加锁的对象时,偏向锁会首先升级为轻量级锁,轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞 争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场 合,就会导致轻量级锁膨胀为重量级锁。
总结:当多线程竞争不是很激烈(激烈意思就是其它线程不用等很久,比如说一两秒那种),就会是轻量级锁,否则,就升级为重量级锁。

3.4、重量级锁

是OS的一个mutex锁,非常消耗性能,也是一种互斥锁,由操作系统维护。

3.5、自旋锁

由于一般情况下锁的等待都会很短,而将线程挂起与激活都需要状态切换(用户态到内核态之间的切换),这个状态切换是非常消耗性能的,所以当已知在等很短的时间的时候,再切换状态是很得不偿失的,所以JVM会让当前的线程自己做几个空循环,可能是50个或者100个(这也就是自旋的由来),当在这个自旋的过程中获取到了锁,就去执行相应的业务逻辑,如果没有获取到,就将线程挂起。

3.6、锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时 进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以 节省毫无意义的请求锁时间,
总结:锁消除是Jvm通过上下文的扫描之后,通过逃逸分析这个锁对象不会有公共资源的竞争,就会进行锁的消除。

3.7、逃逸分析

分析当前的锁对象会不会逃出当前线程的控制范围,比如说,方法里面的局部变量,就不会逃出当前线程的范围,当前线程栈销毁后,就会销毁那个局部变量。

10、辅助知识 10.1、synchronized 三个锁阶段的hashcode分别存储在哪

1、偏向锁
可能是实时计算的,可能没有存储,因为当一个对象在拥有偏向锁时,你去调用它的hashcode方法,它会升级成轻量级锁,代码示例如下:

public class Juc_PrintMarkWord {

    public static void main(String[] args) throws InterruptedException {
        // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
        // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动
        // 偏向锁,会出现很多没有必要的锁撤销
        Thread.sleep(5000);
        T t = new T();
        //未出现任何获取锁的时候
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        synchronized (t){
            // 获取一次锁之后
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
        // 输出hashcode
        System.out.println(t.hashCode());
        // 计算了hashcode之后,将导致锁的升级
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        synchronized (t){
            // 再次获取锁
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
    }
}

class T{
    int i = 0;
}

上面的代码执行之后,我们就会发现在调用了Hashcode方法之后,偏向锁就会升级成轻量级锁,如下图所示:


2、轻量级锁
hashCode存储在本地线程栈里面


3、重量级锁
hashCode存放在minitor中

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/287183.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号