系列文章目录
ReentrantLock初认知
AQS中可重入锁ReentrantLock源码一加锁过程
AQS中可重入锁ReentrantLock源码一释放锁过程
AQS中那些waitStatus(一)
AQS中那些waitStatus(二)
目录
ReentrantLock使用
public class NonFairReentrantLock {
public static void main(String[] args) {
final ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
new Thread("线程: "+i){
@Override
public void run() {
lock.lock(); // block until condition holds
try {
// ... method body
System.out.println(Thread.currentThread().getName()+" 开始执行!");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" 执行结束!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}.start();
}
// 主线程阻塞
LockSupport.park();
}
}
上面启动了10个线程分别去打印两句话,两句话中间线程会睡眠100ms。通过ReentrantLock的lock和unlock方法来加锁和释放锁。
执行结果:
线程: 0 开始执行!
线程: 0 执行结束!
线程: 1 开始执行!
线程: 1 执行结束!
线程: 2 开始执行!
线程: 2 执行结束!
线程: 3 开始执行!
线程: 3 执行结束!
线程: 4 开始执行!
线程: 4 执行结束!
线程: 5 开始执行!
线程: 5 执行结束!
线程: 6 开始执行!
线程: 6 执行结束!
线程: 7 开始执行!
线程: 7 执行结束!
线程: 8 开始执行!
线程: 8 执行结束!
线程: 9 开始执行!
线程: 9 执行结束!
每个线程都保证了 lock与nulock之间的方法块的互斥性。
推测自己怎么实现(不要较真代码只是屡思路)
ReentrantLock有哪些特性:
- 共享、独占
- 可重入性
- 可中断
- 公平/非公平
下面我们自己挨个来实现这些特性
共享、独占(互斥性就是同一时刻只有一个线程执行互斥性的代码)
1、直接通过synchronized关键字肯定可以实现,但是我们肯定不是直接用这个。代码就不演示了
2、CAS 加while循环来实现。
public class NonFairReentrantLock {
// 标记是否被加锁,一定要加volatile,否则线程CAS成功之后其他线程没法感知到
private volatile int status;
// java 写CAS的 标准代码。拷贝就行 --start
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static long statusOffset;
static {
try {
statusOffset = unsafe.objectFieldOffset(NonFairReentrantLock.class.getDeclaredField("status"));
} catch (NoSuchFieldException e) {
}
}
public boolean CAS(int expect, int update){
return unsafe.compareAndSwapInt(this, statusOffset,expect,update);
}
//java 写CAS的 标准代码。拷贝就行 --end
private void lock(){
while (true){
if(CAS(0,1)){
return;
}
}
}
private void unLock(){
CAS(1,0)
}
}
// 使用:
NonFairReentrantLock lock = new NonFairReentrantLock()
lock.lock()
//do something
lock.unlock()
代码逻辑:
-
多个线程来加锁的时候共同去对status变量赋值从0-1,只要CAS成功就意味着线程拿到了“锁”,就可以执行它的互斥业务代码。
-
没有拿到锁的线程会一直空循环去在哪里一直尝试CAS。此时逻辑上是没有问题的。
思考:
没有获取到“锁”的线程在哪里死循环,这是致命的。非常的消耗CPU。
解法:
- Thread.sleep();
private void lock(){
while (true){
if(CAS(0,1)){
return;
}else {
// 没加到锁我们让其睡眠一会?这里我就很纠结了。我到底该睡眠多久。
Thread.sleep(?????);
}
}
}
- Object.wait();
private void lock() throws InterruptedException {
while (true){
if(CAS(0,1)){
return;
}else {
// 是可以等待,但是由谁来唤醒?
// 的确是可以通过notify(),或者notifyAll()来唤醒,但是这会唤醒该对象关联的所有线程
// 我们最好只唤醒一个,这样才能避免多个线程再次无效的竞争
wait();
}
}
}
- UNSAFE.park() + UNSAFE.unPark() + 阻塞容器
private final Deque<Thread> deque = new LinkedList();
//java 写CAS的 标准代码。拷贝就行 --end
private void lock() throws InterruptedException {
while (true){
if(CAS(0,1)){
return;
}else {
// 阻塞了就放到 阻塞容器里。等别的线程来唤醒
deque.push(Thread.currentThread());
// 调用底层的park方法,直接阻塞线程
UNSAFE.park(false, 0L);// 此处代码只是表示park意思,代码不全
}
}
}
// 已经获取到锁的线程来释放锁
private void unLock(){
CAS(1,0);
// unPark()方法可以指定的唤醒一个指定的线程。但是此处有点疑问,这个指定的thread怎么来?
// 如果在线程被阻塞的时候直接把阻塞的线程引用放到一个容器中。我们从容器中获取一个被阻塞的线程即可。
Thread thread = deque.pop();
UNSAFE.unPark(thread);
}
至此,我觉得解决 独占这个问题思路基本ok了(手动滑稽)。
但是经不起推敲 LinkedList不是可不是线程安全的,如果很多个阻塞线程一起往里面push元素,也会出现线程安全问题。而且线程永远不会被唤醒,等于被遗弃了。那么该线程就会死在那里。但是这个问题我们先遗忘。关注主题思路
可重入性
先解释一下什么是可重入性,对一个锁可以加锁多次,但是必须释放同样的次数。
public class NonFairReentrantLock {
public static void main(String[] args) {
final ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
new Thread("线程: "+i){
@Override
public void run() {
lock.lock(); // block until condition holds
lock.lock();
lock.lock();
lock.lock();
try {
// ... method body
System.out.println(Thread.currentThread().getName()+" 开始执行!");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" 执行结束!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
lock.unlock();
lock.unlock();
lock.unlock();
}
}
}.start();
}
// 主线程阻塞
LockSupport.park();
}
}
1、既然支持重入,我们肯定得在lock对象里面记录获取锁的线程,下次该线程再来加锁的时候对比一下。如果还是它自己我们只要将status+1,解锁的时候将status-1就可以了
public class NonFairReentrantLock {
// 标记是否被加锁
private volatile int status;
// java 写CAS的 标准代码。拷贝就行 --start
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static long statusOffset;
static {
try {
statusOffset = unsafe.objectFieldOffset(NonFairReentrantLock.class.getDeclaredField("status"));
} catch (NoSuchFieldException e) {
}
}
public boolean CAS(int expect, int update) {
return unsafe.compareAndSwapInt(this, statusOffset, expect, update);
}
private final Deque<Thread> deque = new LinkedList();
private volatile Thread currentThread = null;
//java 写CAS的 标准代码。拷贝就行 --end
private void lock() throws InterruptedException {
while (true) {
if (CAS(0, 1)) {
currentThread = Thread.currentThread();
return;
} else if (currentThread ==Thread.currentThread()) {
CAS(status,status+1);
return;
} else {
// 阻塞了就放到 阻塞容器里。等别的线程来唤醒
deque.push(Thread.currentThread());
// 调用底层的park方法,直接阻塞线程
UNSAFE.park(false, 0L);// 此处代码只是表示park意思,代码不全
}
}
}
// 已经获取到锁的线程来释放锁
private void unLock() {
CAS(status,status-1);
if(status==0){
// unPark()方法可以指定的唤醒一个指定的线程。但是此处有点疑问,这个指定的thread怎么来?
// 如果在线程被阻塞的时候直接把阻塞的线程引用放到一个容器中。我们从容器中获取一个被阻塞的线程即可。
Thread thread = deque.pop();
UNSAFE.unPark(thread);
}
}
}
可中断 :
线程执行的过程中允许被别的线程中断。最流行的是取消线程上的某些任务.
例如:在应用socket编程的时候,需要创建一个serversocket实例,serversocket类的accept方法就是阻塞方法,即accept会一直等在那里,直到有一个连接请求到达,程序才继续执行。这时候问题就来了,如果没有连接请求,程序会一直阻塞在那里,即不会往下执行,这时候我们就需要中断他。例如可以设置一个等待时间,如果超过此时间,就中断accept方法。
JDK1.5的时候如果想停止一个线程的任务,可以用stop()方法,但是这会直接杀死线程,是非常暴力的。如果这个线程在处理比较重要的逻辑直接杀死线程是非常尴尬的。
微服务的应用优雅下线,肯定不是直接杀死线程,而是传递一个中断信号。让业务自己去处理好中断逻辑。
或者线程在批量提交数据,此时直接杀死线程。肯定会出现数据丢失的问题
公平\非公平
何为公平:谁等的时间长,谁就先被唤醒
何为不公平:唤醒的时候不一定就根据等待时间来决定。可能会被后来的线程提前抢到锁。