同步和协作工作类
简介:
之前章节我们学习的线程之间的协作是靠wait和notify,或者使用显示条件Condition 的await和signal。但是在一些特殊的场景,使用起来比较复杂。而
java提供了一些用于在这些特殊场景使用的工具类,他们都是基于AQS实现的。AQS是基于CAS和LockSupport提供的基础方法来实现的。
ReentrantReadWriteLock – 读写锁:
synchronized和ReentrantLock 无论是读还是写的操作,都要获取到同样的锁才能操作,否则进入等待。在一些场景是没必要这样的,多个线程的读操作完全是可以并行的,在读多写少的时候,让读操作并行是可以提高效率的。ReentrantReadWriteLock可以让读并行,同时也能保证一致性,它是可重入读写锁,继承读写锁readWriteLock。ReentrantReadWriteLock内部有两个锁。一个读锁,一个写锁。ReentrantReadWriteLock 只能保证读并行,不可以保证读写和写写并行。
//获取读锁
public ReentrantReadWriteLock.ReadLock readLock() {
return this.readerLock;
}
//获取写锁
public ReentrantReadWriteLock.WriteLock writeLock() {
return this.writerLock;
}
//ReentrantReadWriteLock使用例子:
public class ReentrantReadWriteLockMIne {
static class MyCacher {
private Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public Object get(String key){
readLock.lock();
try{
return map.get(key);
}finally {
readLock.unlock();
}
}
public void put(String key, Object value){
writeLock.lock();
try{
map.put(key,value);
}finally {
writeLock.unlock();
}
}
public void clear(){
writeLock.lock();
try{
map.clear();
}finally {
writeLock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
//ReentrantReadWriteLock读写锁只有一个线程可以进行写操作,在获取写锁时,
//只有没有任何线程持有任何锁才可以获取到,在持有写锁时,其他线程都获取不到任何锁。
//没有线程持有写锁时,多个线程可以获取和持有读锁。
MyCacher myCacher = new MyCacher();
Thread readCached = new Thread(() ->{
for (int i = 0; i <= 50 ;i++){
myCacher.get(("key"+i));
Object value = myCacher.get(("key"+i));
System.out.println("同步读缓存"+i+":"+value);
}
});
Thread readCached1 = new Thread(() ->{
for (int i = 50; i <= 100 ;i++){
Object value = myCacher.get(("key"+i));
System.out.println("同步读缓存"+i+":"+value);
}
});
Thread writeCached = new Thread(() -> {
for (int i =0; i<200;i++) {
myCacher.put("key"+i,i);
System.out.println("写入缓存"+i);
}
});
Thread readCached2 = new Thread(() ->{
for (int i = 100; i <= 150 ;i++){
Object value = myCacher.get(("key"+i));
System.out.println("同步读缓存"+i+":"+value);
}
});
Thread readCached3 = new Thread(() ->{
for (int i = 150; i < 200 ;i++){
Object value = myCacher.get(("key"+i));
System.out.println("同步读缓存"+i+":"+value);
}
});
writeCached.start();
readCached.start();
readCached1.start();
readCached2.start();
readCached3.start();
}
}
Semaphore – 信号量:
信号量是用来限制并发对同一资源访问数量的控制,比如医院每天门诊部门的 并发看诊人数。
Semaphore 常用方法:
方法名 | 返回值 | 说明 |
---|---|---|
acquire() throw InterruptedException | void | 获取许可,响应中断 |
acquireUninterruptedException | void | 获取许可,不响应中断 |
tryAcquire() | boolean | 尝试获取许可 |
tryAcquire(long timeout,TimeUnit unit) | boolean | 限定时间获取许可 |
release() | void | 释放许可 |
acquire(int permits) throw InterruptedException | void | 批量获取许可 ,响应中断 |
public class SemaphoreMine {
static class ScheduleService {
private static final int MAX_PERMITS = 1;
private Semaphore semaphore = new Semaphore(MAX_PERMITS,false);
public boolean scheduleCount(String name){
if(!semaphore.tryAcquire()){
System.out.println("看诊号已预约完!");
}
return true;
}
public void scheduleOut(){
//预约退改
semaphore.release();
}
}
public static void main(String[] args) {
ScheduleService scheduleService = new ScheduleService();
//Semaphore每调用一次tryAcquire()和acquire()都会消耗掉一个信号
scheduleService.scheduleCount("张三");
scheduleService.scheduleCount("李四");
}
}
CountDownLatch – 门栓:
倒计时门栓CountDownLatch,它相当于是一个门栓,一开始是关闭的,所有希望通过该门的线程都需要等待,然后开始倒计时,倒计时为0后,门栓打开,等待的线程都可以通过,它是一次性的,打开就不能再关上了。创建CountDownLatch需要初始化计数。
CountDownLatch 常用方法:
方法名 | 返回值 | 说明 |
---|---|---|
await() | void | 检查计数是否为0,大于零则等待,响应中断 |
countDown() | void | 检查计数,当计数大于0时,减1,如果为0,则唤醒所有等待的线程 |
public class MyLatch {
//运动员跑步同时开始,使用门栓实现。
static class Runner extends Thread {
CountDownLatch countDownLatch;
public Runner(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
countDownLatch.await();
System.out.println("运动员同时起步!!");
}catch (InterruptedException i){
Thread.interrupted();
}
}
}
static class MasterAndSlaveCooperation extends Thread {
CountDownLatch countDownLatch;
public MasterAndSlaveCooperation(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//工作线程执行业务
System.out.println("执行业务!!!");
countDownLatch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
//运动员跑步同时开始
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread[] threads = new Thread[10];
for(int i =0;i<threads.length;i++){
threads[i] = new Runner(countDownLatch);
threads[i].start();
}
Thread.sleep(1000);
countDownLatch.countDown();
//主从协作
CountDownLatch msLatch = new CountDownLatch(20);
Thread[] slaves = new Thread[20];
for(int i =0; i<slaves.length;i++){
slaves[i] = new MasterAndSlaveCooperation(msLatch);
slaves[i].start();
}
msLatch.await();
System.out.println("master and slave is work!!");
}
}
CyclicBarrier --同步栅栏:
类似集合点问题可以使用CyclicBarrier,它相当于一个栅栏,所有线程到达该栅栏都需要等待其他的线程,等所有线程都到了再一起通过,它是循环的,可以用作重复的同步。CyclicBarrier特别适用于并行迭代计算,每个线程负责计算一部分,然后再栅栏处等待其他线程完成,所有线程到齐后交换数据和计算结果,再进行下一次的迭代。CyclicBarrier的构造函数如下:
public CyclicBarrier(int parties):partie参数代表参与的线程数
public CyclicBarrier(int parties, Runnable barrierAction):Runnable 参数表示栅栏动作,在所有线程到达栅栏后,由最后一个到达的线程执行这个动作。
CyclicBarrier 常用方法:
方法名 | 返回值 | 说明 |
---|---|---|
await() throw InterruptedException,BrokenBarrierException | void | 线程到达栅栏后调用await表示到达,等待其他线程如果自己是最后一个到达,执行可选的命令,执行后,唤醒所有等待线程,然后重置内部的同步计数,以循环使用。CyclicBarrier中参与的线程都是相互影响的,其中一个被中断或者超时,所有调用await的线程都会被取消,同时抛出BrokenBarrierException意思是栅栏破了。 |
await(long timeout,TimeUnit unit) | int | 执行线程集合时间,如果超时有线程未执行完,直接抛出超时异常。 |
public class CyclicBarrierMine {
//旅游集合点
static class MarshalPoint extends Thread {
CyclicBarrier cyclicBarrier;
public MarshalPoint(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println("执行计算");
cyclicBarrier.await();
//重复执行
System.out.println("并发迭代");
cyclicBarrier.await();
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(10,() ->{
System.out.println("并发数据交互,计算结果");
});
Thread[] threads = new Thread[10];
for (int i =0;i<threads.length;i++){
threads[i] = new MarshalPoint(cyclicBarrier);
threads[i].start();
}
}
}
ThreadLocal --线程本地变量:
每个线程都有同一个变量的拷贝。Threadlocal 常用于日期处理,传递上下文信息。
ThreadLocal 常用方法:
方法名 | 返回值 | 说明 |
---|---|---|
get() | T | 获取当前线程变量值,如果没设置过,返回null,否则调用initalValue()方法获取初始化值。 |
set(T t) | void | 设置本地变量值 |
initialValue() | T | 用于提供初始值,这是一个受保护的方法,可以通过匿名类提供。 |
remove() | void | 删除当前线程设置的变量值,如果删除了,再次调用get,会调用initalValue()方法获取初始化值。 |
public class ThreadLocalMine {
//上下文信息。这是ThreadLocal的典型用途。它被用于各种框架中比如spring。
//使用ThreadLocal保存上下文信息
static class RequestContext {
private static ThreadLocal<String> userIdThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Request> requestThreadLocal = new ThreadLocal<>();
public static String getUserId(){
return userIdThreadLocal.get();
}
public static void setUserId(String userId){
userIdThreadLocal.set(userId);
}
}
public static void main(String[] args) throws InterruptedException {
//每个线程都有同一个变量的拷贝,原理是ThreadLocal每次调用set都是获取当前线程的属性ThreadLocalMap,
// 将线程作为key,传入的值位value,存到ThreadLocalMap中。调用get时,从当前线程中的ThreadLocalMap中取出值。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
Thread thread = new Thread(() ->{
System.out.println("childrenThread initValue is:"+ threadLocal.get());
threadLocal.set("3");
System.out.println("childrenThread set is :"+ threadLocal.get());
});
thread.start();
thread.join();
threadLocal.remove();
threadLocal.set("2");
System.out.println("mainThread value is:"+threadLocal.get());
//日期处理,SimpleDateFormat是线程不安全的,当并发时使用时,会出现获取到的时间都是同一时间。
//要解决这问题是加锁,或者每次都创建一个新的对象。更好的方式是用ThreadLocal,每个线程只需要创建一次,又是线程安全的
ThreadLocal<SimpleDateFormat> formatThreadLocal = new ThreadLocal<>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
}
}
总结:
本章学习的读写锁,信号量,门栓,栅栏,线程本地变量可以很方便的帮助我们解决特定场景的问题。