《并发系列二》自己动手实现互斥锁
Posted PIGP
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《并发系列二》自己动手实现互斥锁相关的知识,希望对你有一定的参考价值。
经过上一篇文章的学习我们已经大致了解了AQS互斥锁的原理,本文将使用AQS实现自己的互斥锁,通过自己实现互斥锁来加深对AQS的理解,同时讲解锁的可重入性。
自定义实现互斥锁
使用AQS时,通常将在类的内部定义一个子类继承AQS,实现互斥锁需要实现的方法,通过子类使锁的实现透明化,代码如下:
public class Mutex {
Sync sync = new Sync();
public void lock(){
sync.acquire(1);
}
public void unlock(){
sync.release(0);
}
class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, 1)){
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(getState()==0){
throw new IllegalArgumentException("state is error");
}
setState(0);
return true;
}
}
}
由代码可以看出,我们定义了一个内部类Sync继承了AbstractQueuedSynchronizer,实现与互斥锁相关的方法tryAcquire与tryRelease方法。 在该类中,我们设计为当state为0是表示锁可用,state为1表示锁已经被其他线程占有,获取锁需要等待。
tryAcquire
通过CAS原子操作将state的值由0修改为1,如果state值为0,则可以修改成功获取锁,如果state值为1,则CAS操作失败,按照上节将的流程进入等待队列
tryRelease
判断state的状态是否合法,如果state为0, 则状态出错,抛出异常;否则,状态正常,直接设置sate置为0,由于互斥锁中获得锁的线程只有一个,释放锁的线程就只有一个,不会出现并发问题,直接使用setState()方法进行设置即可
编写测试类进行测试
测试代码中,我们启动5个线程,使用自定义锁Mutex去控制线程为串行执行,代码如下:
public class MutexTest {
static class Worker implements Runnable{
Mutex mutex;
public Worker(Mutex mutex) {
this.mutex = mutex;
}
@Override
public void run() {
mutex.lock();
try {
System.out.println(Thread.currentThread().getName()+"====获取锁");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + new Date().toString());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"====释放锁");
mutex.unlock();
}
}
}
public static void main(String[] args) {
Mutex mutex = new Mutex();
new Thread(new Worker(mutex), "mutex-1").start();
new Thread(new Worker(mutex), "mutex-2").start();
new Thread(new Worker(mutex), "mutex-3").start();
new Thread(new Worker(mutex), "mutex-4").start();
new Thread(new Worker(mutex), "mutex-5").start();
}
}
使用互斥锁,上述线程会串行的执行,串行执行的意思是一个一个的执行,由于线程调度时机不同,线程mutex-1不一定会在mutex-2前面执行,只是保证多个线程之间执行锁内代码逻辑不会交叉,执行结果如下:
mutex-2====获取锁
mutex-2Thu Jul 12 17:01:59 CST 2018
mutex-2====释放锁
mutex-1====获取锁
mutex-1Thu Jul 12 17:02:00 CST 2018
mutex-1====释放锁
mutex-5====获取锁
mutex-5Thu Jul 12 17:02:01 CST 2018
mutex-5====释放锁
mutex-4====获取锁
mutex-4Thu Jul 12 17:02:02 CST 2018
mutex-4====释放锁
mutex-3====获取锁
mutex-3Thu Jul 12 17:02:03 CST 2018
mutex-3====释放锁
不可重入锁存在的问题
针对上面实现的锁,如果线程在获得锁之后再次申请锁时,是申请不到的,会出现自己等待自己的死锁的现象。修改上述的测试代码:
package com.qunar.alpaca.Test;
import java.util.Date;
/**
* Created by xinfeng.xu on 2018/7/12.
*/
public class MutexTest {
static class Worker implements Runnable{
Mutex mutex;
public Worker(Mutex mutex) {
this.mutex = mutex;
}
@Override
public void run() {
mutex.lock();
try {
System.out.println(Thread.currentThread().getName()+"====获取锁");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"====准备重新获取锁");
mutex.lock();
System.out.println(Thread.currentThread().getName()+"====成功重入锁");
System.out.println(Thread.currentThread().getName() + new Date().toString());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"====释放锁");
mutex.unlock();
}
}
}
public static void main(String[] args) {
Mutex mutex = new Mutex();
new Thread(new Worker(mutex), "mutex-1").start();
new Thread(new Worker(mutex), "mutex-2").start();
new Thread(new Worker(mutex), "mutex-3").start();
new Thread(new Worker(mutex), "mutex-4").start();
new Thread(new Worker(mutex), "mutex-5").start();
}
}
先忽略lock两次,unlock一次的问题。在上面的代码中,在第一次lock并休眠1s后,再次lock获取锁,会发生什么样的事情,执行代码,结果如下:
mutex-2====获取锁
mutex-2====准备重新获取锁
从结果中可以看出,线程阻塞了,自己在第二次执行lock方法时,在等待自己释放锁,也就是说这段代码发生了死锁。
可重入锁使用场景
说实话我也不是很清楚什么时候会用锁重入的情况,就举一个简单的例子吧,如果执行一个递归方法,递归方法内部加锁了,就会出现锁重入的情况
可重入锁
现在我们需要修改之前写的互斥锁,主要修改部分如下: (1)获取锁成功后,需要将线程设置为占有锁的线程 (2)在获取锁失败后,需要判断当前线程是否为占有锁的线程,如果是则占有锁,并增加state的值 (3)为了记录线程重入的次数,保证线程lock几次,就需要unlock几次,state的值不能仅有0与1,而是修改为0表示可以获取锁,>0表示锁已经被占有,state的值表示重入的次数 (4)锁释放时,判断state值是否为0,如果为0才是真正的释放,真正释放后需要将占有锁的线程标志置空 修改后的代码如下:
public class Mutex {
Sync sync = new Sync();
public void lock(){
sync.acquire(1);
}
public void unlock(){
sync.release(0);
}
class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if(state<0){
throw new IllegalArgumentException("state is error");
}
if(state==0 && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
if(Thread.currentThread()==getExclusiveOwnerThread()){
setState(state+1);
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
int state = getState();
if(state==0){
throw new IllegalArgumentException("state is error");
}
setState(state-1);
if(state==1){
setExclusiveOwnerThread(null);
}
return true;
}
}
}
由此,我们就完成了可重入的互斥锁的实现
总结
锁的实现,我们只需要继承AQS,实现响应的扩展方法即可,对于互斥锁我们只需要实现tryAcquire与tryRelease方法即可。下一节将讲解AQS对于共享锁的实现原理
欢迎扫描下方二维码,关注公众号,我们可以进行技术交流,共同成长
以上是关于《并发系列二》自己动手实现互斥锁的主要内容,如果未能解决你的问题,请参考以下文章
自己动手写数据库:并发管理组件lock_table的原理和实现
自己动手写数据库:并发管理组件lock_table的原理和实现