并发包的基石
简介:
上一章节学习的是synchronzied关键字来解决了并发过程的内存可见性和竟态条件的问题。但是synchronzied关键字本身也遗留了一部分局限性,比如死锁问题和线程之间的协调只能有一个等待队列(条件队列),让其在开发复杂业务时,开发难度增大。当然有问题,就必然会有解决之道。这个章节我们来学习并发包的基石原子变量和CAS,以及显示锁和显示条件。
原子变量和CAS:
原子变量是什么呢?
下面先看段代码,在来解释原子变量是什么?
public class Counter {
private int count;
public synchronized void incr(){
count++;
}
public synchronized int getCount(){
return count;
}
对于count++ 这种操作,使用syhchronized 成本太高,需要先获得锁,最后还需要释放锁。获取不到锁的情况还需要等待,还有线程上下文的切换。而使用synchronized 来保证原子操作却可以使用原子变量来替代。
原子变量:
之前分析并发的条件基本是以变量作为条件,而原子变量就是提供了一部分原子操作方法的变量。通过这些方法对原子变量进行操作,而这些操作都是保证原子操作的。原子变量与synchronized锁相比,这种原子更新方式代表一种不同的思维方式。synchronized是悲观的,它假定更新是很可能发生冲突,所以要先获得锁,得到锁才能更新。原子变量的更新逻辑是乐观的,它假定更新冲突比较少但使用CAS更新,也就是进行冲突检查。如果冲突了,那也没关系,继续尝试就好。synchronzied代表一种阻塞式算法,得不到锁,进入等待队列,等待其它线程的唤醒,有上下文切换的开销。原子变量的更新是非阻塞式的,更新冲突的时候,它就重试,不会阻塞,不会有上下文切换的开销。对于大部分简单的操作,无论是低并发还是高并发,这种乐观非阻塞的方式的性能远高于悲观阻塞方式。
Java 并发包下有如下几种基本原子变量类型:
(1)AtomicInteger:原子Integer类型。
(2)AtomicLong:原子Long类型,常用来在程序中生成唯一的序列号。
(3)AtomicBoolean:原子Boolean类型,常用来表示一个标志位。
(4)AtomicReference:原子引用类型,用来原子类型更新复杂的类型。
原子变量基石方法:
compareAndSet(Object expect,Object update) 是一个非常重要的方法,比较并设置,我们以后简称为CAS。该方法有两个参数expect和update,以原子方式实现如下功能:如果当前值等于expect,则更新为update。否则不更新,如果更新成功返回true,否则返回false。
该方法的基础原理和思维:
//AtomicInteger的基本原理和思维
static class AtomicCustomInteger {
//用volatile声明,用来保证内存的可见性
private volatile int value;
AtomicCustomInteger(int value) {
this.value = value;
}
public final int incrementAndGet(){
//代码主体是个死循环,先获取当前值current,计算期望值next, 然后调用CAS方式进行更新
//如果更新没成功,说明value 被其它线程改了,则再去取最新值并尝试更新到成功为止。
for(;;){
int current = get();
int next = current + 1;
// compareAndSet()是怎么实现的?
// private static final Unsafe U = Unsafe.getUnsafe();
// public final boolean compareAndSet(int expectedValue, int newValue) {
// return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
//}
//它是Sun的私有实现,从名字看,表示的也是'不安全',一般应用程序不应该直接使用。
//原理上,一般计算机系统都在硬件层次上直接支持CAS指令。而JAVA的实现都会利用这些特殊指令。
//从程序的角度看,可以将compareAndSet视为计算机的基本操作,直接接纳就好。
if(new AtomicInteger().compareAndSet(current,next)){
return next;
}
}
}
public int get(){
return value;
}
}
compareAndSet()方法底层是直接利用了计算机系统底层的命令去进行数据的 操作。
简单的使用AtomicInteger 原子变量实现的程序计数器:
/**
*用AtomicInteger作为并发的一个程序计数器
*/
static class AtomicIntegerDemo extends Thread{
private static AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i<1000; i++){
atomicInteger.incrementAndGet();
System.out.println(atomicInteger.get());
}
}
}
//测试以AtomicInteger并发程序计数器的结果是否正确
int num = 1000;
Thread [] threads = new Thread[num];
for (int i = 0; i < num; i++) {
threads[i] = new AtomicIntegerDemo();
threads[i].start();
}
for (int i =0; i < num; i++) {
threads[i].join();
}
AtomicIntegerDemo atomicIntegerDemo = new AtomicIntegerDemo();
System.out.println(AtomicIntegerDemo.atomicInteger);
AtomicInteger 原子变量的一些基础方法:
//以AtomicInteger来作为例子进行探讨
//AtomicInteger有两个构造,一个可以设置初始化值,调用无参构造那默认值就是0
AtomicInteger atomicInteger = new AtomicInteger(1);
//获取和设置AtomicInteger的值的方法是get()和set(int value)
int value = atomicInteger.get();
System.out.println(value);
Thread thread = new Thread(() -> {
atomicInteger.set(5);
});
Thread thread1 = new Thread(() -> {
atomicInteger.set(3);
});
thread.start();
thread1.start();
thread.join();
thread1.join();
value = atomicInteger.get();
System.out.println(value);
//之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方式,部分方法如下:
//(1)以原子方式获取旧值并设置新值
value = atomicInteger.getAndSet(6);
System.out.println(value);
//(2)以原子方式获取旧值并给当前值加1
value = atomicInteger.getAndIncrement();
System.out.println(value);
//(3)以原子方式获取旧值并给当前值减1
value = atomicInteger.getAndDecrement();
System.out.println(value);
//(4)以原子方式获取旧值并给当前值加delta,任意加多少数
value = atomicInteger.getAndAdd(10);
System.out.println(value);
//(5)以原子方式给当前值加1并获取新值
value = atomicInteger.incrementAndGet();
System.out.println(value);
//(6)以原子方式给当前值减1并获取新值
value = atomicInteger.decrementAndGet();
System.out.println(value);
//(7)以原子方式给当前值加delta,并获取新值
value = atomicInteger.addAndGet(10);
System.out.println(value);
CAS方式实现悲观阻塞式锁:
static class MyLock {
private AtomicInteger atomicInteger = new AtomicInteger(0);
//lock()和unLock()使用CAS方法更新,lock()只有在更新成功后才退出。实现了阻塞的效果。
//不过这种阻塞方式过于消耗cpu
public void lock(){
//0表示未加锁,1表示加锁
while(!atomicInteger.compareAndSet(0,1)){
Thread.yield();
}
}
public void unLock(){
atomicInteger.compareAndSet(1,0);
}
}
这种方式的锁成本太高。
使用CAS方式更新产生的ABA问题:
使用CAS方式更细产生的问题是 :假设当前值为A,如果另外一个线程将A值先修改为B,后在改为A。当前前程的CAS操作无法判断当前值发生过变化。ABA是不是一个问题与程序的逻辑有关。如果确实有问题,解决的方法就是使用AtomicStampedReference,在修改值的同时插入一个时间戳,只有值和时间戳都相同才进行修改。
//public boolean compareAndSet(V expectedReference,VneWReference,int expectedStamp,int new Stamp)
Pair pair = new Pair(100,200);
int stamp = 1;
AtomicStampedReference<Pair> pairRef = new AtomicStampedReference<>(pair,stamp);
int newStamp = 2;
//AtomicStampedReference在compareAndSet中需要同时修改两个值:一个是引用,一个是时间戳。
//怎么实现原子性呢。实际上AtomicStampedReference会将两个值组合为一个对象,修改的是一个值。
//Pair是AtomicStampedReference的内部类,有一个of()方法将值和时间戳合并成一个对象变成单个值去比较和修改
pairRef.compareAndSet(pair,new Pair(200,200),stamp,newStamp);
显示锁:
显示锁Lock接口:相比synchronized关键字,ReentrantLock (Lock实现类)可以实现与synchronized相同的语义,而且显示锁底层是调用CAS实现支持非阻塞式方式获取锁,可以响应中断,可以限时,更为灵活。不过,synchronized的使用更为简单,写的代码更少,也更不容易出错。
synchronized:代表的是一种声明式的编程思想,程序员更多的是表达一种同步声明,由java系统负责具体实现,程序员不知道其实现细节。显示锁代表一种命令式编程思想,程序员实现所有细节。声明式编程的好处除了简单,还在于性能,在较新的版本的JVM上,ReentrantLock和synchronized的性能是接近的但Java编译器和虚拟机可以不断优化synchronized的实现,比如自动分析synchronized的使用。对于没有竞争的场景,自动省略对锁的获取和释放。总之能用synchronized就使用synchronized。
ReentrantLock类有两个构造方法:public ReentrantLock() 和 public ReentrantLock(boolean fair) 参数fair表示是否保证公平,不指定的情况下,默认为false,表示不保证公平。所谓公平是指,等待时间最长的线程优先获得 锁。保证公平会影响性能,一般也不需要。所以默认不保证公平,synchronized也是不保证公平的。
显示锁基本方法:
方法名 | 返回值 | 说明 |
---|---|---|
lock() | void | 当前线程获得锁,并阻塞到执行完毕 |
unlock() | void | 当前线程释放锁 |
lockInterruptibly() | void | 当前线程获得锁,并阻塞到执行完毕,但是支持中断,中断后抛出异常InterruptException |
tryLock() | boolean | 尝试获取锁,立即返回不阻塞,如果获取成功返回true,否则返回false |
tryLock(long time,TimeUnit unit) | boolean | 尝试获取锁,如果成功返回true,否则阻塞等待,等待的时间由执行的参数设置,在等待的时候响应中断,如果中断抛出InterruptException异常,如果在指定等待时间获得锁,返回true,否则返回false |
使用显示锁实现程序计数器:
//比如之前的并发程序计数器,synchronized能实现,原子变量基于CAS能实现,现在显示锁基于CAS也可以实现。
static class Counter{
private volatile int count = 0;
private final Lock lock = new ReentrantLock();
public void incr(){
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount(){
return count;
}
}
static class CountThread extends Thread {
Counter counter;
public CountThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.incr();
}
}
}
Counter counter = new Counter();
Thread[] threads = new Thread[1000];
for (int i = 0; i< 1000; i++){
threads[i] = new CountThread(counter);
threads[i].start();
threads[i].join();
}
System.out.println(counter.getCount());
显示锁解决死锁例子:
//银行转账死锁案例
static class Account {
private Lock lock = new ReentrantLock();
private volatile double money;
public Account(double money) {
this.money = money;
}
public void add(double money){
lock.lock();
try{
this.money += money;
}finally {
lock.unlock();
}
}
public void reduce(double money){
lock.lock();
try{
this.money -= money;
}finally {
lock.unlock();
}
}
public double getMoney(){
return money;
}
}
//异常类
static class NoEnoughMoneyException extends Exception{
public NoEnoughMoneyException() {
System.out.println("转账失败");
}
}
//转账
static class AccountMgr {
private static volatile NoEnoughMoneyException noEnoughMoneyException;
//会产生死锁的转账
public static void transfer(Account from,Account to,double money) throws NoEnoughMoneyException {
from.lock.lock();
try{
to.lock.lock();
try{
if(from.getMoney() > money){
from.reduce(money);
to.add(money);
System.out.println("执行成功");
}else{
throw noEnoughMoneyException;
}
}finally {
to.lock.unlock();
}
}finally {
from.lock.unlock();
}
}
//不会发生死锁的转账,显示锁解决死锁问题
public static void transfer1(Account from,Account to, double money) throws NoEnoughMoneyException {
if(from.lock.tryLock()){
try {
if (to.lock.tryLock()) {
try {
if (from.getMoney() > money) {
from.reduce(money);
to.reduce(money);
System.out.println("执行成功");
} else {
System.out.println("执行失败");
throw noEnoughMoneyException;
}
}finally {
to.lock.unlock();
}
}
}finally {
from.lock.unlock();
}
}
}
//用synchronized关键字解决死锁问题
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void synchronizedTranfer(Account from,Account to,double money) throws NoEnoughMoneyException {
synchronized (lockA){
synchronized (lockB){
if(from.getMoney() > money){
from.reduce(money);
to.reduce(money);
System.out.println("转账成功!");
}else{
System.out.println("转账失败!");
throw noEnoughMoneyException;
}
}
}
}
}
//演示死锁,随机出现死锁。当两个账户互相给对方转账时便会发生死锁
int accountNum = 2;
Account[] accounts = new Account[accountNum];
for (int i = 0; i< accounts.length; i++){
accounts[i] = new Account(new Random().nextInt(1000));
}
for (int i = 0; i<100;i++) {
Thread thread = new Thread(() -> {
try {
AccountMgr.synchronizedTranfer(accounts[new Random().nextInt(2)],
accounts[new Random().nextInt(2)], new Random().nextInt(10));
} catch (NoEnoughMoneyException e) {
e.printStackTrace();
}
});
thread.start();
}
显示条件:
上一章节我们学习了在synchronized代码块下线程之间通过wait和notify来进行协作。既然synchronized有对应的线程间协作的方式,那么显示锁也应该有,而显示锁线程之间相互协作是需要通过显示条件Condition对象里的await和signal方法。同时相比于wait和notify的协作模式只能有一个等待队列(条件队列)的局限,await和signal可以有多个等待队列。这样可以避免不必要的线程唤醒和检查,而且代码更加清晰。
await在进入等待队列后,会释放锁,释放cpu,当其他线程将它唤醒后,或者等待超时后,或发生中断异常后,它都需要重新获得锁。获得锁后,才会从await方法中退出。signal和signalAll与notify和notifyAll一样,调用它们得先获取锁,如果没有锁就会抛出lllegaMonitorStateException异常。signal和notify都是挑一个线程进行唤醒,被唤醒线程获得锁。 signalAll和notifyAll 都是唤醒所有线程,但是这些线程需要重新竞争锁, 但是signalAll唤醒的线程是需要获取的锁后才会从等待队列(这个等待是条件队列)返回,而notifyAll是直接从条件队列中删除直接到等待队列上去竞争锁。同时需要注意的是Condition 也有wait和notify方法,但是显示条件的 方法不能和wait和notify混合使用,不然会抛出异常。
显示条件的基础方法:
方法名 | 返回值 | 说明 |
---|---|---|
await() | void | 当期线程休眠,等待被唤醒,但是响应中断 |
awaitNanos(long nanosTimeout) | long | 等待相对时间,但参数是纳秒,返回值是nanosTimeout 减去实际等待时间 ,响应中断 |
await(long time,TimeUint unit) | boolean | 等待相对时间,如果由于等待超时返回false,否则返回true,响应中断 |
awaitUntil(Date deadline) | boolean | 等待相对时间,如果由于等待超时返回false,否则返回true,响应中断 |
awaitUnInterruptibly() | void | 该方法不响应中断结束,但当它返回时,如果等待过程中发生中断,中断标志位会被设置 |
signal() | void | 唤醒等待队列里的任意一个线程 ,并获得锁,并从等待队列删除 |
signalAll() | void | 唤醒等待队列的所有线程,让它们去竞争锁,获得锁线程移除等待队列 |
显示条件例子:
/**
* 用显示条件作为协调的列子
*/
static class DisplayThread extends Thread {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean fire = false;
@Override
public void run() {
try {
lock.lock();
try {
while (!fire) {
condition.await();
}
} finally {
lock.unlock();
}
System.out.println("fire");
}catch (InterruptedException i){
Thread.interrupted();
}
}
public void fire(){
lock.lock();
try {
this.fire = true;
condition.signal();
}finally {
lock.unlock();
}
}
}
/**
* 消费者生产者模式用显示条件去实现,可以避免不必要的检查和唤醒。
* synchronized 下的wait和notify局限是只能有一个等待队列,这样每次
* 唤醒都会唤醒所有线程来,同时也需要检查当前线程的条件。而显示条件
* 可以有多个等待队列,当有多个等待条件时,可以用多个等待队列,代码清晰
* 也可以减少不必要的唤醒和检查。
*/
static class MyBlockingQueue<E> {
private Queue<E> queue = null;
private int limit;
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public MyBlockingQueue(int limit) {
queue = new ArrayDeque<>(limit);
}
public void put(E e)throws InterruptedException {
lock.lockInterruptibly();
try {
while (queue.size() == limit) {
notFull.await();
}
queue.add(e);
notEmpty.signal();
}finally {
lock.unlock();
}
}
public E take()throws InterruptedException {
lock.lockInterruptibly();
try{
while (queue.isEmpty()){
notEmpty.await();
}
E e = queue.poll();
notFull.signal();
return e;
}finally {
lock.unlock();;
}
}
}
public static void main(String[] args) throws InterruptedException {
DisplayThread displayThread = new DisplayThread();
displayThread.start();
Thread.sleep(1000);
System.out.println("main is fire");
displayThread.fire();
}
总结:
本章节学习了原子变量、CAS、显示锁、显示条件。原子变量是通过CAS方式实现了一些原子操作的方法来对原子变量进行操作。这样在并发情况下操作原子变量相比于synchronized方式,可以减少锁的获取,释放锁,获取不到锁时需要等待时线程之间上下文的切换等成本,效率更高。CAS实现的原子变量是一种全新的思维方式,它是一种乐观的更新逻辑,它假定更新冲突少,但使用CAS去更新检查。如果冲突就重新尝试。而synchronzied是悲观的,它假定更新冲突多,所以要先获取锁,获取锁后才能更新。显示锁和显示条件是为了解决synchronized 的局限性,显示锁可以更灵活的解决死锁问题,显示条件可以有多个等待队列,使代码更清晰,协作条件更容易分析。