多线程笔记--锁(synchronized)

  • synchronized

在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。

实现原理和作用

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性,它可以:

  • 原子性:确保线程互斥的访问同步代码
  • 可见性:保证共享变量的修改能够及时可见
  • 有序性:有效解决重排序问题。即“一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

锁的三种应用方式

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
  2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
  3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 [图片上传失败...(image-bac42-1557385226672)] 如图,synchronized可以用在方法上也可以使用在代码块中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。
  • 对象头

在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。

每个对象分为三块区域:对象头、实例数据和对齐填充

  • 对象头包含两部分,第一部分是Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,这一部分占一个字节。第二部分是Klass Pointer(类型指针),是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,这部分也占一个字节。(如果对象是数组类型的,则需要3个字节来存储对象头,因为还需要一个字节存储数组的长度)
  • 实例数据存放的是类属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
  • 填充数据是因为虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标记位
无锁 对象的haahcode 分代年龄 0 01
轻量级锁 指向栈中锁记录的指针 合并第一列 合并第一列 00
重量级锁 指向互斥量(重量级锁)的指针 合并第一列 合并第一列 10
GC标志 合并第一列 合并第一列 11
偏向锁 线程ID(23bit)和Epoch(2bit) 对象分代年龄 1 01

如上表在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

  • 对象锁(monitor)机制,也叫监视器

从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。可以把它理解为 一个同步工具,也可以描述为 一种同步机制,实现了在一个时间点,最多只有一个线程在执行管程的某个子程序,这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。

我们可以把监视器理解为一个医院,医院里面只要一个医生,每次只能看一个病人(线程),如果一个病人想看病,他首先要在走廊里面排队(Entry Set),依次进入看病,但是假如某个正在看病的人可能晕血或者血糖低不能暂时继续看病(线程被挂起),这时候不能强行给他看,也不能让后面的病人等他一个,于是就要送他到休息室去休息(Wait Set),休息室里面呆的都是因为各种原因不能继续看病的病人,等休息好了,还可以继续去看病。如下图

灵魂画作
总之,监视器是一个用来监视这些线程进入特殊的房间的。他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。

Monitor的实现原理

在Java虚拟机(HotSpot)中,Monitor是基于C++实现的ObjectMonitor,其主要数据结构如下


 ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //用来记录该线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0; //锁的重入次数
    _object       = NULL;
    _owner        = NULL;  //指向当前持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  //存放wait状态的线程队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //这是一个和_WaitSet类似存等待线程的地方,
                           //但是是否存在这里是要根据Policy的值(这里不知道说的对不对,顺便说下,这玩意儿每次看都以为是cxk)      
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放处于等待锁的线程队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
复制代码

当多个线程同时访问一段同步代码时,首先进入 _EntryList,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时_count加一,获得对象锁。 如果持有monitor的线程被挂起(例如调用wait方法),将释放当前持有的monitor,_owner变量回复为null,_count减一,同时该线程进入_WaitSet队列中等待被唤醒(notify),如果当前线程顺利执行完代码块后会释放monitor并复位变量的值,以便下一个线程进来获取monitor锁,下面看个例子。

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
            System.out.printf("synchronized");
        }
        function();
    }

    private static void function() {
        System.out.printf("function");
    }
}
上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。下面是字节码文件
public class com.example.javalib.SynchronizedDemo {
  public com.example.javalib.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/example/javalib/SynchronizedDemo
       2: dup
       3: astore_1
       4: monitorenter
       5: aload_1
       6: monitorexit
       7: goto          15
      10: astore_2
      11: aload_1
      12: monitorexit
      13: aload_2
      14: athrow
      15: invokestatic  #3                  // Method function:()V
      18: return
复制代码

上面的4,6,12行就是需要注意的部分了,这是添加Synchronized关键字之后才会出现的。执行同步代码块首先要执行monitorenter,退出的时候执行monitorexit指令。 使用Synchronized之所以能够进行同步,其关键就是对对象的监视器monitor的获取,当执行线程获取到monitor后才能继续执行下去,否则只能继续等待。 上面的demo中同步代码块后还有一个静态方法,这个方法是同步的,而且该方法锁的对象依然是这个类对象,那么执行线程就不必再去获取这个锁,从字节码中可以看到,有一条monitorenter指令和两条monitorexit指令,并没有第二次获取锁的指令,这就是锁的重入性:即在同一个锁程中,线程不需要去再次获取同一把锁,Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态,关于线程的状态可以看这篇文章

从上面我们知道了sychronized加锁的时候,会调用objectMonitorenter方法,解锁的时候会调用exit方法。事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitorenterexit,这种锁被称之为重量级锁。为什么说这种方式操作锁很重呢? 因为Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。 所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有 只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。

感谢参考文章

以上文章是解决一个同步问题时发现synchronized知识点一知半解后查找资料后摘抄的笔记,算是自己个人的整理,漏了什么欢迎指出来。

彻底理解synchronized 深入多线程系列 深入分析Synchronized原理

猜你喜欢

转载自juejin.im/post/5cd3d038f265da03867e6ab9