并发编程(二)synchronized锁

前言

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

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

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

所有的并发模式在解决线程安全问题时,采用的方案都是序列化(有序)访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
在这里插入图片描述

一、synchronized

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,保证数据原子性是可重入的。

1、对于静态方法,由于此时对象还未生成,所以只能采用类锁;

2、只要采用类锁,就会拦截所有线程,只能让一个线程访问。

3、对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。

4、如果对象锁跟访问的对象没有关系,那么就会都同时访问。

加锁的方式:

  • 同步代码块
    public void method(){
    
        /**
         * 加锁加到当前实例对象上
         */
        synchronized (object){
            int id = 1;
            System.out.println(id);
        }
    
    }
    
  • 同步实例方法,锁是当前实例对象
  • 同步类方法(有static关键字),锁是当前类对象
    /**
     * 加static 加锁加到类对象上
     * 锁是当前类对象
     * 
     * 不加static 加锁加到this类对象上,当前bean由容器管理,bean必须是单例模式,否则无效
     * 锁是当前实例对象
     */
    public static synchronized void method1(){
             
        int id = 1;
        System.out.println(id);
    
    }
    
  • 不使用synchronized跨方法枷锁
    
    public class DemoSynchronized {
        private static Object object =new Object();
    
        public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchFieldException {
            method2();
            method3();
        }
    
        public static void method2() throws InstantiationException, IllegalAccessException {
            Unsafe unsafe = reflectGetUnsafe();
            // 越过jvm虚拟机直接操作底层
            unsafe.monitorEnter(object);
    
            int id = 1;
            System.out.println(id);
    
        }
    
    
        public static void method3() throws InstantiationException, IllegalAccessException, NoSuchFieldException {
            Unsafe unsafe = reflectGetUnsafe();
            // 越过jvm虚拟机直接操作底层
            unsafe.monitorExit(object);
            int value = 1;
    
        }
    
        private static Unsafe reflectGetUnsafe() {
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                return (Unsafe) field.get(null);
            } catch (Exception e) {
                return null;
            }
        }
    
    }
    

每一个对象Object被创建之后,都会在jvm内部维护一个与之对应的Monitor

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁:操作系统维护)实现,它是一个重量级锁性能较低。

JVM内置锁在1.5之后版本做了重大的优化

  • 锁粗化(Lock Coarsening)
  • 锁消除(Lock Elimination)
  • 轻量级锁(Lightweight Locking)
  • 偏向锁(Biased Locking)
  • 适应性自旋(Adaptive Spinning)
  • 等技术来减少锁操作的开销
  • 内置锁的并发性能已经基本与Lock持平。

synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
在这里插入图片描述
线程Monitor.Exit结束后会通知其他线程获取Monitor.Enter

示例:

public class DemoSynchronized {
    private static Object object =new Object();

    public static void main(String[] args) {
        method();
    }

    public static void method(){
        synchronized (object){
            int id = 1;
            System.out.println(id);
        }

    }
}

在这里插入图片描述

对象内存结构

在这里插入图片描述

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头 (Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
  • 实例数据:即创建对象时,对象中成员变量,方法等
  • 对齐填充:对象的大小必须是8字节的整数倍

锁状态是被记录在每个对象的对象头(Mark Word)中

实例对象内存中存储

- 如果实例对象存储在堆区时:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据class存在方法区或者元空间

- Object实例对象不一定是存在堆区:如果实例对象可能产生线程逃逸行为

逃逸分析

/**
 * 进行两种测试
 * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
 * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 *
 * 开启逃逸分析 jdk1.7之后默认开启可以不加 -XX:+DoEscapeAnalysis 这个参数
 * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 *
 * 执行main方法后
 * jps 查看进程
 * jmap -histo 进程ID
 * 堆:可以理解为java 虚拟机空间
 * 栈:线程开辟的空间
 */
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 500000; i++) {
        // JIT即时编译器会对编译后的文件进行优化(逃逸优化)
        // 并不是所有对象存放在堆区,有的一部分存在线程栈空间
        Student student = new Student();
    }
    long end = System.currentTimeMillis();
    //查看执行时间
    System.out.println("cost-time " + (end - start) + " ms");
    try {
        Thread.sleep(100000);
    } catch (InterruptedException e1) {
        e1.printStackTrace();
    }
}

static class Student {
    int id;
    String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

关闭逃逸行为
在这里插入图片描述
开启逃逸行为
在这里插入图片描述### 逃逸分析编译器可以对代码做如下优化

  1. 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  2. 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  3. 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

所以不是所有的对象和数组都会在堆内存分配空间

从jdk 1.7开始已经默认开始逃逸分析

public void setObject(){
    // 锁不会生效,jvm会进行逃逸分析,进行优化,会产生逃逸,不会进行加锁
    // 想当于每次锁都是一个新的对象锁
    synchronized (new Object()){
        System.out.println(11111);
    }
}

锁的膨胀升级过程

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

锁的升级过程不会失去cpu使用权

Mark Word

在这里插入图片描述
在这里插入图片描述

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作耗时)的代价而引 入偏向锁。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

  • 性能能提升10%左右

场景:

  • 适合没有锁竞争的场景(单独一个线程访问共享资源 ),偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。

注意:

  • 不适合锁竞争比较激烈的场合,偏向锁会失效,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
  • 偏向锁不会自动释放,只有产生竞争才会释放
public class Biaslock {

    public static Vector<Integer> vector = new Vector<Integer>();

    /**
     * 默认开启偏向锁
     * 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
     * 关闭偏向锁:-XX:-UseBiasedLocking
     * @param args
     */
    public static void main(String[] args){
        long begin = System.currentTimeMillis();
        int count = 0;
        int num = 0;
        while(count < 10000000){
            vector.add(num);
            num = num + 5;
            count++;
        }
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }

}

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。

轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。

线程在进入阻塞块时,jvm不会直接生成重量级锁,因为jvm认为线程的阻塞和唤醒代价太高了,暂时性的自旋,不丢失cpu使用权。

场景:

  • 线程竞争不激烈,线程交替执行同步块的场合(线程访问共享资源产生等待的时间相对较短)

注意:

  • 如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

在这里插入图片描述

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。 最后没办法也就只能升级为重量级锁了。

java1.7之前可以自定义自旋次数。

java1.7之后自旋,可以根据上一次自旋成功值的次数弹性的决定下一次自旋次数(自适应自旋)

锁消除

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

JIT:Just In Time Compiler,一般翻译为即时编译器,这是是针对解释型语言而言的,而且并非虚拟机必须,是一种优化手段,Java的商用虚拟机HotSpot就有这种技术手段,Java虚拟机标准对JIT的存在没有作出任何规范,所以这是虚拟机实现的自定义优化技术。

例如:StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量, 并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

/**
 * StringBuffer:线程安全的
 */
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {

    // 同步锁方法,锁加载this实例对象上,理论上3次append相当于加了3次 synchronized锁,每调用一次append相当于加一次锁,需要最少3次上下文切换
    // 实际上jvm会对这种情况进行优化,实际上只加了一次 synchronized锁,相当于在所有的append外加了一个总的锁
    // 锁的粗化:锁消除
    sb.append("1");
    sb.append("2");
    sb.append("3");
}

锁优化升级过程

在这里插入图片描述
Mark Word
在这里插入图片描述
无锁到轻量级锁

  • 开始只有一个线程,几乎无竞争
    在这里插入图片描述
    到达安全点不意味着线程执行结束,有可能退出同步块,也可能没有退出

  • CAS:在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成

无锁到重量级锁

  • 起始则由并发访问同一共享资源,存在激烈竞争
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值