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写的缓存数据无效,实现数据原子操作。