Java同步互斥访问一(synchronized)

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文件如下:

<dependencies>
    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.10</version>
    </dependency>
</dependencies>

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中

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个基于Java的使用synchronized关键字解决进程间同步互斥问题的示例代码: ``` public class MyClass { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public synchronized int getCount() { return count; } } ``` 在这个示例中,MyClass类有一个count属性,用于记录一个计数器的值。increment()方法和decrement()方法分别用于增加和减少计数器的值,getCount()方法用于获取计数器的当前值。 这些方法都使用了synchronized关键字来实现同步互斥。在使用synchronized关键字时,需要将需要同步的代码块或方法用synchronized关键字进行修饰。在这个示例中,所有的方法都使用了synchronized关键字,因此同一时刻只有一个线程可以访问这些方法。 使用这个示例的方法很简单,在多个线程中创建一个MyClass的实例,然后分别调用increment()和decrement()方法即可。在多个线程同时访问这个对象时,synchronized关键字会自动实现同步互斥,保证计数器的值正确无误。 需要注意的是,在使用synchronized关键字时,需要避免死锁和竞态条件等问题。死锁是指多个线程互相等待对方释放资源的情况,可以通过避免重复获取锁、按照固定的顺序获取锁等方法来避免死锁。竞态条件是指多个线程同时访问共享资源,并且对资源的访问顺序不确定的情况,可以通过使用volatile关键字、使用同步容器等方法来避免竞态条件。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值