多线程同步

Synchronized同步锁

什么是Synchronized同步锁?

        Synchronized同步锁,简单来说,使用Synchronized关键字将一段代码逻辑,用一把锁给给锁起来,只有获得了这把锁的线程才访问。并且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码,从而确保代码的线程安全。

什么是锁?

        每个Java对象多可以充当一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)

synchronized(reference-to-lock){
    //临界区
}

         其中, 括号里面的reference-to-lock就是锁的引用, 任何一个Java对象都可以成为reference-to-lock。你可以实例化一个Object对象,将它作为锁。如果直接使用this,代表使用当前使用对象作为锁。

Synchronized关键字的用法

  1. 修饰实例方法:Synchronized修饰实例方法,则用到的锁,默认为this当前方法调用对象;
  2. 修饰静态方法:Synchronized修饰静态方法,则其所用的锁,默认为Class对象;
  3. 修饰代码块:Synchronized修饰代码块,则其所用的锁,是某个指定Java对象。

Synchronized修饰实例方法

  • 使用当前对象this充当锁,完成对当前方法的锁定,只有获取this锁的线程才能访问当前方法;
  • 并发过程中,同一时刻,可以有N个线程请求执行方法,但只有一个线程可以持有this锁,才能执行;
  • 不同线程,持有的对象,必须相同。

当使用Synchronized修饰实例方法是,以下两种写法作用和意义相同:

public class Foo{
    //实例方法
    public synchronized void doSth1(){
        //获取this锁,才能执行该方法
    }
    
    //实例方法
    public void doSth2(){
        synchronized (this){
            //获取this锁,才能执行该方法
        }
    }
}

 

public static void main(String[] args) {
        //实例化一个对象
        Foo fa = new Foo();
        
        //创建不同的线程1
        Thread thread1 = new Thread(){
            public void run(){
                //使用相同的对象访问synchronized方法
                fa.doSth1();
            }
        };

        //创建不同的线程2
        Thread thread2 = new Thread(){
            public void run(){
                //使用相同的对象访问synchronized方法
                fa.doSth1();
            }
        };
        
        //启动线程
        thread1.start();
        thread2.start();
    }

Synchronized修饰静态方法

  • 使用地区对象的Class对象充当锁,完成对当前方法的锁定,只有获取Class锁的线程才能访问当前对象;
  • 不同线程,持有的对象,可以不同,但必须相同class类型

当使用Synchronized修饰静态方法时,以下两种写法作用和意义相同:

public class Foo {
    //静态方法
    public synchronized static void doSth1(){
        //获取当前对象的Class对象锁,才能执行该方法
    }

    //实例方法
    public static void doSth2(){
        synchronized (this.getClass()){
            //获取当前对象的Class对象锁,才能执行该方法
        }
    }
}
 public static void main(String[] args) {
        //创建不同的对象(相同类型)
        Foo fa = new Foo();
        Foo fb = new Foo();

        //创建不同的线程1
        Thread thread1 = new Thread(){
            public void run(){
                //使用不同的对象访问synchronized方法
                fa.doSth2();
            }
        };

        //创建不同的线程2
        Thread thread2 = new Thread(){
            public void run(){
                //使用不同的对象访问synchronized方法
                fb.doSth2();
            }
        };

        //启动线程
        thread1.start();
        thread2.start();
    }

Synchronized修饰代码块

synchronized (自定义对象){
            //临界区
}

Synchronized关键字的补充

1.当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。

  • 在没有加锁的情况下,所有的线程都可以自由地访问对象中的代码,而synchronized关键字只是限制了线程对于已经加锁的同步代码块的访问,并不会对其他代码做限制。所以,同步代码块应该越短小越好。

2.父类中 synchronized 修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用 synchronized 修饰,则该方法不是线程安全的;

3.在定义接口方法时,不能使用 synchronized 关键字;

4.构造方法不能使用 synchronized 关键字,但可以使用 synchronized 代码块来进行同步;

5.离开 synchronized 代码块后,该线程所持有的锁,会自动释放。

Synchronized实现原理

概述

        synchronized 是Java 内建的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能阻塞在那里。
        在 java 5 以前,synchronized 是仅有的线程同步手段。在代码中,synchronized 可以用来修饰方法,也可以使用在特定的代码块上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。
        synchronized 同步锁在加锁和解锁的过程中,依赖于操作系统互斥锁(MutexLock) 所实现的锁,消耗资源,属于重量级锁 (在 java1.6 以后进行了优化)。另外在获取锁时,必须一直等待,没有额外的尝试机制。

synchronized底层实现

        使用synchronized保证线程安全,就是保证原子性,简单说就是执行过程中不会被其他线程干扰。

监视器(monitor)

        在 JVM 实现规范中关于 monitor 描述: 每个对象有一个监视器 ( monitor ),线程通过执行 monitorenter 指令尝试获取 monitor 的所有权,当 monitor 被占用时就会处于锁定状态。

获取 monitor 的所有权的过程如下

  1. 如果 monitor 的进入数为  ,则该线程进入 monitor ,然后将进入数设置为 1 ,该线程即为 monitor 的所有者,代表持有锁;
  2. 如果线程已经占有该 monitor2,只是重新进入,则进入 monitor 的进入数加 +1
  3. 如果其他线程已经占用了 monitor ,则该线程进入阻塞状态,直到 monitor 的进入数为 ,再重新尝试获取 monitor 的所有权

锁升级

        在 JVM 底层实现锁的过程中,有三种类型的锁:偏斜锁,轻量级锁,重量级锁
        在 java 6 之前, synchronized 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,非常消耗系统资源。
在 ava 6 之后,在 oracle JDK 中,JVM 对此 synchronized 进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁( Biased Locking )、轻量级锁和重量级锁,大大改进了其性能。
        所谓锁的升级降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到降级不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

偏向锁 

        偏向锁的核心思想是“假设加锁的代码从始至终就只有一个线程在调用,如果发现有多于个线程调用,再升级成轻量级锁”
        偏向锁是为了在单线程(没有出现多个线程并发)执行情况下,尽量减少不必要的轻量级锁执行路径,该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。因为轻量级锁的加锁与释放锁,也需要多次执行CAS原子指令。而偏向锁只需要在切换线程设置 ThreadID 的时候,执行一次CAS原子指令。所以,偏向锁的作用是在只有一个线程执行同步块时,进一步提高性能。

        当没有线程并发出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作 (compareand swap),在对象头上的 Mark word 部分设置线程 ID ,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象, JVM 就需要撤销( revok偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark word 来试图获取e锁,如果重试成功,就使用普通的轻量级锁,否则,进一步升级为重量级锁。

轻量级锁

        “轻量级”的概念,是相对于“使用操作系统互斥锁来实现的重量级锁”,但轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一把锁的情况,就会导致轻量级锁升级为重量级锁。

        根据轻量级锁的实现,虽然轻量级锁不支持“并发”,遇到“并发”就要升级为重量根据轻量级锁的实现,虽然轻量级锁不支持“并发”级锁。但是轻量级锁可以支持多个线程以串行的方式访问同一个加锁对象。但是,每次执行,都消耗了重复的加锁与解锁的性能开销。
        例如: A线程可以先获取对象 obj 的轻量锁,然后A线程释放了锁,这个时候B线程来获取 obj 的轻量锁,可以成功获取 obj 的轻量锁。其余线程对这个 obj 轻量锁的获取,也以这种方式可以一直串行下去。之所以能实现这种串行,是因为有一个释放锁的动作
轻量级锁与偏向锁的区别:假设有一个加锁的方法,这个方法在运行的时候,并没有出现并发的情况,从始至终只有一个线程在调用,如果使用轻量级锁,每次调用完也要释放锁,下次调用还要重新获得锁。

        锁的状态,保存在对象头中。在 Hotspot 虚拟机中,一个 JAVA 对象的存储结构在内存中的存储布局分为 3 块区域: 对象头 ( Header )、实例数据 ( Instance Data ) 和对齐填充( Padding )。

 

  • lock :标志位: 2位二进制,锁状态标记位
  • age :Java对象年龄: 在 GC 中,如果对象在 survivor 区复制一次,年龄增加 1 。当对象达到设定的闽值时,将会晋升到老年代。
  • thread : 持有偏向锁的线程ID
  • ptr_to_lock_record : 指向栈中锁记录的指针
轻量级锁的加锁过程
  1. 在代码进入同步块的时候,如果对象锁状态为无锁状态 (lock标志位 ”1biased lock标志位””),虚拟机首先将在当前线程的栈顿中建立一个名为锁记录( Lock Record ) 的空间,用于存储锁对象目前的 Mark word 的拷贝,官方命名为 Displaced Mark Word 。
  2. 拷贝对象头中的 Mark word 复制到锁记录  Lock Record ) 中。
  3. 拷贝成功后,虚拟机将尝试将对象的 Mark word 中的ptr to lock record更新为指向 Lock Record 的指针,并将 Lock record 里的 owner 指针指向到对象的 Mark word 。如果更新成功,则执行步骤4,否则执行步骤5。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark word 的lock标志位设置为”“即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的 Mark word 是否已经指向当前线程的栈帧。如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争该对象的锁, 轻量级锁就要升级为 重量级锁lock标志位的状态值变为 ”10 ”, Mark word 中存储的就是指向重量级锁 (互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的解锁过程
  1. 通过 CAS 指令,尝试把线程中复制的 Displaced Mark word 对象替换当前的 Mark Word。
  2. 如果替换成功,整个同步过程就完成了
  3. 如果替换失败,说明有其他线程尝试过获取该锁,该锁已升级为重量级锁那就要在释放锁的同时,通知其它线程重新参与锁的竞争。

重量级锁

        依赖于操作系统互斥锁( Mutex Lock ) 所实现的锁。操作系统的互斥锁实现线程之间的切换,需要从用户态转换到核心态,切换成本非常高,状态之间的转换需要相对比较长的时间,这是早期 synchronized 效率低的原因。因此,这种依赖于操作系统互斥锁(Mutex Lock ) 所实现的锁,称之为“重量级锁”。

        用户态和核心态,代表两种不同的CPU状态。内核态( Kernel Mode )用于运行操作系统程序,用户态(user Mode ) 用于运行用户程序。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值