• java多线程--锁学习


      (近期整理了下java多线程的知识,顺便写下来)

    一  synchronized 使用和原理

    使用:

    synchronized 是 java自带关键字,估计也是我们接触到java多线程时最早使用的锁机制,synchronized 使用java对象作为锁,线程执行到同步代码块时,尝试获取锁,一个线程获取到锁未释放的这段时间内,其他线程再尝试获取锁,则等待,从而实现多线程代码安全执行。

       1 普通的同步方法: 锁是当前对象实例,如果new了多个实例,他们之间不互相影响,同一个对象实例的同步方法需要竞争同一个锁

           例如 public class TestA{

        public synchronized void method1(){

        System.out.println("method1");

        }

        public synchronized void method2(){

        System.out.println("method2");

        }

      }

    2 静态同步方法:使用类的Class对象(java.lang.Class )作为锁对象,类Class对象和类的实例对象之间互不影响。

           例如 public class TestB{

        public static synchronized void method1(){

        System.out.println("method1");

        }

        public static synchronized void method2(){

        System.out.println("method2");

        }

      }

    3 同步方法块 : 使用synchronized 括号后面的对象作为锁

           例如 public class TestC{

        private Object lock

        public void method1(){

        synchronized(lock){

          System.out.println("method1");

        }

        }

        public void method2(){

        synchronized(lock){

          System.out.println("method2");

        }

        }

      }

             了解了使用后,进一步考虑类或者对象实例只是内存中的一块数据,是如何提供锁的功能的呢?

     synchronized原理:

          (参考oracle的 java语法规范  https://docs.oracle.com/javase/specs/jls/se12/html/jls-17.html#jls-17.1  和

                 jvm 说明 https://docs.oracle.com/javase/specs/jvms/se12/html/jvms-2.html#jvms-2.11.10

                      

                  

        大致翻译一下就是: 

           (1). 每一个java类都有一个关联的monitor,线程可以lock 和 unlock这个monitor,一旦一个线程lock了这个monitor,其他线程只有等待他unlock后才能unlock,对象的monitor是可重入,同一线程重入计数加一,对象的wait方法也是在wait monitor 。

           (2).普通同步方法在执行前会获取当前对象(this)的monitor锁,静态方法获取Class实例的monitor锁,同步方法块执行前会获取括号里的对象的monitor锁。

           (3) jvm具体执行过程是这样的 对于同步方法,编译后的constantpool会给这个方法打一个标志(ACC_SYNCHRONIZED),执行时会被识别出来,执行方法前先enters a monitor 执行方法,然后方法正常执行或者异常执行结束后exits the monitor,同步方法块 时编译时 会在 方法块前后加上 monitorenter  和 monitorexit的指令

      继续了解monitor的实现原理需要 了解java对象模型和java的对象头数据结构,这部分比较复杂,可以参考HotSpot的教程和源码。(https://github.com/openjdk-mirror/jdk7u-hotspot

         HotSpot是基于c++实现,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

         

         再来一个源码(oop.hpp)截图: 

          

     _mark是图里的markword metadata里的_klass 是图里指向方法区instanceKlass 的元数据指针,_mark 字段则包含了需要java类的状态信息,对象的锁状态也在其中。

        以32位操作系统为例子:

        

    monitor机制:

      

    ObjectMonitor(objectMonitor.cpp)的源码里的尝试获取锁解析。

    简单说明: 尝试获取monitor 方法,THREAD 是请求获取monitor的线程,  _owner 是记录的获取到当前monitor的线程。

           (1)两者不相等时再判断是否线程可以拥有_own的锁(等线程部分详解),不拥有再基于CAS尝试获取,都失败返回false。

           (2)两者相等计数加一,返回true

    这只是monitor其中一个方法,其他的enter,exit等方法可以参考源码,monitor的方法和java对象的头部信息完成了synchronized的锁机制。

    java synchronized 锁说明:

    (1)偏向锁: 锁标志01 和无锁标志一样,java对象头里还记录了线程ID,线程获取锁时判断线程ID为空或者等于自己,则成功。其他线程同时获取则失败,锁变成轻量级锁。

    (2)轻量级锁: 锁标志00 ,获取不到锁的线程会自旋(循环获取),再次获取失败,锁膨胀为重量级锁

    (3)重量级锁:锁标志10, 获取不到锁的线程block。

    到这里synchronized 关键字 锁的使用和原理基本结束,但有一个最最基本的问题没有解释,线程怎么通过CAS就能安全地获取到锁了,CAS可以说是锁的最基本的原子操作,等看完java并发包的锁再一起讲

     

            

    二 java并发包里的锁

     使用:

     先看一下lock接口的方法:

    void lock();                                                    获取锁,获取不到则线程block
    void lockInterruptibly() throws InterruptedException; 获取锁,获取不到,如果线程的中断标识为true,则抛出异常,否则线程block,
    boolean tryLock(); 尝试获取锁,成功返回true,不成功返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;尝试获取锁,成功返回true,不成功如果中断标识为true,抛出异常,否者返回false
    void unlock(); 释放锁
    Condition newCondition(); 返回一个Condition 对象
    并发包提供的对象有:ReentrantLock-可重入锁,ReentrantReadWriteLock-可重入读写锁,StampedLock-不可以冲入,适用于内部
    ReentrantLock reentrantLock = new ReentrantLock();
    if(reentrantlock.tryLock()){
    try{
    //reentrantLock.lock(); 这里原来理解错误,trylock实际上已经cas 和 设置了线程, 不需要再lock,
    ......
    }
    finally{
    reentrantLock.unlock(); //in case of code crush ,unlock the reentrantlock in the finally statement block
    }
    }else
    {
    ....
    }

      ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); 

       Lock writeLock = reentrantReadWriteLock.writeLock();

      Lock readLock = reentrantReadWriteLock.readLock();

       writeLock是独占锁,一个线程占用了,其他线程都需要排队

       readLock是共享锁,readLock可以多个线程公用,但是跟writeLock 互斥

    原理:

    打开java并发包的源码,ReentrantLock的源码,ReentrantLock的Lock等方法基本就是Sync 属性的方法.ReentrantLock 有 lock lockInterruptibly tryLock 带时间的trylock unlock等方法。Sync有公平锁和非公平锁,默认非公平锁


    以调用ReentrantLock lock方法为例子 内部调用FairSync或者UnFairSync的lock方法,内部调用AbstractQueuedSynchronizer的acquire的方法,父类方法调用抽象方法tryAcquire(抽象方法在子类实现,FairSync实现时考虑了排队)
    方法成功后 再acquireQueued,正真排队获取锁。其他方式的lock和trylock等方法可以参考代码。
    简单的流程就是
    Lock()调用tyrAcquire() -》 cas成功 -》doAcquire-》线程执行 -》unLock()释放锁(唤醒等待队列第一个进程去竞争锁)
    .....................................................cas失败 -》block....-》......被上一行的线程唤醒,在循环里继续竞争锁,成功就走上一行逻辑,失败继续block
    其中还有线程的 interupt标志,共享,是否公平锁等概念。

    下面时部分代码片段
    公平锁的tryAcqiure 里 hasQueuedPredecessors 方法时判断当前线程是不是在排队的对头部。不在头部返回true。这样可以让最新进入队列的成功,保证公平

       

    再来看看AbstractQueuedSynchronizer 的队列和获取锁的方法,判断如果是在head后的节点,再次tryacquire,成功则返回中断标志位, 如果需要中断则调用unsafe类中断线程。

    unlock方法:

    一般就是cas state为空闲,激活排队里的线程。

      

    等待队列模型: 

     ReentrantReadWriteLock读写锁原理:

      trylock时,内部的state int字段不是简单的cas(0,1),而是字段以二进制的方式,某位16为作为非共享的控制,其他字段作为共享的控制。

         

    tryReadLock时判断共享位控制字段非零,锁非自己占用,返回失败(-1)否者共享位加 1

    tryWriteLock时  state(含共享和非共享,整个int字段)非零  ,如果非共享为零,非自己线程占用锁,返回失败

                             state 为零时 cas(0,1) .....

    进等待队列时共享的线程的node节点会 nextWaiter 指向一个shared 标志 ,非共享指向Execlusive标志

    共享锁获得锁后会判断head后的Node如果也是share,会唤醒这个线程,被唤醒的线程已经在队列里只有pred时head才继续竞争共享锁。

    共享锁释放时会先判断占用线程数(应该共享所以可以多个线程获得锁,为0 了才正在释放)。

    并发包的锁机制逻辑非常绕,并发的设计也非常巧妙,可以看下源码。

    CAS

    这里用到了Unsafe类提供的基本的CAS操作和线程的中断方法,UnSafe类还提供了线程安全的基本类。这里先看看CAS操作的原理

    unsafe的CAS方法一个例子:

    打开hotspot的Unsafe类(unsafe.cpp)

     

    步骤基本意思是oopDesc (oopDesc::atomic_compare_exchange_oop)方法判断孤弱入参对象指定位置的值等于期望的值,交换为给定的新值,并设置barrier,内存屏障是多线程同时运行时取到某个值时 因为缓存锁必须到主存里取最新的值,volatile关键字就是类似方式实现

    继续看原子交换方法(opp.inline.hpp):其他是一些判断主要是 Atomic::cmpxchg 和  Atomic::cmpxchg_ptr 方法

    atomic的方法跟计算机的操作系统有关系,选一个atomic_linux_x86.inline.hpp文件:

    由内嵌的汇编代码的汇编指令cmpxchgl 完成值的比较和交换,原子性得以保证。

    volatile和CPU原子操作

    volatile关键字的作用是让每个线程的缓存里的数据跟主存里保持一致。
    问题引入: 多个线程多个cpu执行时,每个cpu都有自己的缓存(电脑的L1,L2,寄存器都是缓存),修改和读取数据都是先从缓存修改读取,再同步到内存,这样就可能出现其他线程读取不到最新数据的问题
    解决方法: 设置成volatile的属性字段,多个缓存会读取时都回读取到最新的数据。
    实现原理:
    volatile的属性字段读写操作前面和后面加一个 内存屏障,强制读和写到主存


    Cpu数据原子操作实现:
    基于总线或者缓存锁 #lock,总线锁 锁住所有的cpu,性能较差。 缓存锁锁住缓存并基于缓存一致性,其他cpu写的缓存数据无效,实现数据原子操作。


     

     







     
  • 相关阅读:
    JAVA的反射理解
    网络编程-小结
    JAVA多线程的总结
    Mysql基本语句的总结
    IO流
    JAVA集合介绍
    时间复杂度
    JAVA面向对象-多态的理解
    求A的B次方
    最大公约数
  • 原文地址:https://www.cnblogs.com/thinkqin/p/11099384.html
Copyright © 2020-2023  润新知