zoukankan      html  css  js  c++  java
  • Java|Synchronized的基本知识、实现原理以及其与ReentrantLock的区别

    synchronized知识

      在谈论synchronized之前,我们需要了解线程安全问题的主要诱因。线程安全问题的主要诱因如下:

    • 存在共享数据(也称为临界资源)
    • 存在多条线程共同操作这些共享数据

      而解决线程安全的根本方法就是:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。

      基于上述,引入了互斥锁,其具有两个特性:

    1. 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称作操作的原子性。
    2. 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

      需要注意的是,synchronized锁的不是代码,而是对象。

      根据获取锁的分类,可分为获取对象锁、获取类锁。

    获取对象锁的两种方法:

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

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

    获取类锁的两种方法:

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

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

    接下来,对对象锁和类锁进行总结对比:

    1、有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;

    2、若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;

    3、若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象的同步方法的线程会被阻塞;

    4、若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步方法的线程会被阻塞,反之亦然;

    5、同一类的不同对象的对象锁互不干扰;

    6、类锁由于也是一种特殊的对象锁,因此表现和上述的1、2、3、4一致,而由于一个类只有一把对象锁,所以同一类的不同对象使用类锁将会是同步的;

    7、类锁和对象锁互不干扰。

    synchronized底层实现原理

      实现synchronized的基础:Java对象头+Monitor

      对象头的结构如下:

    虚拟机位数 头对象结构 说明
    32/64bit Mark Word 默认存储对象的hashcode,分代年龄,锁类型,锁标志位等信息。
    32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据。

      下面是32bit的Mark Word的说明图:

      

      而对于Monitor,每个Java对象天生自带一把看不见的锁(内部锁/Monitor锁)。

      Monitor锁的竞争与释放如下图所示:

      

      在早期的Java版本中,synchronized属于重量级锁,依赖于Mutex Lock实现。线程之间的切换需要从用户态转换到核心态,开销很大。

      在Java 6之后,synchronized性能得到很大提升。主要是因为引入了:

      1:Adaptive spinning(自适应自旋)

      2:Lock Eliminate(锁消除)

      3:Lock Coarsening(锁粗化)

      4:Lightweight Locking(轻量级锁)

      5:Biased Locking(偏向锁)

      6:......

      下面对这些概念进行介绍。

      自旋锁与自适应自旋锁:

      自旋锁:在很多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。就通过让线程执行忙循环等待锁的释放,而不让出CPU。缺点是若锁被其他线程长时间占用,会带来许多性能上的开销。用户使用preBlockspin来修改等待时间、次数,不好设定。

      这样的话,自适应自旋锁就出现了,它自旋的次数不固定,由前一次在同一个锁上的自旋时间及锁的持有者的状态来决定。

      锁消除:

      JIT编译时,对运行的上下文进行扫描,去除不可能存在竞争的锁,节省毫无意义的请求锁的时间。

      锁粗化:

      通过扩大加锁的范围,避免反复加锁和解锁。

      偏向锁:

      减少同一线程获取锁的代价。

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

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

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

      轻量级锁:

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

      使用场景:线程交替执行同步块。

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

    synchronized和ReentrantLock的区别

      ReentrantLock称为重入锁,位于JUC包的locks,和CountDownLatch、FutureTask一样基于AQS实现。能够实现比synchronized更细粒度的控制,比如控制公平性。此外需要注意,调用lock()之后,必须调用unlock()释放锁。它的性能未必比synchronized高,并且是可重入的。

      ReentrantLock公平性的设置:

      ReentrantLock fairLock = new ReentrantLock(true);

      参数为true时,倾向于将锁赋予等待时间最久的线程;

      公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用);

      非公平锁:抢占的顺序不一定,看运气;

      synchronized是非公平锁。

      ReentrantLock能够将锁对象化:

      判断是否有线程,或者某个特定线程,在排队等候获取锁;

      带超时的获取锁的尝试;

      感知有没有成功获取锁。

      ReentrantLock能够将wait/notify/notifyAll对象化。

      总结:

      1、synchronized是关键字,ReentrantLock是类;

      2、ReentrantLock可以获取锁的时间进行设置,避免死锁;

      3、ReentrantLock可以获取各种锁的信息;

      4、ReentrantLock可以灵活实现多路通知;

      5、机制:sync操作Mark Word,lock调用Unsafe类的park()方法。

  • 相关阅读:
    黑马程序员——正则表达式
    黑马程序员——集合框架知识点总结
    黑马程序员——String类知识点详细
    黑马程序员——System、Runtime、Date、Calender、Math静态类
    黑马程序员——IO流总结
    黑马程序员——多线程中的安全问题 :
    获取一段字符串中含有某一子字符串的个数的方法定义:
    debian彻底删除apache2
    linux下mysql的安装
    markdown学习
  • 原文地址:https://www.cnblogs.com/jlutiger/p/10548291.html
Copyright © 2011-2022 走看看