JUC-synchronized

JUC-synchronized,第1张

        Java采用synchronized关键字、以互斥同步的方式的解决线程安全问题,那么什么是线程安全呢?这里引用《Java并发编程实战》作者Brian Goetz给出的定义:

        “当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调 *** 作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。”—— Brian Goetz

一、synchronized的使用 (一)使用方法
      // 修饰实例方法
      public synchronized void test1(){

      }

      // 修饰代码块
      public void test2(){
        synchronized(new Test()){

        }
      }

      // 修饰静态方法
      public static synchronized void test3(){

      }
1、synchronized作用于实例方法
public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    /**
     * 输出结果:
     * 2000000
     */
}

        两个线程 *** 作同一个共享资源即变量 i ,由于i++; *** 作并不具备原子性,该 *** 作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取 i 的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1 *** 作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance。

        因此,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁。

2、synchronized作用于静态方法
public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

        由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发 *** 作。当synchronized作用于静态方法时,其锁就是当前类的class对象锁。

        如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

3、synchronized作用于代码块
public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时 *** 作....
        //使用同步代码块对变量i进行同步 *** 作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++; *** 作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

    //this,当前实例对象锁
    synchronized(this){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

    //class对象锁
    synchronized(AccountingSync.class){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

总结: 

        无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

(二)字节码分析
  public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED    // here

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/easy/helloworld/Test
         3: dup
         4: invokespecial #3                  // Method "":()V
         7: dup
         8: astore_1
         9: monitorenter                   // here
        10: aload_1
        11: monitorexit                    // here
        12: goto          20
        15: astore_2
        16: aload_1
        17: monitorexit                    // here
        18: aload_2
        19: athrow
        20: return

  public static synchronized void test3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED   // here

 可以观察到:

  • 修饰实例方法:隐式调用moniterenter、moniterexit
  • 修饰代码块:通过moniterenter、moniterexit 关联到到一个monitor对象,进入时设置Owner为当前线程,计数+1、退出-1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。
  • 修饰静态方法:隐式调用moniterenter、moniterexit
1、Java对象头

        moniterenter和moniterexit这两个jvm指令,主要是基于 Mark WordObject monitor来实现的。其中Mark Word在对象头中,对象头的具体结构如下:

2、Monitor 

        Monitor被翻译成监视器或管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word就被设置为指向Monitor对象的指针。

(这部分后续补充)

二、轻量级锁

        引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁使用的 *** 作系统互斥量带来的开销,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

        轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的,也就是没有竞争,那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized。

进入轻量级锁的流程:

  1. 获取对象的Mark Word,判断标识位是否为001,无锁状态且不可偏向。
  2. 如果标识位是001,创建锁记录(Lock Record)对象。每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
  3. 让锁记录中的Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。
  4. 如果CAS替换成功,对象头中存储了锁记录的机制和状态00,表示由该线程给对象加轻量级锁。
  5. 如果是锁重入情况,则再添加一条锁记录,并设置Mark Word=null。
  6. 当退出synchronized代码块时(解锁),如果锁记录取值不为null,使用CAS将Mark Word的值恢复给对象头,成功则解锁成功,反之说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,需要进入重量级锁解锁流程。
三、锁膨胀

        如果在尝试加轻量级锁的过程中,CAS *** 作无法成功,一种情况就是由其它线程为次对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当thread1进行轻量级加锁时,thread0已经对该对象加了轻量级锁

  •  这时thread1加轻量级锁失败,进入锁膨胀流程:
  1. 为Object对象申请Monitor锁,让Object指向重量级锁的地址,
  2. 然后自己进入Monitor的EntryList中阻塞。

  • 当thread0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中阻塞的线程。
四、自旋优化

        重量级锁竞争的时候,还可以使用自旋在进行优化,如果当前线程自旋成功,也就是这时候持锁线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞。

五、偏向锁

        经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS *** 作,耗时)的代价而引入偏向锁。

        偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步 *** 作,即获取锁的过程,这样就省去了大量有关锁申请的 *** 作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。

        但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

六、撤销 (一)调用对象的hashCode

        调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销,因为偏向锁对象MarkWord里没有hashCode。

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode
 (二)其它线程使用对象

        有偏向锁会使用偏向锁,如果有错开的其它线程使用这个对象,就撤销偏向锁,变成轻量级锁。如果这时有竞争发生(并没有错开),锁就会膨胀为重量级锁。

七、批量重偏向

        如果对象虽然被多个线程访问,但没有竞争,这时偏向了thread1的对象仍有机会重新偏向thread2,重偏向会重置对象的threadID。当撤销偏向锁的次数达到超过阈值20次之后,jvm会认为偏向错误,于是给这些对象加锁时重新偏向至加锁线程。

        假设有30个对象,这30个对象都偏向threadA,后来由于threadB来使用这些对象,在使用前20个对象的时候,会先撤销它们的偏向,再改为轻量级锁。循环20次后,jvm批量修改后面的对象,让它们偏向threadB。

八、批量撤销

        当撤销偏向锁阈值超过40次后,jvm会觉得这个类竞争过于激烈,根本不该偏向,于是整个类的所有对象都变为不可偏向,新建的对象也都是不可偏向的。

参考

https://blog.csdn.net/javazejian/article/details/72828483​​​​​​​

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/924636.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存