Java 并发编程(二)

AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包

将暂时获取不到锁的线程加入到队列中。

 

**注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功

cas: compareAndSetXXXX(Object o1, Object o2); 查看值是否等于o1, 如果是则改为o2

 

AQS 定义了两种资源共享方式:

1.Exclusive:独占,只有一个线程能执行,如ReentrantLock

2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier

 

ReentrantLock为例,(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。

注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。

 

以CountDownLatch为例,任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。

 

AQS的简单应用

Mutex:不可重入互斥锁,锁资源(state)只有两种状态:0:未被锁定;1:锁定。

 

 

 

lock AQS 实现的技术

自旋

park--unpark

cas

 

 

ReentrantLock:

单个线程/线程交替执行时,与队列无关,jdk级别解决同步问题

发生竞争时:

判断锁是否为自由状态;

自由状态:(公平锁)判断是否需要排队

已被持有:入队

 

入队:

实例化一个node node中有记录的当前线程,上一个节点,下一个节点

根据线程实例化一个node

判断tail是否为null,如果为null,再实例化一个thread为null的node节点,初始化队列作为队列首尾

第二个node入队

维护链表,告诉自己上一个节点和下一个节点

查看自己是否是第一个等待的,自旋一次,查看是否可以拿到锁(因为线程并发执行,此时可能上一个拿到锁的线程已经释放),如果拿不到或者不是第一个等待的,判断上一个节点的waitStatus的值是否是-1(此时初始值为0),将上一个节点prev的waitStatus的值由0改为-1

查看自己是否是第一个等待的,再自旋一次,查看是否可以拿到锁,如果拿不到或者不是第一个等待的,判断上一个节点的waitStatus的值是否是-1,为-1,则将线程阻塞park

private final boolean parkAndCheckinterrupt(){

LockSupport.park(this);

return Thread.interrupted();

}

 

为什么多自旋了一次:尽量不park,因为一park就会变成重量级锁

 

第二个等待的线程,因为在它之前还有排队的人,所以没有资格查看是否可以拿到锁,

自旋一次判断上一个节点的waitStatus的值是否是-1(此时初始值为0),将上一个节点prev的waitStatus的值由0改为-1

再自旋一次,判断上一个节点的waitStatus的值是否是-1,为-1,则将线程阻塞park

 

为什么修改上一个节点的waitStatus,因为自己需要下一个人判断自己是否处于睡眠状态,执行park则停止运行,不能在park后修改自己的状态,也不能在park前修改,因为可能修改了但没有park,所以需要别人的观测来确定自己是否睡眠

 

public class Node{

volatile Node prev;//上一个节点

volatile Node next;//下一个节点

volatile Thread thread;//所记录的线程

volatile int waitStatus;//当前线程的状态,初始化为0,-1为阻塞

}

AQS中的队列,有head和tail首节点和尾结点

head指向的Node节点中的thread永远为null,从第二个节点开始thread才记录线程

但是第一个节点在需要排队操作时才会被实例化,(因为单线程或是线程交替执行并不需要队列)

 

 

解锁后,查看head是否为null,且waitStatus!=0,这种情况说明有线程在等待,因为park的线程需要将前一个节点的waitStatus改为-1

将head的waitStatus改为0,unpark下一个节点的线程

第一个等待的线程被唤醒,又一次自旋,尝试加锁,将第二个node节点变为head,thread和prev变成null

 

为什么要将thread改成null:因为原则是持有锁的线程不参与排队,且第一个节点的thread为null

 

 

如果锁是自由状态:

判断自己是否需要排队

如果需要排队,查看队列是否被初始化

如果没有被初始化(这说明没有人等待),持有锁

如果已被初始化

如果队列中只有一个节点(首节点的thread为null),则说明没有线程等待,则持有锁

如果队列中的节点大于一个,则入队

 

 

打断

ReentrantLock可以对阻塞或者队列中或者park的线程进行打断

rl.lockInterruptibly();//作用是修改标识,要想有意义需要捕获异常InterruptedException,在catch中写代码

 

ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。

 

ReentrantLock将lock和lockInterruptibly共用部分方法,所以lock中的代码会无意义的修改interrupt标识

你可能感兴趣的:(Java)