JUC包下的一些常见类

注:本文不介绍 ReentrantLock 和线程安全的集合类。

1. 读写锁

1.1 ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用读写锁让读-读可以并发,提高性能。 类似于数据库中的 select ... from ... lock in share mode

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

class DataContainer {
    private Object data;
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock r = rw.readLock();//获取读锁 保护读操作
    private final ReentrantReadWriteLock.WriteLock w = rw.writeLock();//获取写锁 保护写操作

    public Object read() {
        log.debug("获取读锁...");
        r.lock();
        try {
            log.debug("读取");
            sleep(1);
            return data;
        } finally {
            log.debug("释放读锁...");
            r.unlock();//释放读锁
        }
    }

    public void write() {
        log.debug("获取写锁...");
        w.lock();
        try {
            log.debug("写入");
            sleep(1);
        } finally {
            log.debug("释放写锁...");
            w.unlock();//释放写锁
        }
    }
}

测试 读锁-读锁 可以并发

public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.read();
        }, "t2").start();
    }

输出结果,从这里可以看到 t2 锁定期间,t1 的读操作不受影响


测试 读锁-写锁 相互阻塞

public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        Thread.sleep(100);
        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }

运行结果:

读锁获取到锁后,休眠1秒,写锁尝试获取锁,失败,等读锁释放锁后,写锁才获取到锁。


测试 写锁-写锁 相互阻塞

public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
            dataContainer.write();
        }, "t1").start();

        Thread.sleep(100);
        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }

运行结果:

t1释放掉写锁后,t2才能获取锁。

注意事项

  • 读锁不支持条件变量,写锁支持条件变量。
  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待。
  • 重入时降级支持:即持有写锁的情况下去获取读锁

以下从 ReentrantLock 中找的一个例子,来验证第三点注意事项:

class CachedData {
     //需要缓存的数据
     Object data;
     // 是否有效,如果失效,需要重新计算 data
     volatile boolean cacheValid;
     final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     void processCachedData() {
         //加读锁
         rwl.readLock().lock();
         if (!cacheValid) {//缓存数据失效
             // 尝试获取写锁重新计算
             // 获取写锁前必须释放读锁
             rwl.readLock().unlock();
             // 获取写锁
             rwl.writeLock().lock();
             try {
                 // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
                 // 双重检查,外层的cahceVaild不受写锁保护,可能被别的线程读取到
                 if (!cacheValid) { 
                     //确认失效,重新计算
                     data = ...
                     cacheValid = true;
                 }
                 // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存,且接下来读数据时,不被其他写线程干扰
                 rwl.readLock().lock();
             } finally {
                 rwl.writeLock().unlock();
             }
         }
         // 缓存数据未失效
         // 自己用完数据, 释放读锁
         try {
             use(data);
         } finally {
            rwl.readLock().unlock();
         }
     }
}

1.2 StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加解读锁

long stamp = lock.readLock();//戳
lock.unlockRead(stamp);//根据该戳去解锁

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead();//返回一个戳,没有加锁
// 验戳 防止其他写线程干扰
if(!lock.validate(stamp)){
     // 验戳失败,被其他线程修改 锁升级 升级为读锁
}

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

class DataContainerStamped {
    private int data;// 共享的数据
    private final StampedLock lock = new StampedLock();

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public int read(int readTime) {
        long stamp = lock.tryOptimisticRead();// 乐观读,返回戳
        log.debug("optimistic read locking...{}", stamp);
        sleep(readTime);// 模拟读的时间
        if (lock.validate(stamp)) { // 戳的校验
            // 验证通过,返回数据
            log.debug("read finish...{}, data:{}", stamp, data);
            return data;
        }
        // 验戳失败
        // 锁升级 - 读锁
        log.debug("updating to read lock... {}", stamp);
        try {
            // 加读锁
            stamp = lock.readLock();
            log.debug("read lock {}", stamp);
            sleep(readTime);
            log.debug("read finish...{}, data:{}", stamp, data);
            return data;
        } finally {
            log.debug("read unlock {}", stamp);
            lock.unlockRead(stamp);// 释放读锁,传入戳
        }
    }

    public void write(int newData) {
        long stamp = lock.writeLock();// 加写锁,返回戳
        log.debug("write lock {}", stamp);
        try {
            sleep(2);
            this.data = newData;
        } finally {
            log.debug("write unlock {}", stamp);
            lock.unlockWrite(stamp);// 释放写锁,传入戳
        }
    }
}

测试 读-读 可以优化

public static void main(String[] args) {
        DataContainerStamped dataContainer = new DataContainerStamped(1);
        new Thread(() -> {
            dataContainer.read(1);
        }, "t1").start();

        sleep(0.5);

        new Thread(() -> {
            dataContainer.read(0);
        }, "t2").start();
    }

输出结果,可以看到实际没有加读锁

测试 读-写 时优化读补加读锁

public static void main(String[] args) {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -> {
        dataContainer.read(1);
    }, "t1").start();

    sleep(0.5);

    new Thread(() -> {
        dataContainer.write(100);
    }, "t2").start();
}

输出结果

t2写线程修改了戳,t1线程验戳失败,进行锁升级,加读锁,但是此时t2写线程还在工作,所以t1线程加读锁阻塞,等t2写线程工作结束(戳又被更新了)释放锁后,t1线程才加上读锁。

注意 :StampedLock 不支持条件变量; StampedLock 不支持可重入。所以 StampedLock 并不能取代 ReentrantReadWriteLock。

2. Semaphore[ˈsɛməˌfɔr]

信号量,用来限制能同时访问共享资源的线程上限。用在共享资源有多个,且允许多个线程访问这些共享资源,只是希望对访问的线程上限加以限制。

构造函数:

// permits 指共享资源的数量,或想限制的最大访问线程数量
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

// fair 表示是公平或非公平
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

测试案例:

public static void main(String[] args) {
        // 1. 创建 semaphore 对象
        Semaphore semaphore = new Semaphore(3);

        // 2. 10个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                    log.debug("running...");
                    sleep(1);
                    log.debug("end...");
            }).start();
        }
    }

运行结果:

此时没有线程限制,10个线程都运行起来了。

加上限制:

public static void main(String[] args) {
        // 1. 创建 semaphore 对象
        Semaphore semaphore = new Semaphore(3);

        // 2. 10个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    // 运行前获取许可
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    log.debug("running...");
                    sleep(1);
                    log.debug("end...");
                } finally {
                    // 运行后释放许可
                    semaphore.release();
                }
            }).start();
        }
}

运行结果:

3个线程获取锁开始运行,其他线程进入阻塞,只有当这三个线程运行结束后释放锁,才能又有3个线程获得锁运行,依次执行。

3. CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时。

其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一

看一下 CountdownLatch 的源码:

public class CountDownLatch {

    // 内部维护了一个同步器,同步器继承了AQS
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        // 判断线程获得锁的条件是getState() 是否等于0,等于0相当于获得了锁,大于0相当于获得不到锁会被阻塞住,使用的是共享锁
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        // 如果其线程调用了 release 方法,则它会间接调用该方法
        protected boolean tryReleaseShared(int releases) {
            // 让计算值每次减1,减到0便会唤醒阻塞的线程,没有减到0就return false,不会唤醒阻塞的线程
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    ......
}

测试案例:

private static void test4() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        new Thread(() -> {
            log.debug("begin...");
            sleep(1);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        }).start();

        new Thread(() -> {
            log.debug("begin...");
            sleep(2);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        }).start();

        new Thread(() -> {
            log.debug("begin...");
            sleep(1.5);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        }).start();

        log.debug("waiting...");
        latch.await();
        log.debug("wait end...");
    }

运行结果:

只有当线程Thread-2运行结束后,将计数减到0后,才能将主线程从AQS阻塞队列中唤醒,发现计数变成0,结束运行。

可以配合线程池使用,改进如下

private static void test5() {
        CountDownLatch latch = new CountDownLatch(3);
        // 线程池
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(() -> {
            log.debug("begin...");
            sleep(1);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
        service.submit(() -> {
            log.debug("begin...");
            sleep(1.5);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
        service.submit(() -> {
            log.debug("begin...");
            sleep(2);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
        // 汇总
        service.submit(()->{
            try {
                log.debug("waiting...");
                latch.await();
                log.debug("wait end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
}

运行结果:

15秒时,4个任务都开始运行,包括等待汇总的任务也被提交到了线程池,直到线程thread-3运行结束将计数修改为0,等待汇总的任务才被唤醒,进行汇总工作。

下面来看一个 CountdownLatch 的应用小案例:

模拟打游戏时多个玩家进入游戏时加载情况,只有10个玩家都加载完成后,游戏才能开始:

private static void playGame() throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);
        ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
            return new Thread(r, "t" + num.getAndIncrement());
        });
        CountDownLatch latch = new CountDownLatch(10);
        String[] all = new String[10];
        Random r = new Random();
        for (int j = 0; j < 10; j++) {
            // lambda 表达式只能引用局部常量
            int x = j;
            service.submit(() -> {
                for (int i = 0; i <= 100; i++) {
                    try {
                        // 随机休眠一段时间,模拟不同的加载速度
                        Thread.sleep(r.nextInt(100));
                    } catch (InterruptedException e) {
                    }
                    all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";
                    //不换行,用回车符回退(覆盖)之前的打印
                    System.out.print("\r" + Arrays.toString(all));
                }
                // 让计数减一
                latch.countDown();
            });
        }
        // 主线程等待
        latch.await();
        System.out.println("\n游戏开始...");
        service.shutdown();
}

运行结果:

4. CyclicBarrier [ˈsaɪklɪk ˈbæriɚ]

循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        CyclicBarrier barrier = new CyclicBarrier(2, ()-> {
            // 第三个任务,当第1个和第2个任务执行完后才执行。
            log.debug("task1, task2 finish...");
        });
        for (int i = 0; i < 3; i++) { // task1  task2  task1
            service.submit(() -> {
                log.debug("task1 begin...");
                sleep(1);
                try {
                    barrier.await(); // 2-1=1
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
            service.submit(() -> {
                log.debug("task2 begin...");
                sleep(2);
                try {
                    barrier.await(); // 1-1=0
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        service.shutdown();
}

注意:线程池线程数要和 CyclicBarrier 的计数一致,否则,由于任务1比任务2先执行完,假如线程池线程数为3 ,则会执行俩次任务1,1次任务2(task1  task2  task1),俩次任务1将计数减为了0,此时任务3便会执行,这样就不是任务1和任务2执行完(task1, task2 finish...)。

运行结果:

注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值