并发编程从入门到放弃系列开始和结束(二)

发布于 2022-6-13 17:42
浏览
0收藏

 

读写锁
读写锁,也可以称作共享锁,区别于独占锁,共享锁则可以允许多个线程同时持有,如ReentrantReadWriteLock允许多线程并发读,要简单概括就是:读读不互斥,读写互斥,写写互斥。

ReentrantReadWriteLock

通过阅读源码发现它内部维护了两个锁:读锁和写锁。

private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;

 

本质上,不管是ReentrantLock还是ReentrantReadWriteLock都是基于AQS,AQS只有一个状态位state,对于ReentrantReadWriteLock实现读锁和写锁则是对state做出了区分,高16位表示的是读锁的状态,低16表示的是写锁的状态。

我们可以看一个源码中给出的使用例子。

class CacheData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // 必须先释放读锁,再加写锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 重新校验状态,防止并发问题
                if (!cacheValid) {
                    data = ...
                    cacheValid = true;
                }
                // 写锁降级为读锁
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock(); // 写锁释放,仍然持有读锁
            }
        } try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}

 

这个例子嵌套写的其实不太好理解,因为他包含了一个写锁降级的概念,实际上我们自己写最简单的例子就是这样,例子中给到的示例其实是一个意思,只是在写锁释放前先降级为读锁,明白意思就好。

rwl.readLock().lock();
doSomething();
rwl.readLock().unlock();

rwl.writeLock().lock();
doSomething();
rwl.writeLock().unlock();

额外需要注意的是,写锁可以降级为读锁,但是读锁不能升级为写锁,比如下面这种写法是不支持的。

rwl.readLock().lock();
doSomething();
rwl.writeLock().lock();
doSomething();
rwl.writeLock().unlock();
rwl.readLock().unlock();

StampedLock

这是JDK1.8之后新增的一个锁,相比ReentrantReadWriteLock他的性能更好,在读锁和写锁的基础上增加了一个乐观读锁。

写锁:他的写锁基本上和ReentrantReadWriteLock一样,但是不可重入。

读锁:也和ReentrantReadWriteLock一样,但是不可重入。

乐观读锁:普通的读锁通过CAS去修改当前state状态,乐观锁实现原理则是加锁的时候返回一个stamp(锁状态),然后还需要调用一次validate(stamp)判断当前是否有其他线程持有了写锁,通过的话则可以直接操作数据,反之升级到普通的读锁,之前我们说到读写锁也是互斥的,那么乐观读和写就不是这样的了,他能支持一个线程去写。所以,他性能更高的原因就来自于没有CAS的操作,只是简单的位运算拿到当前的锁状态stamp,并且能支持另外的一个线程去写。

总结下来可以理解为:读读不互斥,读写不互斥,写写互斥,另外通过tryConvertToReadLock()和tryConvertToWriteLock()等方法支持锁的升降级。

还是按照官方的文档举个栗子,方便理解,两个方法分别表示乐观锁的使用和锁升级的使用。

public class StampedLockTest {

    private double x, y;
    private final StampedLock sl = new StampedLock();

    double distanceFromOrigin() {
        // 乐观锁
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) {
            //状态已经改变,升级到读锁,重新读取一次最新的数据
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
  
    void moveIfAtOrigin(double newX, double newY) {
        // 可以使用乐观锁替代
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // 尝试升级到写锁
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    //升级成功,替换当前stamp标记
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    //升级失败,再次获取写锁
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

 

LockSupport
LockSupport是一个比较基础的工具类,基于Unsafe实现,主要就是提供线程阻塞和唤醒的能力,上面我们提到对线程生命周期状态的时候也说过了,LockSupport的几个park功能将会把线程阻塞,直到被唤醒。

看看他的几个核心方法:

public static void park(); //阻塞当前线程
public static void parkNanos(long nanos); //阻塞当前线程加上了超时时间,达到超时时间之后返回
public static void parkUntil(long deadline); //和上面类似,参数deadline代表的是从1970到现在时间的毫秒数
public static void unpark(Thread thread);// 唤醒线程

举个栗子:

public class Test {

    public static void main(String[] args) throws Exception {
        int sleepTime = 3000;
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "挂起");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "继续工作");
        });
        t.start();

        System.out.println("主线程sleep" + sleepTime);
        Thread.sleep(sleepTime);
        System.out.println("主线程唤醒阻塞线程");
        LockSupport.unpark(t);
    }
}
//输出如下
主线程sleep3000
Thread-0挂起
主线程唤醒阻塞线程
Thread-0继续工作

 

原子类

多线程环境下操作变量,除了可以用我们上面一直说的加锁的方式,还有其他更简单快捷的办法吗?

JDK1.5之后引入的原子操作包下面的一些类提供给了我们一种无锁操作变量的方式,这种通过CAS操作的方式更高效并且线程安全。

并发编程从入门到放弃系列开始和结束(二)-开源基础软件社区

 原子类

 

基本数据类型
我们先说针对基本数据类型提供的AtomicInteger、AtomicLong、AtomicBoolean,看名字都知道是干嘛的,由于基本上没什么区别,以AtomicInteger的方法举例来说明。

public final int getAndIncrement(); //旧值+1,返回旧值
public final int getAndDecrement(); //旧值-1,返回旧值
public final int getAndAdd(int delta); //旧值+delta,返回旧值
public final int getAndSet(int newValue); //旧值设置为newValue,返回旧值
public final int getAndAccumulate(int x,IntBinaryOperator accumulatorFunction); //旧值根据传入方法进行计算,返回旧值
public final int getAndUpdate(IntUnaryOperator updateFunction)//旧值根据传入进行计算,返回旧值

与之相对应的还有一套方法比如incrementAndGet()等等,规则完全一样,只是返回的是新值。

我们看看下面的例子,针对自定义规则传参,比如我们可以把计算规则改成乘法。

public class AtomicIntegerTest {
    public static void main(String[] args) {
        AtomicInteger atomic = new AtomicInteger(10);
        System.out.println(atomic.getAndIncrement()); //10
        System.out.println(atomic.getAndDecrement()); //11
        System.out.println(atomic.getAndAdd(2));//10
        System.out.println(atomic.getAndSet(10)); //12
        System.out.println(atomic.get());             //10

        System.out.println("=====================");
        System.out.println(atomic.getAndAccumulate(3, (left, right) -> left * right)); // 10
        System.out.println(atomic.get()); //30
        System.out.println(atomic.getAndSet(10)); //30

        System.out.println("=====================");
        System.out.println(atomic.getAndUpdate(operand -> operand * 20)); // 10
        System.out.println(atomic.get()); //200
    }
}

另外提到一嘴,基本数据类型只给了Integer、Long、Boolean,那其他的基本数据类型呢?其实看下AtomicBoolean的源码我们发现其实他本质上是转成了Integer处理的,那么针对其他的类型也可以参考这个思路来实现。

 

数组
针对数组类型的原子操作提供了3个,可以方便的更新数组中的某个元素。

AtomicIntegerArray:针对Integer数组的原子操作。

AtomicLongArray:针对Long数组的原子操作。

AtomicReferenceArray:针对引用类型数组的原子操作。

和上面说的Atomic其实也没有太大的区别,还是以AtomicIntegerArray举例说明,主要方法也基本一样。

public final int getAndIncrement(int i);
public final int getAndDecrement(int i);
public final int getAndAdd(int i, int delta);
public final int getAndSet(int i, int newValue);
public final int getAndAccumulate(int i, int x,IntBinaryOperator accumulatorFunction);
public final int getAndUpdate(int i, IntUnaryOperator updateFunction);

操作一模一样,只是多了一个参数表示当前索引的位置,同样有incrementAndGet等一套方法,返回最新值,没有区别,对于引用类型AtomicReferenceArray来说只是没有了increment和decrement这些方法,其他的也都大同小异,不再赘述。

说实话,这个都没有举栗子的必要。

public class AtomicIntegerArrayTest {
    public static void main(String[] args) {
        int[] array = {10};
        AtomicIntegerArray atomic = new AtomicIntegerArray(array);
        System.out.println(atomic.getAndIncrement(0)); //10
        System.out.println(atomic.get(0));//11
        System.out.println(atomic.getAndDecrement(0)); //11
        System.out.println(atomic.getAndAdd(0, 2));//10
        System.out.println(atomic.getAndSet(0, 10)); //12
        System.out.println(atomic.get(0));             //10

        System.out.println("=====================");
        System.out.println(atomic.getAndAccumulate(0, 3, (left, right) -> left * right)); // 10
        System.out.println(atomic.get(0)); //30
        System.out.println(atomic.getAndSet(0, 10)); //30

        System.out.println("=====================");
        System.out.println(atomic.getAndUpdate(0, operand -> operand * 20)); // 10
        System.out.println(atomic.get(0)); //200
    }
}

 

文章转自公众号:艾小仙

分类
标签
已于2022-6-13 17:42:00修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐