synchronized及其优化

参考博文:
http://www.cnblogs.com/wade-luffy/p/5969418.html
http://www.cnblogs.com/kniught-ice/p/5189997.html
https://www.zhihu.com/question/270564693



一、锁是什么?

在java中对象都可以作为锁。
普通同步方法:锁是当前实例对象。
静态同步方法:锁是当前的class对象
同步方法块:锁是sychonized括号中的对象。


1、根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法
  1. 同步代码块synchronized (this) , synchronized (类实例对象),锁是小括号()中的实例对象。
  2. 同步非静态方法 synchronized method , 锁是当前对象的实例对象。
获取类锁的两种用法
  1. 同步代码块synchronized (类.class), 锁是小括号()中的类对象(Class对象)。
  2. 同步静态方法 synchronized static method, 锁是当前对象的类对象(Class对象)。

2、对象锁和类锁的总结
  • 有线程访问对象的同步代码块时 ,另外的线程可以访问该对象的非同步代码块;
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
  • 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
  • 同一个类的不同对象的对象锁互不干扰;
  • 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
  • 类锁和对象锁互不干扰。

3、其他锁相关知识点:
  • jvm是基于进入和退出monitor对象来实现方法的同步和代码块的同步。
  • synchronized锁的不是代码,锁的是对象。
  • Java提供了synchronized关键字来支持内在锁。Synchronized关键字可以放在方法的前面、对象的前面、类的前面。
  • Java虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,无论是显式同步(有明确的monitorentermonitorexit指令,即同步代码块)还是隐式同步都是如此。


二、synchronized底层实现


1、synchronized底层语义原理

在JVM的规范中,有这么一些话:“在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的,为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁,锁住了一个对象,就是获得对象相关联的监视器”

在Java中,每个对象都会有一个monitor对象监视器。

  • Java虚拟机中的一个线程在它到达监视区域开始处的时候请求一个锁。JAVA程序中每一个监视区域都和一个对象引用相关联。某一线程占有这个对象的时候,先monitor的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
  • 同一线程可以对同一对象进行多次加锁,+1, +1,重入性,而一个锁就像一种任何时候只允许一个线程拥有的特权.一个线程可以允许多次对同一对象上锁.对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了.
  • 在Java语言中,同步用的最多的地方可能是被synchronized修饰的同步方法。同步方法并不是由monitorentermonitorexit指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的ACC_ SYNCHRONIZED标志来隐式实现的。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

2、下面我们通过代码进行理解
当使用synchronized关键字对方法加上同步锁
public class SynchronizedDemo{
    private static int m = 0;
    public synchronized static void synchronizedFun(){
        try {
            Thread.sleep(2);
            m++;
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

我们对字节码文件使用javap -v时,我们会发现反编译的文件中对方法添加synchronized关键字,会在该方法头的flags中存在一个ACC_SYNCHRONIZED(隐式同步)

public static synchronized void synchronizedFun();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=0
         0: ldc2_w        #2                  // long 2l
         3: invokestatic  #4                  // Method java/lang/Thread.sleep:(J)V
         6: getstatic     #5                  // Field m:I
         9: iconst_1
        10: iadd
        11: putstatic     #5                  // Field m:I
        14: goto          22
        17: astore_0
        18: aload_0
        19: invokevirtual #7                  // Method java/lang/InterruptedException.printStackTrace:()V
        22: return
      Exception table:
         from    to  target type
             0    14    17   Class java/lang/InterruptedException
      LineNumberTable:
        line 5: 0
        line 6: 6
        line 9: 14
        line 7: 17
        line 8: 18
        line 10: 22
      StackMapTable: number_of_entries = 2
        frame_type = 81 /* same_locals_1_stack_item */
          stack = [ class java/lang/InterruptedException ]
        frame_type = 4 /* same */
当使用synchronized关键字对代码块加上同步锁
public class SynchronizedDemo{
    public  static void synchronizedFun(){
        try {
        	synchronized(this){
	        	TimeUnit.MINUTES.sleep(2);
    	        m++;
        	}    
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

我们对字节码文件使用javap -v时,我们会发现反编译的文件中发现第3行出现了monitorexit,第19行和第25行出现了monitorexit,对于为什么只有一个enter却有两个exit是因为,同步锁是原子性的,所以有一个为当失败时进行回滚的exit

public void synchronizedFun();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: ldc2_w        #2                  // long 2l
         7: invokestatic  #4                  // Method java/lang/Thread.sleep:(J)V
        10: getstatic     #5                  // Field m:I
        13: iconst_1
        14: iadd
        15: putstatic     #5                  // Field m:I
        18: aload_1
        19: monitorexit
        20: goto          28
        23: astore_2
        24: aload_1
        25: monitorexit
        26: aload_2
        27: athrow
        28: goto          36
        31: astore_1
        32: aload_1
        33: invokevirtual #7                  // Method java/lang/InterruptedException.printStackTrace:()V
        36: return
      Exception table:
         from    to  target type
             4    20    23   any
            23    26    23   any
             0    28    31   Class java/lang/InterruptedException
      LineNumberTable:
        line 5: 0
        line 6: 4
        line 7: 10
        line 8: 18
        line 11: 28
        line 9: 31
        line 10: 32
        line 12: 36
      StackMapTable: number_of_entries = 4
        frame_type = 255 /* full_frame */
          offset_delta = 23
          locals = [ class SynchronizedDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
        frame_type = 66 /* same_locals_1_stack_item */
          stack = [ class java/lang/InterruptedException ]
        frame_type = 4 /* same */


三、理解Java对象头与Monitor

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
  • Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark WordClass Metadata Address组成,其结构说明如下表:
长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果当前对象是数组)

1、对象头的Mark Word默认存储的对象的hashCode,分代年龄和锁标志位
锁状态25bit4bit1bit是否是偏向锁2bit锁标志位
无锁状态对象的hashCode对象分代年龄001
2、 Mark Word的状态变化

在这里插入图片描述



四、synchronized的优化-偏向锁、轻量级锁、重量级锁

  • synchronized是java多线程编程的元老级人物,也被称为重量级锁
  • 偏向锁和轻量级锁之所以会在性能上比重量级锁好是因为本质上偏向锁和轻量级锁仅仅使用了CAS

1、偏向锁:仅适用于锁没有竞争的情况,假设共享变量只有一个线程访问。如果有其他线程竞争锁,锁则会膨胀为轻量级锁。
加锁方式:
  1. 初始时对象处于biasable状态,并且ThreadID为0即biasable&unbiased状态。
  2. 当一个线程视图锁住处于biasable&unbiased状态的对象时,通过一个CAS锁将自己的ThreadID放置到Mark Word中的相应位置,如果CAS操作成功则进入第三步,否则进入第四步。
  3. 当进入此步则表示所没有竞争,Object继续保持biasable状态,但是这是的ThreadID字段设置成了偏向锁所有者的ID,然后执行同步代码块。
  4. 当线程执行CAS获取偏向锁失败,表示在该锁对象上存在竞争并且这个时候另一个线程获得偏向锁的所有权。当到达全局安全点是获取偏向锁的线程被挂起,并将Object设置为LightWeight Lock(轻量级锁)状态并且Mark Word中的Lock Record指向刚才持有偏向锁线程的Monitor record(监视器记录),最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。
解锁过程:
  • 偏向锁解锁过程很简单,只需要测试下是否Object上的偏向锁模式是否还存在,如果存在则解锁成功不需要任何其他额外的操作。

2、轻量级锁:适用于锁有多个竞争,但是在一个同步方法块周期中锁不存在竞争,如果在同步周期内有其他线程竞争锁,锁会膨胀为重量级锁。
加锁过程:
  • 线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获取锁,如果失败,则进行自旋获取锁,当自选获取锁仍然失败是,表示当前线程存在两条或两条以上线程竞争同一个锁,则轻量级锁膨胀成重量级锁。
解锁过程:
  • 轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败则表示有其他线程尝试过获取锁,则要将释放锁的同时唤醒被挂起的线程。

3.重量级锁:竞争激烈的情况下使用重量级锁。
  • 重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

4、通过JVM参数来修改锁状态
  • 偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

5、自旋锁
  • 线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了自旋锁。

  • 所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。

  • 自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。


6、自旋锁和轻量级锁的关系
  • 轻量级锁是一种状态,而自旋锁是一种获取锁的方式。线程首先会通过CAS获取锁,失败后通过自旋锁来尝试获取锁,再失败锁就膨胀为重量级锁。所以轻量级锁状态下可能会有自旋锁的参与(cas将对象头的标记指向锁记录指针失败的时候)


五、偏向锁,轻量级锁,重量级锁对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程使用自旋会消耗CPU追求响应时间,锁占用时间很短
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,锁占用时间较长
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
优化是指在多线程编程中,通过改进的机制和使用方式来提高程序的性能和并发能力。synchronized关键字是Java中最常用的机制之一,它可以保证同一时间只有一个线程可以进入被synchronized修饰的代码块。下面是一些synchronized优化的方法: 1. 减小的粒度:如果在一个方法中有多个synchronized代码块,可以考虑将这些代码块拆分成多个方法,以减小的粒度。这样可以使得多个线程可以并发执行不同的代码块,提高程序的并发性能。 2. 使用局部变量替代成员变量:在使用synchronized关键字时,尽量使用局部变量而不是成员变量。因为成员变量的访问需要通过对象实例来进行,而局部变量的访问是线程私有的,不需要加。 3. 使用同步代码块代替同步方法:在某些情况下,使用同步代码块比使用同步方法更加灵活。同步代码块可以指定的粒度,只对需要同步的代码进行加,而不是整个方法。 4. 使用volatile关键字:volatile关键字可以保证变量的可见性和禁止指令重排序,可以在一定程度上替代synchronized关键字。但是需要注意,volatile关键字只能保证单个变量的原子性,不能保证多个操作的原子性。 5. 使用Lock接口:Java提供了Lock接口及其实现类ReentrantLock,相比于synchronized关键字,Lock接口提供了更加灵活的机制。可以手动控制的获取和释放,可以实现公平和非公平,并且支持多个条件变量。 6. 使用读写:如果在多线程环境下,读操作远远多于写操作,可以考虑使用读写ReadWriteLock来提高程序的并发性能。读写允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。 7. 使用并发集合类:Java提供了一些并发集合类,ConcurrentHashMap、ConcurrentLinkedQueue等,它们内部使用了一些优化的技术,可以提高多线程环境下的并发性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值