java多线程解说【伍】_锁实现:ReentrantLock的实现
前两篇文章中我们分析了Reentrantlock和Condition的实现,从中我们知道,它们都是基于队列的先进先出机制,通过构建节点排队的方式完成的调度。这里还有一个问题,就是当一个节点尝试去修改公共的变量值的时候,如何保证这个修改操作的原子性和一致性,这就要说说CAS操作。
CAS机制
什么是CAS,简单来说就是Compare-And-Set。核心的思想是,当一个线程准备去修改一个公共的参数时,先取得该参数的old值,然后计算出要修改成的new值。在写入new值前,会判断old值是否和当前的值匹配,如果匹配才会把new值写到主存。
但是CAS机制无法处理ABA问题。什么是ABA问题,简单来说就是,如果线程初次读取变量的时候是A,后来被其他线程修改为B,随后又修改为A,那么当第一个线程准备赋值的时候检查到它仍然是A,此时并不能说明它的值没有被其他线程修改过。想解决这个问题,可考虑引入版本号机制。
在AQS(AbstractQueuedSynchronizer)中,就封装了如下的方法提供给线程来修改自己的属性值;
compareAndSetState
compareAndSetHead
compareAndSetTail
compareAndSetWaitStatus
compareAndSetNext
一个lock/condition的demo
首先我们可以定义一个账户类,包含账号和余额,以及存款取款接口:
public class MyCount {
private String oid; // 账号
private int cash; // 账户余额
private Lock lock = new ReentrantLock(); // 账户锁
private Condition _save = lock.newCondition(); // 存款条件
private Condition _draw = lock.newCondition(); // 取款条件
MyCount(String oid, int cash) {
this.oid = oid;
this.cash = cash;
}
/**
* 存款
*
* @param x
* 操作金额
* @param name
* 操作人
*/
public void saving(int x, String name) {
lock.lock(); // 获取锁
if (x > 0) {
cash += x; // 存款
System.out.println(name + "存款" + x + ",当前余额为" + cash);
}
_draw.signalAll(); // 唤醒所有等待线程。
lock.unlock(); // 释放锁
}
/**
* 取款
*
* @param x
* 操作金额
* @param name
* 操作人
*/
public void drawing(int x, String name) {
lock.lock(); // 获取锁
try {
while (cash - x < 0) {
_draw.await(); // 阻塞取款操作, await之后就隐示自动释放了lock,直到被唤醒自动获取
System.out.println(name + "阻塞中");
}
cash -= x; // 取款
System.out.println(name + "取款" + x + ",当前余额为" + cash);
_save.signalAll(); // 唤醒所有存款操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
如代码所示,我们针对存款和取款两个业务场景声明了两个Condition实例,都是在同一个锁下创建的。
我们声明一个存款的线程:
public class SaveThread extends Thread {
private String name; // 操作人
private MyCount myCount; // 账户
private int x; // 存款金额
SaveThread(String name, MyCount myCount, int x) {
this.name = name;
this.myCount = myCount;
this.x = x;
}
public void run() {
myCount.saving(x, name);
}
}
再声明一个取款的线程:
public class DrawThread extends Thread {
private String name; // 操作人
private MyCount myCount; // 账户
private int x; // 取款金额
DrawThread(String name, MyCount myCount, int x) {
this.name = name;
this.myCount = myCount;
this.x = x;
}
public void run() {
myCount.drawing(x, name);
}
}
后面我们就可以搞一个main函数了:
public static void main(String[] args) {
// 创建并发访问的账户
MyCount myCount = new MyCount("95599200901215522", 10000);
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(3); //这个修改成2,可能导致老牛和胖子的死锁
Thread t1 = new SaveThread("张三", myCount, 1000);
Thread t2 = new SaveThread("李四", myCount, 1000);
Thread t3 = new DrawThread("王五", myCount, 12600);
Thread t4 = new SaveThread("老张", myCount, 600);
Thread t5 = new DrawThread("老牛", myCount, 1300);
Thread t6 = new DrawThread("胖子", myCount, 800);
Thread t7 = new SaveThread("测试", myCount, 2100);
// 执行各个线程
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
pool.execute(t7);
// 关闭线程池
pool.shutdown();
}
执行输出
当顺利的情况,控制台的日志是这样的:
李四存款1000,当前余额为11000
老张存款600,当前余额为11600
老牛取款1300,当前余额为10300
胖子取款800,当前余额为9500
测试存款2100,当前余额为11600
张三存款1000,当前余额为12600
王五取款12600,当前余额为0
有时也会这样输出:
张三存款1000,当前余额为11000
李四存款1000,当前余额为12000
老牛取款1300,当前余额为10700
老张存款600,当前余额为11300
胖子取款800,当前余额为10500
王五阻塞中
测试存款2100,当前余额为12600
王五阻塞中
王五取款12600,当前余额为0
由于王五的取款金额较大,当账户余额不足的时候就会让其等待,直到余额足够其提取。