并发笔记(一)

基本概念

  • 同步与异步
    • 同步方法等待返回,异步方法立即返回
  • 并发和并行
    • 并行是两个任务同时进行,并发是两个任务切换执行
    • 并发是交替的执行
    • 并行是多个任务真实的同时执行
  • 临界区
    • 公共资源或者共享数据,可以被多个线程使用。但每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个资源就必须等待
  • 阻塞和非阻塞
    • 如果一个线程占用了临界区资源,其他的线程就得在这个临界区中等待,这就是阻塞;非阻塞强调没有一个线程可以妨碍其他线程执行
  • 死锁、饥饿、活锁
    • 死锁:两个及两个以上的进程互相等待,【哲学家进餐】;总是按照一个全局的固定顺序获取锁,可避免死锁
    • 活锁:多个线程谦让导致无法使用资源
    • 饥饿:某一个或多个线程一直占用资源,使其他需要资源的线程无法执行
  • 并发级别
    • 阻塞: 当一个线程进入临界区之后,其他线程必须等待
    • 无饥饿:非公平锁会导致低优先级的线程饥饿,而公平锁饥饿就不会产生
    • 无障碍:自由出入临界区,无竞争时有限步内完成操作,有竞争时回滚数据
    • 无锁:无障碍,保证有一个线程可以胜出
    • 无等待:无锁,要求所有线程都必须在有限步内完成,无饥饿
  • JMM的特性
    • Atomicity: 一个操作不可中断
    • Visibility: 一个线程修改了某一个变量值,其他线程能否立即获取到这个改动值
    • Ordering: 对于一个线程来说,它内部的执行顺序一定是一致的,多个线程之间涉及到指令重排,所以顺序是没有保障的
  • Happen-before规则:指令重排的原则:
    • 程序顺序原则:一个线程内保证语义的串行性
    • volatile规则:volatile变量的写先发生于读,保证了volatile变量的可见性
    • 锁规则:解锁必然发生在加锁前
    • 传递性:A先于,B先于C,那么A一定先于C
    • 线程的start()方法先于它的每一个动作
    • 线程的所有操作先于线程的终结
    • 线程的中断先于被中断线程的代码
    • 对象的构造函数执行、结束先于finalize()方法
  • synchronized的用法:
    • 指定加锁对象
    • 作用于实例方法
    • 作用于静态方法

多线程-同步控制

  • 重入锁 java.util.concurrent.locks.ReentrantLock,又名递归锁
    • 中断响应,lock.lockInterruptibly()优先响应中断
    • 锁申请等待时限,lock.tryLock(); lock.tryLock(long time, TimeUnit tu) 给定时间获取锁,如果未指定时间则立即返回
    • 公平锁,new ReentrantLock(boolean fair)
  • Condition:与lock搭配使用,使线程在合适的时间等待,或者在一个合适的时间得到通知继续执行
    • await()会使当前线程等待,并释放当前锁,当其他线程使用signal()或者signalAll()方法时线程或重新得到锁继续执行,或者等待的时候被中断时跳出等待
    • awaitUninterruptibly()await()类似,但不会在等待中响应中断
    • signal()会唤醒一个等待的线程,signalAll()会唤醒所有等待的线程
    • 重入锁+condition在jdk内部广泛使用,如ArrayBlockingQueueput() take()方法
  • 信号量Semaphore:指定多个线程访问一个资源,通过acquire() tryAcqire()获取授权,使用完毕后通过release()释放资源授权;
    • 使用场景:数据库连接数固定,可以采用信号两个让数据库连接得以充分使用
  • ReadWriteLock,读写分离锁可有效减少锁竞争,以提升系统性能;读写锁约束:
    • 读-读:不阻塞
    • 读-写、写-写:阻塞
  • StampedLock:对读写锁的改进,不仅读不阻塞读,读也不阻塞写;在读的时候发生了写,则应当重读而不是阻塞写(读写锁的时候读多写少会发生写饥饿);
    • StampedLock控制锁带有三种模式 乐观读
    • 此控制锁的状态由版本和模式组成,锁获取操作返回一个用于展示和访问锁状态的stamp变量,锁释放以及其他相关方法使用stamp变量作为参数,如果与当前锁状态不符则失败
    • 三种模式之写入:writeLock独占访问阻塞当前线程返回一个stamp变量,当锁被写模式占有,没有读和乐观读能成功
    • 三种模式之读取:readLock 获取访问而阻塞写入,返回一个stamp变量
    • 三种模式之乐观读: tryOptimisticRead返回一个非0 stamp变量,仅在当前锁没有被写入模式持有;validate用于检查乐观读时是否发生了写操作,如果发生则重读(悲观锁)
  • CountDownLatchawait()后等待线程计数完成可继续执行,与join的区别:
    • 假如每个线程的工作有两阶段
    • 第一个线程需要等待第二、第三个线程的第一阶段工作结束时才能开始执行
    • 此时join不能做到但CountDownLatch可以做到,count.countDown()
  • CyclicBarrier: 循环屏障,当所有线程到达屏障(await())之后,被屏障阻塞的线程才会继续执行,并且可重复设置屏障; 场景:
    • 数据合并:多个线程统计各自的数据,统计完毕后,使用barrierAction进行合并
  • LockSupport: 线程阻塞工具,可以在线程内任意位置阻塞,无需提前获取锁。
    • park() 如果许可可用,则消费许可后立即返回;如果许可不可用,则阻塞等待许可;阻塞时可被中断
    • unpark()使许可变为可用,许可不能累加只能有一个

多线程-线程池

在生产环境中线程的数量必须加以管控,否则大量线程轻则拖慢系统速度,重则导致CPU、内存资源耗尽

  • JDK Executor框架

线程池类图

  • Executors 工厂类产生各种线程池
    • newFixedThreadPool() 返回一个固定线程数量的线程池
    • newSingleThreadExecutor()返回一个只有一个线程的线程池
    • newCachedThreadPool()返回一个可根据实际情况调整数量的线程池,优先复用
    • newSingleThreadScheduledExecutor() 返回一个ScheduledExecutor对象,线程池大小为1
    • newScheduledThreadPool()返回指定数量ScheduledExecutor对象的线程池
  • ScheduledExecutorService 根据时间需要对线程进行调度
    • schedule(Runnable cmd, long delay, TimeUnit unit) 给定时间的时候,调度一次
    • scheduleAtFixedRate(Runnable cmd, long initialDelay, long period, TimeUnit unit) 固定频率进行调度,以上次任务开始时间为参照,但任务不会被堆叠,当执行周期过短时,上次任务结束立即会调用本次任务
    • scheduleWithFixedDelay(Runnable cmd, long initialDelay, long delay, TimeUnit unit) 固定延迟进行调度,以上次任务结束时间为参照
  • ThreadPoolExecutor线程池的内部实现:Executors工厂类产生线程池的方法主要是调用:
    • new ThreadPoolExecutor(int coreSize, int maxPoolSize, long keepAlive, TimeUnit unit, BlockingQueue<Runnable> wordQueue, ThreadFactory factory, ThreadPoolExecutor handler)
    • coreSize: 线程池中的线程数量
    • maxPoolSize: 线程池中最大线程池数量
    • keepAlive: 当线程池数量超过coreSize时,多余的空闲线程的存活时间
    • workQueue: 任务队列,被提交但尚未执行的任务
    • factory:线程工厂用于创建线程;
    • handler:拒绝策略,当任务太多时如何拒绝任务
  • 参数workQueue
    • 直接提交队列-SynchronousQueue,没有容量,每一个插入操作都需要等待一个相应的删除操作;提交的任务不会被真实保存,总将新任务提交给线程执行,如果没有空闲线程则创建新的线程,如果线程数已到最大值,则执行拒绝策略,如newCachedThreadPool使用队列
    • 有界任务队列-ArrayBlockingQueue 若线程池中线程实际数量小于coreSize则创建新的线程,如果池中线程数等于coreSize则将任务加入队列,如果队里已满,则在coreSize不小于maxSize的前提下创建新的线程,否则执行拒绝策略,
    • 无界任务队列-LinkedBlockingQueue 若池中线程数量等于coreSize则后续的任务都会被加入到队列中,直到资源耗尽否则任务不会被拒绝(失败),如newFixedThreadPool
    • 优先任务队列-PriorityBolckingQueue 带有优先级的队列,可控制任务执行的先后顺序,是一个特殊的无界队列
  • 拒绝策略:当线程池中的线程不足处理现有任务的时候,会触发任务拒绝策略
    • AbortPolicy:抛出异常,阻止系统工作
    • CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者的线程中执行当前被拒绝的任务
    • DiscardOledestPolicy:丢去最老的一个任务,并提交当前任务
    • DiscardPolicy:丢弃当前任务
    • 如果无法满足则可以自定策略,实现RejectedExecutionHandler
  • 自定义线程池的创建:实现ThreadFactory接口即可,在创建时可以给线程分组、设置优先级、设置名称、设置是否守护线程等
  • jdk线程池扩展:ThreadPoolExecutor提供了beforeExecute()afterExecute()以及terminated()方法
  • 线程池数量:一般考虑CPU数量、内存大小等,最优的线程池数量:n = Ncpu * Ucpu * (1 + W/C)
    • Ncpu:cpu的数量
    • Ucpu:目标CPU的使用率
    • W/C: 等待时间与计算时间的比率
  • Fork/Join框架:分治思想,如MapReduce
    • RecursiveTask<V> 有返回值,超出当前线程处理能力时,进行fork,fork结束后进行join
    • RecursiveAction 无返回值

参考资料:《Java高并发程序设计》

猜你喜欢

转载自blog.csdn.net/u010209217/article/details/78974137