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()){ // even you tryLock is true when you lock can still block by the lock ,that is multiply -thread programming
try{
reentrantLock.lock(); //only one thread can lock the reentrantLock,the others will be blocked
......
}
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等方法可以参考代码。

   

    hasQueuedPredecessors 方法时判断当前线程是不是在排队的对头部。不在头部返回true。

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

  等待队列模型: 

释放锁:非共享锁释放队列头。

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

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


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


 

 







 

猜你喜欢

转载自www.cnblogs.com/thinkqin/p/11099384.html
今日推荐