《并发编程的艺术》阅读笔记④(第7~11章)

115 阅读11分钟

Java中的13个原子操作类

原子更新基本类型类

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

讲解AtomicInteger

常用方法

  • int addAndGet(int delta) 以原子方式将输入数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
  • boolean compareAndSet(int expect, int update) 如果输入值等于预期值,则以原子方式将该值设置为输入值。
  • int getAndIncrement() 以原子方式加一,但返回是自增前的值
  • void lazySet(int newValue) 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一段时间内还可以读到旧值。

getAndIncrementUnsafe源码

getAndIncrement
  • 取得AtomicInteger
  • 对当前数加一
  • 调用compareAndSet方法进行原子更新操作
    • 看当前数是否等于current(即查看是否被其他线程所改变)
      • 相等则更新成next
      • 不相等则进入for循环,并进行compareAndSet操作
public final int getAndIncrement() {
    for(;;) {
        int current = get();
        int next = current + 1;
        if(compareAndSet(current, next))
            return current;
    }
}

public final boolean compareAndSet(int expect, int  update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe

提供三种CAS方法:

  • compareAndSwapObject
  • compareAndSwapInt
  • compareAndSwapLong
/**
 * 如果当前数是`expected`,则原子的将Java变量更新成x
 * @return 如果更新成功则返回true
 */
public final native boolean compareAndSwapObject(Object o,
                                                long offset.
                                                Object expected,
                                                Object x);

public final native boolean compareAndSwapInt(Object o, long offset.
                                              int expected,
                                              int x);

public final native boolean compareAndSwapLong(Object o, long offset.
                                               long expected,
                                               long x);

当更新AtomicBoolean时,先转为整型,再使用compareAndSwapInt进行CAS。

原子更新数组

包含3个类:

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

AtomicIntegerArray的常用方法

常用方法:

  • int addAndGet(int i, int delta) 以原子方式将输入值与数值索引i的元素相加
  • boolean compareAndSet(int i, int expect, int update) 如果等于预期值则以原子方式将数组位置i的元素设置成update

原子更新引用类型

  • AtomicReference
  • AtomicReferenceFieldUpdater
  • AtomicReferenceReference 原子更新带有标记位的引用类型,可以原子更新一个布尔类型标记位和引用类型。
    构造方法:AtomicMarkableReference(V initialRef, boolean initialMark)

原子更新字段

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicStampedReference

Java中的并发工具类

  • 并发流程控制工具类:CountDownLatchCyclicBarrierSemaphore
  • 在线线程间交换数据工具类:Exchanger

等待多线程完成CountDownLatch

  • CountDownLatch构造函数接收一个int类型的参数为计数器,传入N就等待N个点完成
  • CountDownLatchcountDown方法时,N就-1
    • N可以是N个线程,也可以是一个线程的N个执行步骤
    • 配合await(long time, TimeUnit unit)方法实现等待特定时间后不再阻塞线程

同步屏障CyclicBarrier

让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。

  • CyclicBarrier(int parties) 参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier当前线程已到达屏障,然后当前线程进入阻塞。
  • CyclicBarrier(int parties, Runnable barrierAction) 到达屏障时,优先执行barrierAction,方便处理更为复杂的场景

实例

处理每个sheet的数据,得到每个sheet的日均流水,最后再计算整个银行的日均流水

import java.util.Map.Entry;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class BankWaterService implements Runnable {
    /**
     * 创建四个屏障,处理完后执行当前类的run方法
     */
    private CyclicBarrier c = new CyclicBarrier(4, this);

    /**
     * 只有4个sheet
     */
    private Executor executor = Executors.newFixedThreadPool(4);

    /**
     * 保存每个sheet计算出的流水结果
     */
    private ConcurrentHashMap<String, Integer>sheetBankWaterCount = new ConcurrentHashMap<String, Integer>();

    private void count() {
        for(int i = 0; i < 4; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    //计算当前sheet流水数据,计算代码省略...
                    sheetBankWaterCount.put(Thread.currentThread().getName(), 1);
                    //计算完成插入屏障
                    try {
                        c.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace()''
                    }
                }
            });
        }
    }

    @Override
    public void run() {
        int result = 0;
        for(Entry<String, Integer>sheet:sheetBankWaterCount.entrySet()) {
            result += sheet.getValue();
        }
        //将结果输出
        sheetBankWaterCount.put("result",result);
        System.out.println(result);
    }

    public static void main(String[] args) {
        BankWaterService banWaterCount = new BankWaterService();
        banWaterCount.count();
    }
}

CyclicBarrierCountDownLatch的区别

CountDownLatch只用一次,CyclicBarrier可以reset(),还可以通过isBroken()方法检查阻塞线程是否被中断

控制并发线程数Semaphore

应用场景:公共资源有限,如数据库连接,但都是IO密集型任务,可以启动几十个线程并发读取,读后存到数据库中必须控制这几十个线程。

举例并发10个线程

其他方法

  • intavailablePermits() 返回此信号量当前可用的许可证数
  • intgerQueueLength() 等待获取许可证的线程数
  • boolean hasQueuedThreads() 是否有线程等待许可证
  • void reducePermits(int reduction) 减少reduction许可证,是个protected方法
  • Collection getQueuedThreads() 返回所有等待许可证的线程集合,protected方法

线程间交换数据的Exchanger

应用场景:遗传算法、校对工作

public class ExchangerTest {
    private static final Exchanger<String>exgr = new Exchanger<String>();
    private static ExecutorServicethreadPool = Executors.newFixedThreadPool(2);
    public static void main(String[] args) {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String A = "银行流水"//A录入银行流水
                    exgr.exchange(A);
                } catch (InterruptedException e) {

                }
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String B = "银行流水B";//录入数据
                    String A = exgr.exchange("B");
                    System.out.println("是否一致: " + A.equals(B) + "A的录入:" + A + "B的录入:" + B);
                } catch(InterruptedException e) {

                }
            }
        });
        threadPool.shutdown();
    }
}

若一直无法执行exchange()方法,则可以使用exchange(V x, long timeout, TimeUnit unit)

Java线程池

优点:

  1. 降低资源消耗。重复利用已创建的线程降低线程的创建和销毁造成的消耗。
  2. 提高响应速度。任务不需要等待线程创建可以直接执行。
  3. 提高线程的可管理性。

线程池实现原理

当一个新任务来到线程池时的处理流程:

  • 线程池判断核心线程池的任务是否都在执行
    • 不是都在执行,则新建一个线程
    • 都在执行,则下一步
  • 线程池判断工作队列是否已满
    • 未满,则入队
    • 满了就下一步
  • 线程池判断线程池的线程是否都处于工作状态
    • 没有,则创建新线程来执行该任务
    • 如果线程池满了就交给饱和策略来处理这个任务

ThreadPoolExecutor执行execute方法分下面4种情况:

  1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
  2. 如果运行的线程等于或多于corePoolSize,则任务加入BlockingQueue
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理(注意,执行这一步骤需要获取全局锁)
  4. 如果创建新线程将使当前运行的线程数超出maximumPoolSize则任务被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法

源码实现

  • 执行任务
public void execute(Runnable command) {
    if(command == null) throw new NullPointException();
    //如果线程数小于core部分的,执行
    if(poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        //尝试加入队列
        if (runState == RUNNING && workQueue.offer(command)) {
            if(runState != RUNNING || poolSize == 0)
                ensureQueueTaskHandled(command);
        }
        //无法入队则尝试新建一个线程
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command);
    }
}
  • run()方法 线程池创建线程时,会将线程封装为工作线程WorkerWorker执行完毕后还会回队列取任务
public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while(task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        } finally {
            workerDone(this);
        }
    }
}

线程池的使用

创建

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler);

  • corePoolSize 如果调用线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue任务队列
    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronousQueue
    • PriorityBlockingQueue
  • maximumPoolSize 如果使用了无界阻塞队列,则该参数没有什么意义了
  • ThreadFactory 用于设置更有意义的名字,使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程设置名字:
    new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
  • RejectedExecutionHandler饱和策略(JDK1.5Java线程池框架提供了以下四种方法)
    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只有调用者所在线程来运行任务
    • DiscardOldestPolicy:丢弃队列里最近一个任务,并执行当前任务
    • DiscardPolicy:不处理,丢弃掉
    • 也可以根据应用场景来实现RejectedExecutionHandler接口自定义策略,如记录日志或持久化存储不能处理的任务
  • keepAliveTime 工作线程空闲后,保持存活的时间
  • TimeUnit 线程活动保持时间的单位

提交任务

  • execute() 没有返回值,Runnable类的实例
threadPool.execute(new Runnable() {
    @Override
    public void run() {...}
});
  • submit()
    • 有返回值,返回future类的对象,该对象可以判断任务是否执行成功
    • future.get()获取返回值,此时将会阻塞当前线程
    • get(long timeout, TimeUnit unit)方法设置超时时长
Future<Object> future = executor.submit(harReturnValuetask);
try {
    Object s = future.get();
} catch(InterruptedException e) {
    //处理中断
} catch(ExecutionException e) {
    //处理无法执行的任务异常
} finally {
    //关闭线程池
    executor.shutdown();
}

关闭

shutdownshutdownNow方法关闭。
其原理是:遍历线程池中的工作线程,然后逐个调用interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

  • shutdown 只是设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程,正在执行的任务会执行完。
  • shutdownNow 状态设置为STOP,尝试停止所有正在执行或暂停的任务,并返回等待执行任务的列表,不保证可以执行完任务。

调用任何一个方法,isShutdown方法都会返回true,所有任务都关闭isTerminaed()方法才会返回true

监控

  • taskCount:线程池需要执行的任务数量
  • completedTaskCount:已完成的任务数量,小于或等于taskCount
  • largestPoolSize:最大线程数量
  • getPoolSize:线程池的线程数量
  • getActiveCount:获取活动的线程数

可以通过继承线程池定义线程池,重写线程池的beforeExecuteafterExecuteterminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。

tips

想要合理地配置线程池,首先要分析任务特性:

  • 任务的性质:CPU密集型(尽可能少的线程)/IO密集型(尽可能大的线程)/混合型任务(分解后执行)
    • Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
  • 任务的优先级:高中低(PriorityBlockingQueue处理)
  • 任务的执行时间:长中短
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接

建议使用有界队列,可以增加系统的稳定性和预警能力。

Executor框架

Java线程会一对一映射为本地操作系统线程

  • 在上层
    • Java多线程程序将应用分解为若干个任务
    • 用户级的调度器(Executor框架)将任务映射为固定数量的线程
  • 在底层
    • 操作系统内核将这些线程映射到硬件处理器上

Executor框架的结构

组成部分

  • 任务:需要实现RunnableCallable接口
  • 任务的执行:包括核心接口Executor,以及继承自ExecutorExecutorService接口(ThreadPoolExecutorScheduledThreadPoolExecutor
  • 异步计算的结果:包括接口Future和实现Future接口的FutureTask

相关接口和类的简介

  • Executor接口 Executor框架的基础,将任务的提交与任务的执行分离开来
  • ThreadPoolExecutor 线程池的核心实现类,用来执行被提交的任务
    有固定的、单线程的、根据需要设定的无界的三种可选
  • ScheduledThreadPoolExecutor 可以给定延迟后执行命令,或者定期执行命令。比Timer更灵活、强大
    单线程的或多个的
  • 接口Future和实现Future接口的FutureTask类 异步计算的结果
  • RunnableCallable接口 都可以被ScheduledThreadPoolExecutorThreadPoolExecutor执行
    Runnable不返回结果Callable返回结果

执行流程

  1. 主线程首先要创建实现RunnableCallable接口的任务对象。
  2. 工具类Executors可以把一个Runnable对象封装成一个Callable对象(Executors.callable(Runnable task)Executors.callable(Runnable task, Object result))然后Runnable对象交给ExecutorService执行(ExecutorService.execute(Runnable command))或者直接把RunnableCallable接口的任务对象交给ExecutorService执行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable<T> task)
  3. 如果执行submit则返回一个实现Future接口的FutureTask类对象。由于FutureTask实现了Runnable,程序员可以创建FutureTask类对象交给ExecutorService执行
  4. 最后,主线程可以执行FutureTask.get()方法,也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消这个任务

Java并发编程实战

生产者消费者模式

生产者按一定规则去邮件系统里抽取邮件,然后存放到阻塞队列里,消费者从阻塞队列里取文章后插入到conflunce里

public class QuickEmailToWikiExtractor extends AbstractExtractor {\

    private ThreadPoolExecutor threadsPool;
    private ArticleBlockingQueue<ExchangeEmailShallDTO> emailQueue;

    public QuickEmailToWikiExtractor() {
        emailQueue = new ArticleBlockingQueue<ExchangeEmailShallDTO> ();
        int corePoolSize = Runtime.getRuntime().auailableProcessors() * 2;
        threadPool = new ThreadPoolExecutor(corePoolSize, 
                                            corePoolSize, 
                                            101, 
                                            TimeUnit.SECONDS, 
                                            new LinkedBlockingQueue<Runnable>(2000))
    }

    public void extract() {
        logger.debug("开始" + getExtractorName() + "。。");
        long start = System.currentTimeMillis();

        //抽取所有邮件放入队列
        new ExtractEmailTask().start();

        //把队列里的文章插入到wiki
        insertToWiki();

        long end = System.currentTimeMillis();
        double cost = (end - start) / 1000;
        logger.debug("完成" + getExtractorName() + ",花费时间:" + cost + "秒");
    }

    /**
     * 把队列里的文章插入到WIKI
     */
    private void insertToWiki() {
        //登录Wiki,每间隔一段时间需要登录一次
        confluenceService.login(RuleFactory.USER_NAME, RuleFactory.PASSWORD);

        while(true) {
            //2秒取不出就退出
            ExchangeEmailShallDTO email = emailQueue.poll(2, TimeUnit.SECONDS);
            if(email == null) {
                break;
            }
            threadPool.submit(new insertToWiki(email));
        }
    }

    protected List<Article> extractEmail() {
        List<ExchangeEmailShallDTO> allEmails = getEmailService().queryAllEmails();
        if(allEmails == null) {
            return null;
        }
        for(ExchangeEmailShallDTO exchangeEmailShallDTO : allEmails) {
            emailQueue.offer(exchangeEmailShallDTO);
        }
        return null;
    }

    /**
     * 抽取邮件任务
     */
    public class ExtractEmailTask extends Thread {
        public void run() {
            extractEmail();
        }
    }
}

消息队列

  • 总消息队列管理
/**
 * 总消息队列管理
 */
public class MsgQueueManager implements IMsgQueue {
    private static final Logger LOGGER = LoggerFactory.getLogger(MsgQueueManager.class);

    /**
     * 消息总队列
     */
    public final BlockingQueue<Message> messageQueue;

    private void put(Message msg) {
        try {
            messageQueue.put(msg);
        } catch(InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return null;
    }
}
  • 消息分发线程
/**
 * 分发消息
 */
static class DispatchMessageTask implements Runnable {
    @Override
    public void run() {
        BlockingQueue<Message> subQueue;
        for(;;) {
            //没有数据就阻塞在这里
            Message msg = MsgQueueFactory.getSubQueue().take();
            //空,则表示没有session机器连接
            //需等待
            while ((subQueue = getInstance().getSubQueue()) == null) {
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException e){
                    Thread.currentThread().interrupt();
                }
            }
            //把消息放到小队列里
            try {
                subQueue.put(msg);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}
  • 散列算法获取一个子队列
/**
 * 均衡获取一个子队列
 */
public BlockingQueue<Message> getSubQueue() {
    int errorCount = 0;
    for(;;) {
        if(subMsgQueue.isEmpty()) {
            return null;
        }
        int index = (int)(System.nanoTime() % subMsgQueue.size());
        try {
            return subMsgQueue.get(index);
        } catch (Exception e) {
            LOGGER.error("获取子队列出现错误", e);
            if((++errorCount) < 3) {
                continue;
            }
        }
    }
}
  • 使用
IMsgQueue messageQueue = MsgQueueFactory.getMessageQueue();
Packet msg = Packet.createPacket(Packet64FrameType.
    TYPE_DATA, "{}".getBytes(), (short)1);
messageQueue.put(msg);