前言
市面上大多数的培训机构都希望又快又好的培训完学员进入EE企业级开发阶段 ,从而在研发课程时候就忽视了基础部分 以至于一些基础知识会被忽视而为未来的道路埋坑
并发编程
窜行与并行的定义
减少上下文切换问题能够加快线程效率
死锁问题
线程基础
进程与线程的区别
进程:是系统进行分配和管理资源的基本单位
线程:进程的一个执行单位,是进程内调度的实体,是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单位。线程也被称为轻量级进程,线程是程序执行的最小单位
线程的挂起与恢复
什么是挂起线程?
-
线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。
-
在线程挂起后,可以通过重新唤醒线程来使之恢复运行
为什么挂起线程
- cpu的分配时间片非常短,同时也非常珍贵,避免资源的浪费
守护线程
线程安全问题
什么是线程安全?
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行s并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。----《并发编程实战》
javac xxx
java -encoding UTF-8 xxx.class //指定字符集编译
javap -c xxx.class //反编译class文件
什么是线程不安全?
多线程并发访问,得不到正确的结果
从字节码角度剖析线程不安全操作
一段 return num++的代码
0: getstatic 获取指定类的静态域,并将其压入栈顶
3: iconst_1 将int型1压入栈顶
4: iadd 将栈顶两个int
5: putstatic 为指定类静态域赋值
8: return
例子中,产生线程不安全问题的原因∶
num++不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快递切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性。
原子性操作
什么是原子性操作
某个操作要么同时成功 要么同时失败
怎么把非原子性操作转为原子性操作
volatile关键字只是保证可见性,并不保证原子性
synchronize关键字,使得操作具有原子性 ,但是有缺陷
深入理解synchronized
首先需要了解 内置锁 与 互斥锁
内置锁
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁
内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
synchronized修饰普通方法:是锁住这个类的实例
synchronized修饰静态方法:锁住整个类
synchronized修饰代码 :锁住一个对象synchronized(lock)即synchronized后面括号里的内容
volatile关键字及其使用场景
能且仅能修饰变量
保证变量的可见性,不保证原子性
禁止指令重排序
场景:
A,B两个线程同时读取volatile关键字修饰的对象 A读取后,修改了变量的值 ,修改后 对B线程是可见的
使用场景
- 作为线程开关
- 单例,修饰对象的实例,禁止指令重排
单例与线程安全
-
饿汉式—本身线程比较安全
- 在类加载时候,就已经实例化好了类对象,无论以后用不用得到,如果这个类比较占内存,之后也没用到就浪费了内存
package cn.alexalbb.singleto; /** * @Author Alex * @Date 2021-04-27 21:17 */ public class HungerSingleton { private static volatile HungerSingleton hungerSingleton = new HungerSingleton(); private HungerSingleton() { } public static HungerSingleton getInstance() { return hungerSingleton; } }
-
懒汉式
- 在需要时候加载 按需加载
- 但是会出现一个线程不安全的情况,使用懒汉式的单例实例化对象时候是需要一个double lock,实例对象需要使用volatile关键字修饰 防止指令重排
package cn.alexalbb.singleto; /** * @Author Alex * @Date 2021-04-27 21:21 */ public class LazySingleton { /** * 本实例 */ private static volatile LazySingleton instance; /** * 私有的构造方法 */ private LazySingleton() { } /** * 单例的实例 * * @return this */ public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { //双重判空解决线程安全问题 if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
如何避免线程安全问题
成因
- 多线程环境—改变为单线程(必要代码加锁)
- 多线程操作共享资源–不共享资源(ThreadLocal,不共享,操作无状态化,不可变)
- 对共享资源进行了非原子性操作—将非原子性操作改成原子性操作
锁
锁的分类
自旋锁
线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起
阻塞锁
阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态
重入锁
支持线程再次进入的锁,就跟我们有房间钥匙,可以多次进入房间类似
读写锁
两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享
互斥锁
上厕所,进内之后就把内关了,不让其他人进来
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻寒直到它拿到锁
乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
公平锁
大家都老老实实排队,对大家而言都很公平
非公平锁
一部分人排着队.但是新来的可能插队
偏向锁
偏向锁使用了一种等到竞争出现扌释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程扌会释放锁
独占锁
独占锁模式下,每次只能有一个线程能持有锁
共享锁
允许多个线程同时获取锁,并发访问共享资源
Lock与synchronized区别
- lock上锁需要手动代码控制 ,而synchronized是自动控制的
- synchronized是托管给jvm的
- synchronized是基于cpu的悲观锁,而lock是乐观锁CAS
ReentrantLock非公平锁实现
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//首先尝试CAS将Sync的状态值更改为1 如果更改成功就将当前线程插队到获取锁线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//更改失败尝试获取锁 调用
acquire(1);
//以下是父类中的获取锁方法
public abstract class AbstractQueuedSynchronizer{
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//非公平锁尝试获取锁 进行CAS算法 强制更改状态
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
ReentrantLock公平锁实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//获取锁时不进行CAS 而是等待线程不插队
acquire(1);
}
//在抽象类父类中定义了获取锁方法 acquire 中调用下面的 tryAcquire()方法 尝试获取锁
public abstract class AbstractQueuedSynchronizer{
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
}
//公平锁尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//重点公平锁与非公平锁的区别在于 公平锁在CAS之前 判断当前线程队列是否有其他线程在等待获取 hasQueuedPredecessors()方法
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//如果头结点不等于尾节点 并且 头结点的下一个节点的线程不等于当前线程 说明整条线程链表不等于空 是有其他的线程在等待获取锁资源
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁的弊端
- 可能导致其他的等待的线程等不到相应的一个CPU资源,从而引起线程饥饿
多线程调试技术Debug
指定debug的调试方式为线程模式
读写锁特效及ReentrantReadWriteLock的使用
- 特性 :写写互斥,读写互斥,读读共享
- 锁降级:写线程获取写入锁后面可以获取读取所,然后释放读取锁,这样写入锁就变成了读取锁,从而实现了锁的降级
源码探究AQS如何用单一的int值表示读写的两种状态
abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本序列号
private static final long serialVersionUID = 6317671515068378041L;
// 高16位为读锁,低16位为写锁
static final int SHARED_SHIFT = 16;
// 读锁单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读锁最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁最大数量
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 本地线程计数器
private transient ThreadLocalHoldCounter readHolds;
// 缓存的计数器
private transient HoldCounter cachedHoldCounter;
// 第一个读线程
private transient Thread firstReader = null;
// 第一个读线程的计数
private transient int firstReaderHoldCount;
}
说明:该属性中包括了读锁、写锁线程的最大量。本地线程计数器等。
同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。
假设当前同步状态值为S,get和set的操作如下:
(1)获取写状态:
S&0x0000FFFF:将高16位全部抹去
(2)获取读状态:
S>>>16:无符号补0,右移16位
(3)写状态加1:
S+1
(4)读状态加1:
S+(1<<16)即S + 0x00010000
在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。
深入剖析ReentrantReadWriteLock之读锁源码实现
读锁的获取,看下tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取状态
int c = getState();
//如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁数量
int r = sharedCount(c);
/*
* readerShouldBlock():读锁是否需要等待(公平锁原则)
* r < MAX_COUNT:持有线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
*/
// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) { // 读锁数量为0
// 设置第一个读线程
firstReader = current;
// 读线程占用的资源数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
// 占用资源数加1
firstReaderHoldCount++;
} else { // 读锁数量不为0并且不为当前线程
// 获取计数器
HoldCounter rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current))
// 获取当前线程对应的计数器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 计数为0
//加入到readHolds中
readHolds.set(rh);
//计数+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
其中sharedCount方法表示占有读锁的线程数量,源码如下:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
说明:直接将state右移16位,就可以得到读锁的线程数量,因为state的高16位表示读锁,对应的第十六位表示写锁数量。
读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。流程图如下。
注意:更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(23行至43行代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。
fullTryAcquireShared方法:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) { // 无限循环
// 获取状态
int c = getState();
if (exclusiveCount(c) != 0) { // 写线程数量不为0
if (getExclusiveOwnerThread() != current) // 不为当前线程
return -1;
} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
} else { // 当前线程不为第一个读线程
if (rh == null) { // 计数器不为空
//
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) // 读锁数量为最大值,抛出异常
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // 比较并且设置成功
if (sharedCount(c) == 0) { // 读线程数量为0
// 设置第一个读线程
firstReader = current;
//
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
说明:在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功)则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。其逻辑与tryAcquireShared逻辑类似,不再累赘。
读锁的释放,tryReleaseShared方法
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
firstReader = null;
else // 减少占用的资源
firstReaderHoldCount--;
} else { // 当前线程不为第一个读线程
// 获取缓存的计数器
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
// 获取当前线程对应的计数器
rh = readHolds.get();
// 获取计数
int count = rh.count;
if (count <= 1) { // 计数小于等于1
// 移除
readHolds.remove();
if (count <= 0) // 计数小于等于0,抛出异常
throw unmatchedUnlockException();
}
// 减少计数
--rh.count;
}
for (;;) { // 无限循环
// 获取状态
int c = getState();
// 获取状态
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) // 比较并进行设置
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
说明:此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。其流程图如下。
在读锁的获取、释放过程中,总是会有一个对象存在着,同时该对象在获取线程获取读锁是+1,释放读锁时-1,该对象就是HoldCounter。
要明白HoldCounter就要先明白读锁。前面提过读锁的内在实现机制就是共享锁,对于共享锁其实我们可以稍微的认为它不是一个锁的概念,它更加像一个计数器的概念。一次共享锁操作就相当于一次计数器的操作,获取共享锁计数器+1,释放共享锁计数器-1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。
先看读锁获取锁的部分:
if (r == 0) {//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//第一个读锁线程重入
firstReaderHoldCount++;
} else { //非firstReader计数
HoldCounter rh = cachedHoldCounter;//readHoldCounter缓存
//rh == null 或者 rh.tid != current.getId(),需要获取rh
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh); //加入到readHolds中
rh.count++; //计数+1
}
这里为什么要搞一个firstRead、firstReaderHoldCount呢?而不是直接使用else那段代码?这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。可能就看这个代码还不是很理解HoldCounter。我们先看firstReader、firstReaderHoldCount的定义:
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
这两个变量比较简单,一个表示线程,当然该线程是一个特殊的线程,一个是firstReader的重入计数。
HoldCounter的定义:
static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}
在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。
诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
ThreadLocalHoldCounter继承ThreadLocal,并且重写了initialValue方法。
故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
深入剖析ReentrantReadWriteLock之写锁源码实现
看下WriteLock类中的lock和unlock方法:
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。
写锁的获取,看下tryAcquire:
1 protected final boolean tryAcquire(int acquires) {
2 //当前线程
3 Thread current = Thread.currentThread();
4 //获取状态
5 int c = getState();
6 //写线程数量(即获取独占锁的重入数)
7 int w = exclusiveCount(c);
8
9 //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
10 if (c != 0) {
11 // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
12 // 如果写锁状态不为0且写锁没有被当前线程持有返回false
13 if (w == 0 || current != getExclusiveOwnerThread())
14 return false;
15
16 //判断同一线程获取写锁是否超过最大次数(65535),支持可重入
17 if (w + exclusiveCount(acquires) > MAX_COUNT)
18 throw new Error("Maximum lock count exceeded");
19 //更新状态
20 //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
21 setState(c + acquires);
22 return true;
23 }
24
25 //到这里说明此时c=0,读锁和写锁都没有被获取
26 //writerShouldBlock表示是否阻塞
27 if (writerShouldBlock() ||
28 !compareAndSetState(c, c + acquires))
29 return false;
30
31 //设置锁为当前线程所有
32 setExclusiveOwnerThread(current);
33 return true;
34 }
其中exclusiveCount方法表示占有写锁的线程数量,源码如下:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
说明:直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位表示。
从源代码可以看出,获取写锁的步骤如下:
(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
方法流程图如下:
写锁的释放,tryRelease方法:
1 protected final boolean tryRelease(int releases) {
2 //若锁的持有者不是当前线程,抛出异常
3 if (!isHeldExclusively())
4 throw new IllegalMonitorStateException();
5 //写锁的新线程数
6 int nextc = getState() - releases;
7 //如果独占模式重入数为0了,说明独占模式被释放
8 boolean free = exclusiveCount(nextc) == 0;
9 if (free)
10 //若写锁的新线程数为0,则将锁的持有者设置为null
11 setExclusiveOwnerThread(null);
12 //设置写锁的新线程数
13 //不管独占模式是否被释放,更新独占重入数
14 setState(nextc);
15 return free;
16 }
写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其方法流程图如下。
ReentrantReadWriteLock总结
通过上面的源码分析,我们可以发现一个现象:
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
综上:
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。
锁降级的应用场景
用于对数据比较敏感,需要对数据修改之后,获取到修改后的值,并进行接下来的其他操作
StampedLock原理以及使用
一般的业务场景都是读多写少 1.8之前的读写锁 因为读写互斥 读的同时不能写 这样就造成了性格能瓶颈 也会造成线程饥饿
StampedLock特点
所有获取锁的方法,都返回一个邮戳(Stamp) , Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
支持锁升级跟锁降级
可以乐观读也可以悲观读
使用有限次自旋,增加锁获得的几率,避免上下文切换带来的开销乐观读不阻塞写操作,悲观读,阻塞写的操作
StampedLock优点
相比较ReentrantReadWriteLock,吞吐量大幅提升
StampedLock缺点
api复杂,容易使用错误
内部实现相比较ReentrantReadWriteLock复杂
StampedLock原理
每次获取锁的时候,都会返回一个邮戳(stamp),相当于mysql里的version字段释放锁的时候,再根据之前的获得的邮戳,去进行锁释放
StampedLock使用注意点
- 使用乐观读API 一定要要对邮戳进行验证 验证不通过就去转为排它锁 读锁
线程之间的通信
wait,notify,notifyAll
wait 线程等待 notify 线程唤醒 notifyAll 唤醒所有线程
何时使用
在多线程环境下,有时候一个线程的执行,依赖于另外一个线程的某种状态的改变,这个时候,我们就可以使用wait与notify或者notifyAll
wait和sleep区别
wait会释放持有的锁,而sleep不会,sleep只是让线程在指定的时间内,不去抢占cpu的资源
注意点
wait notify必须放在同步代码块中, 且必须拥有当前对象的锁,即不能取得A对象的锁,而调用B对象的wait 哪个对象wait,就得调哪个对象的notify
notify和notifyAll区别
nofity随机唤醒一个等待的线程 notifyAll唤醒所有在该对象上等待的线程
ThreadLocal的使用
线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。为每个线程单独存放一份变量副本,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
只要线程处于活动状态并且ThreadLocal实例可访问,那么每个线程都拥有对其本地线程副本的隐式引用变量一个线程消失后,它的所有副本线程局部实例受垃圾回收(除非其他存在对这些副本的引用)
—般用的比较多的是
- ThreadLocal.get:获取ThreadLocal中当前线程共享变量的值。
- ThreadLocal.set:设置ThreadLocal中当前线程共享变量的值。
- ThreadLocal.remove:移除ThreadLocal中当前线程共享变量的值。
- ThreadLocal.initialvalue:ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
Condition的使用
可以在一个锁里面,存在多种等待条件
主要方法
await
signal
signalAll