synchronized

一、synchronized介绍

线程安全问题的主要诱因

  存在共享数据(也称临界资源)

  存在多条线程共同操作这些数据

解决问题的根本方法:

  同一时刻有且只有一个线程操作共享数据,其它线程必须等待该线程处理完数据后再对共享数据进行操作。

互斥锁的特性

互斥性: 即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称操作的原子性。

可见性:必须确保在锁被释放之前,对共享变量所做的修改,对应随后获得该锁的另一个线程是可见的(即获得锁的同时应获得最新共享变量的值),否则另一个线程可能是在本地缓存某个副本上继续操作,从而引起不一致。

synchronized锁的不是代码,锁的都是对象。

根据获取锁的分类: 获取对象锁和获取类锁

获取对象锁的两种用法

1、同步代码块( synchronized(this),  synchronized(类实例对象) ),锁是括号() 中的实例对象。

2、同步非静态方法( synchronized method) ,锁是当前对象的实例对象

获取类锁的两种用法

1、同步代码块(synchronized(类.class)), 锁的是小括号()中的类对象(Class对象).

2、同步静态方法 (synchronized static method ) , 锁是当前对象的类对象 (Class对象)

二、synchronized底层实现原理

实现synchronized基础

Java对象头

Monitor

对象在内存中的布局

对象头

实例数据

对齐填充

对象头的结构

Mark Word

Monitor: 每个Java对象天生自带了一把看不见的锁

进入源码: http://hg.openjdk.java.net/jdk8u/hs-dev/hotspot/file/ae5624088d86/src/share/vm/runtime/objectMonitor.hpp

可以发现里面有两个队列 _WaitSet 和 _EntryList

 _owner 指向持有objectMonitor的线程

下面从字节码的角度查看synchronize

创建类

public class SyncBlockAndMethod {

    public void syncsTask(){
        synchronized (this){
            System.out.println("Hello");
        }
    }

    public synchronized void syncTask(){
        System.out.println("Hello again");
    }

}

  然后编译成class文件

javac SyncBlockAndMethod.java

查看字节码

javap -verbose SyncBlockAndMethod.class

Classfile /xxx/src/thread/SyncBlockAndMethod.class
  Last modified 2019-12-29; size 613 bytes
  MD5 checksum 9aa751fc8ed2cf7d372724572edfb1a8
  Compiled from "SyncBlockAndMethod.java"
public class thread.SyncBlockAndMethod
  SourceFile: "SyncBlockAndMethod.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#20         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            //  Hello
   #4 = Methodref          #24.#25        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #26            //  Hello again
   #6 = Class              #27            //  thread/SyncBlockAndMethod
   #7 = Class              #28            //  java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               syncsTask
  #13 = Utf8               StackMapTable
  #14 = Class              #27            //  thread/SyncBlockAndMethod
  #15 = Class              #28            //  java/lang/Object
  #16 = Class              #29            //  java/lang/Throwable
  #17 = Utf8               syncTask
  #18 = Utf8               SourceFile
  #19 = Utf8               SyncBlockAndMethod.java
  #20 = NameAndType        #8:#9          //  "<init>":()V
  #21 = Class              #30            //  java/lang/System
  #22 = NameAndType        #31:#32        //  out:Ljava/io/PrintStream;
  #23 = Utf8               Hello
  #24 = Class              #33            //  java/io/PrintStream
  #25 = NameAndType        #34:#35        //  println:(Ljava/lang/String;)V
  #26 = Utf8               Hello again
  #27 = Utf8               thread/SyncBlockAndMethod
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/Throwable
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (Ljava/lang/String;)V
{
  public thread.SyncBlockAndMethod();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0

  public void syncsTask();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 9: 0
        line 10: 4
        line 11: 12
        line 12: 22
      StackMapTable: number_of_entries = 2
           frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class thread/SyncBlockAndMethod, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
           frame_type = 250 /* chop */
          offset_delta = 4


  public synchronized void syncTask();
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String Hello again
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 15: 0
        line 16: 8
}

  可以发现有monitorenter 和monitorexit 两条指令

另外一个方法,没有monitorenter 和monitorexit 两条指令。可以看到ACC_SYNCHRONIZED的访问标志,用来区分一个方法是否是同步方法。当发现有同步标志,执行线程将持有Monitor。

为什么会对synchronized嗤之以鼻?

早期版本中,synchronized属于重量级锁,依赖于Mutex Lock实现

线程之间的切换需要从用户态转换为核心态,开销较大

Java6以后,synchronized性能得到了很大的提升

自旋锁和自适应自旋锁

自旋锁:

  许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得

  通过让线程执行忙循环等待锁的释放,不让出CPU

  缺点: 若锁被其它线程长时间占用,会带来许多性能上的开销

自适应自旋锁

  自旋的次数不再固定

  由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。

synchronized的四种状态

无锁,偏向锁,轻量级锁, 重量级锁

锁膨胀方向: 无锁 -> 偏向锁  -> 轻量级锁 -> 重量级锁

偏向锁: 减少同一线程获取锁的代价

大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得

核心思想:

如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadId即可,这样就省去了大量有关锁申请的操作。

不适用于锁竞争比较激烈的多线程场合。

轻量级锁

  轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就好升级为轻量级锁。

  适用的场景: 线程交替执行同步块

  若存在同一时间访问同一锁的情况,就好导致轻量级锁膨胀为重量级锁。

偏向锁、轻量级锁、重量级锁的汇总

原文地址:https://www.cnblogs.com/linlf03/p/12115973.html