java锁的原理


java锁的原理

  • synchronized锁的不是代码,锁的是对象(实例的对象)。

  • 线程安全的原因:

    • 存在共享数据(也称临界资源)
    • 存在多条线程共同操纵这些共享数据
  • 解决办法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。

  • 互斥锁的特性

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

    • 对象锁:
      • 同步代码块(synchronized(this)、synchronized(类实例对象))。锁是小括号中的实例对象。
      • 同步静态方法(synchronized method),锁是当前对象的实例对象。
    • 类锁:
      • 同步代码块synchronized(类.calss),锁是小括号中类对象。
      • 同步静态方法(synchronized static method),锁是当前对象的类对象(class对象)。
  • 对象锁和类锁的总结

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

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

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

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

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

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

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

  • synchronized底层原理:

    • 对象头结构:

      • mark word:默认存储对象的hashcode,分代年龄,锁类型,锁的标志位等信息。

        image-20200607200048051

      • class metadata address:类型指针指向对象的类元数据,jvm通过这个指针确认该对象是哪个类的实例。

    • Monitor:这是每个java对象天生自带了一把看不见的锁。

    • 锁相关知识:

      • 自旋锁:
        • 许多情况下,共享数据的锁定状态持续时间短,线程切换不值得。
        • 通过让线程执行忙循环等待锁释放,不让出cpu。
        • 缺点:若锁被其它线程长时间占用,会带来许多性能上的开销。
      • 自适应自旋锁:
        • 自旋次数不在固定。
        • 由前一次在同一个锁上的自旋时间及锁拥有者的状态来决定。
      • 偏向锁:减少同一线程获取锁的代价
        • 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。
        • 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做可同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
        • 适应场景:不适用于锁竞争比较激烈的多线程场合。
      • 轻量级锁:
        • 轻量级锁是由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
        • 适应场景:线程交替执行同步块
        • 若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
      • 锁消除
      • 锁粗化
    • synchronized 的四种状态(锁的升级):

      • 无锁,偏向锁,轻量级锁,重量级锁
      • image-20200607203625019
  • ReentrantLock(再入锁)

    • 位于java.util.concurrent.locks包。
    • 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现(AbstractQueuedSynchronizer简写。它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架)。
    • 能够实现比synchronized 更细粒度的控制。
    • 调用lock()后必须调用unlock()释放锁。
    • 性能未必比synchronized 高,并且是可重入的。
    • 公平性设置
      • ReentrantLock lock=new ReentrantLock(true);
      • 参数为true时,倾向于将锁赋予等待时间最久的线程。
      • 公平锁:获取锁的先后顺序按先后调用lock方法顺序(慎用)
      • 非公平锁:抢占全靠运气。synchronized 为非公平锁。
    • 与synchronized 的区别
      • 能够将锁对象化
        • 能判断是否有线程或者某个特定的线程在排队等待获取锁
        • 带超时的获取锁的尝试
        • 感知有没有获取锁。
      • 区别:
        • synchronized 是关键字,ReentrantLock是类。
        • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁。
        • ReentrantLock可以获取各种锁的信息。
        • ReentrantLock可以灵活地实现多路通知。
        • 机制:sync操作mark word,lock调用unsafe类的park()方法。
  • JMM内存模型

    • java内存模型(java memory model简称),本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象元素)的访问方式。

      image-20200608203050272

    • JMM中的主内存

      • 存储java实例对象。
      • 包括成员变量,类信息,常量,静态变量等。
      • 属于数据共享区域,多线程并发操作时会引发线程安全问题。
    • JMM中的工作内存

      • 存储当前方法的所有本地变量信息,本地变量对其他线程不可见。
      • 字节码行号指示器,native方法信息。
      • 属于线程私有数据区域,不存在线程安全。
    • JMM与Java内存区域划分区别

      • JMM描述的是一组规则,围绕原子性,有序性,可见性展开。
      • 相似点:存在共享区域和私有区域。
    • 主内存与工作内存的数据存储类型以及操作方式归纳

      • 方法里的基本数据类型本地变量将直接存储在工作内存的栈幁结构中。(整型:byte,short,int,long 浮点型:float,double 布尔:boolean 字符型:char)
      • 引用类型的本地变量:引用存储子啊工作内存中,实例存储在主内存中。
      • 成员变量,static变量,类型均会被存储在主内存中。
      • 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。
    • JMM怎么解决可见性问题:

      • 指令重排序的满足条件

        • 在单线程环境下不能改变程序运行结果。

        • 存在数据依赖关系的不允许重排序。

        • 无法通过happens-before原则推导出来的,才能进行指令的重排序。

          • happens-before关系:a操作结果需要对b操作可见,则a与b存在happens-before关系。是判断数据是否存在竞争,线程是否安全的主要依据,能解决并发冲突问题。

          • 八大原则:不满足以下原则则操作就没有顺序保证

            1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

            2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;

            3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

            4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

            5. 线程启动规则:Thread对象的start() 方法先行发生于此线程的每一个动作;

            6. 线程中断规则:对线程interrupt)方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

            7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行;

            8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize() 方法的开始;

      • volatite:JVM提供的轻量级同步机制

        • 特点:

          • 保证被volatite修饰的共享变量对所有线程写操作总是立即可见的。但是volatite运算操作在多线程环境中并不保证安全性
          public class volatiteVisibility{
              public static volatite int value=0;
              public static void increase(){
                  value++;
              }
          }
          value的任何改变都会立马反应到线程当中,单存在多条线程同时调用increase方法,就会出现线程安全问题。value++不具备原子性,先读取值旧值,再写回新值,在这两步期间可能多线程就会取到同一个值,并执行相同的+1操作。因此必须使用synchronized修饰来保证线程安全。但是就可以省略掉了volatite
          • 禁止指令的重排序。
        • volatite变量为何立即可见:

          • 当写一个volatite变量时,jmm会把该线程对应的工作内存中的共享变量值刷新到主内存中。
          • 当读一个volatite变量时,jmm会把该线程的工作内存置为无效。
        • 如何禁止重排优化

          • 内存屏障:(Memory Barrier)
            • 保证特定操作的执行顺序。
            • 保证某些变量的内存可见性。
          • 操作方式:
            • 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排优化。
            • 强制刷出各种cpu的缓存数据,因此任何cpu上的线程都能读取到这些数据的最新版。
        • 如何解决可见性:下面代码还要加volatite修饰才行

          image-20200608214347765

      • volatile和synchronized的区别:

        1. volatile本质是在告诉JVM当前变量在寄存器(工作内存) 中的值是不确定的, 需要从主存中读取; synchronized则是锁定当前变量, 只有当前
          线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止。
        2. volatile仅能使用在变量级别; synchronized则可以使用在变量、方法和类级别。
        3. volatile仅能实现变量的修改可见性, 不能保证原子性; 而synchronized则可以保证变量修改的可见性和原子性。
        4. volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞。
        5. volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化。

文章作者: zhaolin.long
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zhaolin.long !
 上一篇
JAVA多线程 JAVA多线程
JAVA多线程 进程与线程区别:进程是资源分配的最小单位,线程是cpu调度的最小单位。 进程:进程独占内存资源,保存各自运行状态,相互不干扰且可以相互切换,并为并发任务提供了可能。 线程(轻量级进程):共享进程的内存资源,相互切换更迅速,
2020-06-15
下一篇 
剑指Spring源码讲解 剑指Spring源码讲解
剑指Spring源码 spring架构图 Core Container:是spring构建的基础,所有模块都在该基础之上 Beans:所有应用都要用到,包含访问配置文件,创建和管理bean以及进行IOC和DI操作相关的类,其中BeanF
2020-06-15
  目录