多线程笔记--锁(synchronized)

  • synchronized

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

实现原理和做用

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

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

锁的三种应用方式

Java中每个对象均可以做为锁,这是synchronized实现同步的基础:github

  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种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率。bash

  • 对象锁(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原理

相关文章
相关标签/搜索