首页 > 编程开发 > 多线程与并发编程 (2/3)
2023
05-15

多线程与并发编程 (2/3)

1 Java并发包(JUC)

1.1 JDK 核心库的包

多线程与并发编程 (2/3) - 第1张  | Weiguang的博客

其中,Java 的并发工具类定义在 java.util.concurrency 包中。

1.2 java.util.concurrency

多线程与并发编程 (2/3) - 第2张  | Weiguang的博客

JUC 包中主要包括一下几种类:

**锁机制类Locks : **Lock, Condition, ReentrantLock, ReadWriteLock,LockSupport
**原子操作类Atomic : **AtomicInteger, AtomicLong, LongAdder
**线程池相关类Executor : **Future, Callable, Executor, ExecutorService
**信号量三组工具类Tools : **CountDownLatch, CyclicBarrier, Semaphore
**并发集合类Collections : **CopyOnWriteArrayList, ConcurrentMap

2 到底什么是锁*

2.1 为什么需要显式的Lock

synchronized 方式的问题:
1、同步块的阻塞无法中断(不能Interruptibly);
2、同步块的阻塞无法控制超时(无法自动解锁);
3、同步块无法异步处理锁(即不能立即知道是否可以拿到锁);
4、同步块无法根据条件灵活的加锁解锁(即只能跟同步块范围一致)。

2.2 更自由的锁: Lock

Lock 接口设计:

    // 1.支持中断的API
    void lockInterruptibly() throws InterruptedException;
    // 2.支持超时的API
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 3.支持非阻塞获取锁的API
    boolean tryLock();
  1. 使用方式灵活可控
  2. 性能开销小
  3. 锁工具包: java.util.concurrent.locks

基础接口 Lock

多线程与并发编程 (2/3) - 第3张  | Weiguang的博客

Lock 使用示例

    public class LockCounter {
        private int sum = 0;


        // 可重入锁+公平锁
        private Lock lock = new ReentrantLock(true);
        
        public int addAndGet() {
            try {
                lock.lock();
                return ++sum;
            } finally {
                lock.unlock();
            }
        }
    
        public int getSum() {
            return sum;
        }
    }

测试使用 Lock

    // 测试代码
    public static void testLockCounter() {
        int loopNum = 100_0000;
        LockCounter counter = new LockCounter();
        IntStream.range(0, loopNum).parallel().forEach(i -> counter.incrAndGet());
    }

可重入锁:当前已经持有一个锁,再次请求获取这个已经持有的锁的时候,不会发生阻塞。

公平锁:按照请求的先后获取锁。

非公平锁:所有人获得锁的机会均等。

2.3 读写锁 接口与实现

多线程与并发编程 (2/3) - 第4张  | Weiguang的博客

读读不互斥

读写互斥

写写互斥

构造方法

    // 构造方法
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

读写锁的例子

    public class ReadWriteLockCounter {
        private int sum = 0;
        // 可重入-读写锁-公平锁
        private ReadWriteLock lock = new ReentrantReadWriteLock(true);


        public int incrAndGet() {
            try {
                lock.writeLock().lock(); // 写锁; 独占锁; 被读锁排斥
                return ++sum;
            } finally {
                lock.writeLock().unlock();
            } 
        }
        
        public int getSum() {
            try {
                lock.readLock().lock(); // 读锁; //共享锁; 保证可见性
                return sum;
            } finally {
                lock.readLock().unlock();
            }
        } 
    }

注意:ReadWriteLock 管理一组锁,一个读锁,一个写锁
读锁可以在没有写锁的时候被多个线程同时持有,写锁独占的。
所有读写锁的实现必须确保写操作对读操作的内存影响。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock 适用于读多写少的并发情况。

2.4 基础接口 Condition

多线程与并发编程 (2/3) - 第5张  | Weiguang的博客

通过 Lock.newCondition() 创建。
可以看做是 Lock 对象上的信号。类似于 wait / notify。

2.5 LockSupport 锁当前线程

多线程与并发编程 (2/3) - 第6张  | Weiguang的博客

LockSupport 类似于Thread 类的静态方法,专门处理(执行这个代码的)线程的。

2.6 用锁的最佳实践

Doug Lea《Java 并发编程:设计原则与模式》一书中,推荐的三个用锁的最佳实践,它们分别是:

  1. 永远只在更新对象的成员变量时加锁
  2. 永远只在访问可变的成员变量时加锁
  3. 永远不在调用其他对象的方法时加锁

KK 总结 – 最小使用锁:
1、降低锁范围:锁定代码的范围/作用域
2、细分锁粒度:讲一个大锁,拆分成多个小锁

3 并发原子类*

Atomic 原子类工具包位于 java.util.concurrent.atomic,该工具包中包含了 AtomicInteger、AtomicLong、LongAdder 等类。

多线程与并发编程 (2/3) - 第7张  | Weiguang的博客

3.1 无锁技术

无锁技术的底层实现原理:
• Unsafe API – CompareAndSwap
• CPU 硬件指令支持- CAS 指令
• Value 的可见性- volatile 关键字

核心实现原理:
1、volatile 保证读写操作都可见(注意不保证原子);
2、使用CAS 指令,作为乐观锁实现,通过自旋重试保证写入。

原子类对数据的操作是原子性的,当需要对一个变量进行修改的时候,首先从内存中读取变量的值,然后在栈中对数据进行修改,在将数据写回到内存之前,原子类再次读取这个变量在内存中的值,如果与之前相同则直接将数据写回;如果不同,则重新读取数据到栈中,重复以上操作。

两次读取内存发现数据相同如何确保数据没有被修改过:

  1. 两次数据相同的状态是等价的
  2. 每次修改数据,版本号加1,同时比较变量的值和版本号

3.2 有锁好还是无锁好?

CAS 本质上没有使用锁。
并发压力跟锁性能的关系:
1、压力非常小,性能本身要求就不高;
2、压力一般的情况下,无锁更快,大部分都一次写入;
3、压力非常大时,自旋导致重试过多,资源消耗很大,加锁更好。

3.3 LongAdder 对 AtomicLong 的改进

AtomicLong 通过分段思想改进原子类,LongAdder 的改进思路:
1、AtomicInteger 和 AtomicLong 里的 value 是所有线程竞争读写的热点数据;
2、将单个value 拆分成跟线程一样多的数组 Cell[ ];
3、每个线程写自己的 Cell[ i ]++,最后对数组求和。

4 并发工具类详解*

wait / notify 以及 Lock / Condition 可以作为简单的协作机制。但是更复杂的使用场景需要这些线程满足某些条件(例如数量、时间)。

更复杂的应用场景:

  • 我们需要控制实际并发访问资源的并发数量
  • 我们需要多个线程在某个时间同时开始运行
  • 我们需要指定数量线程到达某个状态再继续处理

4.1 AQS

多线程与并发编程 (2/3) - 第8张  | Weiguang的博客

AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础(如 Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock),是 JUC 并发包中的核心基础组件,抽象了竞争的资源和线程队列。
• AbstractQueuedSynchronizer:抽象队列式的同步器
• 两种资源共享方式: 独占| 共享,子类负责实现公平 OR 非公平

4.2 Semaphore 信号量

  1. 准入数量N, N =1 则等价于独占锁
  2. 相当于synchronized 的进化版

使用场景:同一时间控制并发线程数

4.3 CountdownLatch

阻塞主线程,N 个子线程满足条件时主线程继续。
场景: Master 线程等待 Worker 线程把任务执行完,Master 线程再继续执行。
示例: 等所有人干完手上的活,一起去吃饭。

多线程与并发编程 (2/3) - 第9张  | Weiguang的博客

4.4 CyclicBarrier

场景: 任务执行到一定阶段, 等待其他任务对齐,阻塞N 个线程时所有线程被唤醒继续。
示例: 等待所有人都到达,再一起开吃。

多线程与并发编程 (2/3) - 第10张  | Weiguang的博客

4.5 CountDownLatch与CyclicBarrier比较

多线程与并发编程 (2/3) - 第11张  | Weiguang的博客

多线程与并发编程 (2/3) - 第12张  | Weiguang的博客

4.6 Future / FutureTask / CompletableFuture

多线程与并发编程 (2/3) - 第13张  | Weiguang的博客

多线程与并发编程 (2/3) - 第14张  | Weiguang的博客

多线程与并发编程 (2/3) - 第15张  | Weiguang的博客

最后编辑:
作者:lwg0452
这个作者貌似有点懒,什么都没有留下。
捐 赠如果您觉得这篇文章有用处,请支持作者!鼓励作者写出更好更多的文章!

留下一个回复

你的email不会被公开。