为什么需要AQS
理解相似点:比如说,锁只能允许一个线程,而Semaphore,只允许一定量的线程。比如说:他们还可以去看一看目前是不是陷入等待,tryLock() tryQuire()方法,还有在一定时间内尝试获取。
代码演示:
可以看到他有一个Sync内部类。进入这个内部类,可以看到它继承了AQS(这是类的缩写)
结论:在Semaphore中,有一个Sync内部类,这个内部类是继承了AQS
其实CountDownLatch也是这样的;
其实,在ReentrantLock()中,其实也是这样的套路
所以说,AQS是如何使用呢,就是把这个类放到了要实现类的内部,作为内部实现类。
AQS的功能:1.同步状态的原子性处理,线程的阻塞与解除阻塞,队列的管理。
AQS是一个用于构建锁类时的工具类,它解决了大量细节问题,比如说:等待线程用先进先出的操作,以及用一些标准来判断这个线程是应该等待还是不应该等待,也会帮我们处理,在多个地方的竞争问题,提高吞吐量,提高性能。总之,有了AQS,构建线程协作类就容易多了。
我们先来看一下这个类:
这个类是抽象类,点一下左边这个图标就可以看到他有很多实现。
其中FairSync :代表的是公平实现; NonfairSync:代表的是非公平实现。
比如说,compareAndSetState():
这里是用了unsafe里面的cas操作操作,通过底层指令的原子性,来保证了这个操作的原子性。
在ReentrantLock中,state代表锁的占有情况,如果为0,代表他还没被占有,如果为1,说明该锁被占有了;如果为2说明这个锁,在该线程里被占有了多次。
对于,这部分内容,AQS就可以被当做是排队管理器,当多个线程争取同一把锁时,将那些没能拿到锁的线程串在一起。
head是目前拿到锁的线程,而后面这些就被阻塞了。
比如说ReentrantLock 获取到state变量大于0的时候,就会导致阻塞;
Semaphore中,acquire获取到state大于0时,就可以执行;
CountDownLatch里面,如果await获取到数字大于0 ,就会阻塞;
Semaphore中,释放方法,就是让state加一 ,在CountDownLatch中,countDown方法就是让数减一,不同的实现类方法不同,需要自己去实现。
在讲完线程获取方法和释放方法以后,还需要线程实现类去自己实现一些方法。
比如说:
在CountDownLatch中,就有tryAcquireShared 和 tryReleaseShared方法。
在CountDownLatch中,对AQS进行分析:
构造器分析:
我们创建CountDownLatch时,会传入一个值,就是CountDownLatch要倒数的数量,也是Sync中的state值;
这个就是返回state;
进入acquireSharedInterruptibly方法:
然后在,CountDownLatch中查看这个方法(如果直接点进去是不行的,要在他本类上进行查看)
可以看到,如果等于0就为1;如果大于0。则为-1;如果大于0,返回-1。然后就执行下面语句,
就会入队进行等待,就是将线程阻塞起来。
下面查看countDown()方法
然后点击tryReleaseShared
发现里面没有什么内容,这个时候就说明,它的上一个类中,对它会有实现;
发现在Sync中果然有tryReleaseShared的实现;所以,应该查看这个方法;
会用一个for循环来做cas自旋;
如果等于0说明被人释放过了,就不需要释放了,就返回false;
如果不等于0 就先计算c-1之后的结果;
再用compareAndSetState 即cas的方法把这个值给更新回去;如果发现没被别的线程更新过,那么就直接更新
如果发现被别人更新过了,它就会继续for循环,这时候,它获取到的c会变得不一样,然后nextc也会不一样;
如果nextc == 0 说明这个闸门就打开了 就返回true;如果返回true,就会把这群等待的线程进行唤醒
await()方法介绍:(如果state等于0就放行,如果不等于0就堵塞)
如果state不为0,则返回结果为-1;
就调用doAcquireSharedInterruptibly()方法;
这个方法就是堵塞线程的作用,在parkAndCheckInterrupt()中,是堵塞线程的操作,到最后面就是native方法 native方法,native方法就是告知JVM调用,该方法在外部定义,我们将使用C语言或者C++语言来实现;
AQS在Semaphore的应用
state:代表许可证还剩几个
进入Semaphore查看获取许可证
再进入,发现还是会调用tryAcquireShared方法。这个方法在Semaphore中进行了重写;
如果这个方法返回值小于0,那么他就会让线程进入到等待队列;这个是公平的情况;
先看一下有没有等待队列,如果有等待队列的话,说明肯定轮不到我了,因为这个是公平的情况;所以,直接进入等待。如果没有等待队列,就先获取剩余许可证的数量,然后求出剩余许可证减去需要许可证的值,如果这个值小于零,返回这个值,进入等待,如果这个值不小于0就进行CAS操作,假设它操作成功,然后返回这个值,然后进行获取。获取的操作在这个方法之上。不写在这个方法里;(CAS也是有可能失败了,但是它会重新进入for循环)
AQS在ReentrantLock的应用
最重要的方法就是加锁和解锁;
首先先看一下解锁方法;
在release中,它调用了tryRealse()方法;
在这个方法里面,它会先去看一下这个线程是不是持有锁的线程,只有持有锁的线程才可以进行解锁,如果不是,就不可以进行解锁,抛出一个异常;
如果是持有锁的线程,它会把获取它持有锁的次数,然后要减去释放的次数;如果持有锁的次数减去释放的次数不等于零,那么就setState©;如果等于零,就对其进行释放;free代表当前线程是否有锁,setExclusiveOwnerThread(null)持有这把锁的持有线程设置为null;
如果返回为true,就说明已经释放了;就执行下面的语句;
唤醒下面在等待的锁;unparkSuccessor uppark代表的是唤醒;
下面我们来看一下lock()
发现这个锁是抽象方法,因为根据我们公平与不公平,会有两种锁的策略;
点击ctrl + alt +鼠标左键,我们以非公平为例来进行查看;
它首先会进行cas操作;即当前没有任何人持有的时候,拿到这把锁;如果cas不成功,这把锁被别的线程拿走了;
我们就在该类的非公平下查看一下
首先,会获取state()如果等于0说明没人锁,就通过cas来获取锁,通常情况是不等于0的,这时候,它会进行判断,判断,当前获得锁的线程是不是和我自己的线程是同一个锁,如果是同一个锁的话,先判断是不是溢出 overflow ;如果不是溢出的话,就给当前锁的数量进行加一,多重入了一次;在返回true;如果不是当前线程,那么就获取锁失败;这时候就返回执行以下语句,将线程放入到等待队列中;
代码如下:
package aqs;
import java.util.concurrent.locks.AbstractQueuedLongSynchronizer;
/**
* 描述: 自己用AQS实现一个简单的线程协作器
* 起初门栓是关闭的,很多线程想要调用await()方法,会失败
* 然后有线程会调用signal()方法,这时候,之前等待的线程就会释放;
*/
public class OneShotLatch {
private final Sync sync = new Sync();
// 释放的方法
public void signal(){
sync.releaseShared(0);
}
// 尝试获取锁
public void await(){
sync.acquireShared(0);
}
private class Sync extends AbstractQueuedLongSynchronizer{
// 这个方法是可以自己实现的,如果等于1,说明门就打开了
@Override
protected long tryAcquireShared(long arg) {
return getState() == 1 ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(long arg) {
setState(1);
return true;
}
}
public static void main(String[] args) {
OneShotLatch oneShotLatch = new OneShotLatch();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "尝试获取latch,获取失败就等待");
oneShotLatch.await();
System.out.println(Thread.currentThread() + "开闸,继续运行");
}
}).start();
}
try {
Thread.sleep(5000);
oneShotLatch.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "尝试获取latch,获取失败就等待");
oneShotLatch.await();
System.out.println(Thread.currentThread() + "开闸,继续运行");
}
}).start();
}
}
先分析一下该类:
进入执行的方法acquireShared(因为是共享锁,所以调用acqureShared,如果是独享锁,例ReentrantLock就是调用acquire的);发现执行tryAcquireShared方法,如果返回值小于0就入队;
然后我们对其重写,当状态为1的时候,执行,否则就入队;
然后,现在进行分析释放的方法;
释放的方法,反正就是调用Sync的方法
这里面的tryReleaseShared是需要进行重写的;如果返回true Sync就会让等待的线程进行释放;
我这边重写是当调用的时候,设置为1,然后返回true;
这样,一个尝试获得锁,释放的方法就写好了;运行结论如下:
符合预期;
hashmap
// 默认标准为16
HashMap<String,String> hashMap = new HashMap<>();
底层源码:
1.7 : 数组 + 链表
1.8 : 数组 + 链表 + (红黑树)
为什么需要使用数组、链表、红黑树呢?
1.使用数组对于指定下标的查找,时间复杂度为O(1),一般加入数据,通过其hashCode(),通过特殊的计算(取模),来确定指定的下标;
2.链表:插入很快,但是,查找很慢;
3.红黑树:查找和删除平均复杂度都为O(logn);
创建hashmap时候,并没有创建出对象,只是初始化参数而已;
1.7 put():过程
第一步:先判断数组有没有初始化;如果,是空的,就进行初始化;
如何初始化数组?
默认的初始容量是16,如果,你传入的初始容量不是2的指数次幂;那么他就会转成2的指数次幂(大于当前你传入的值,然后,是最小的2的指数次幂);
为什么需要2的指数次方? 因为,下面通过hash来判断存储数组下标时,是使用tab[(length - 1) & hash])来判断的,如果,不是2的指数次方,就不能使用位运算来进行了;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
第二步:创建一个数组;执行put();
putVal(hash(key), key, value, false, true);
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.如果tab为null
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 使用 位运算会快很多
// 2.如果,当前加入的数组当前位置为null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3.如果,加入数组的位置不为null,但是key相同;那么就覆盖并返回oldValue
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.如果是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 5.如果是链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put() 到那个位置是不清楚的,它会通过hash来判断存储的位置,就通过hash值和15进行与操作,来判断他的存储位置(以15为例);如果,当前数组该位置为null;就新建Node()来进行加入;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果,在当前数组中位置,已经有数据了;那么,首先会进行判断,判断是不是key相同;如果key相同,会执行下面这个语句,返回oldValue;
// 如果hash相同,并且key相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 进行新节点的替换
afterNodeAccess(e);
return oldValue;
}
扩容: java7扩容时,会把原来数组中数据,重新进行计算hash,重新进行存储; 而java8,是不需要rehash的;
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// luguobao :resize() 链表部分
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 1.判断,hash和原来数组的长度进行&;
// 2.如果是0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
hashMap8 如何解决死锁?