张伦聪的技术博客 Research And Development

读《java高并发程序设计》后感

2018-05-01

第三章 jdk并发包

cas 比较交换

它包含三个参数cas(v,e,n).v表示要更新的变量,e表示预期值,n表示下一个新值。仅当v==e时,才会将v的值设为n,如果v!=e,则说明其他线程做了更新,则当前线程说明都不做,执行下一次循环。最后cas返回当前v的真实值。如AtomicInteger的实现。

juc并发包下的重入锁ReentrantLock较synchronized有什么优点?

1.中断响应,如果一个线程中等待锁,接受通知可以中断,以防止无需等待,产生死锁。2.锁申请等待限时,除了等待外部通知外,也可以限时等待,超时让线程自动放弃。3.公平锁,synchronized进行控制的都是非公平锁,使用ReentrantLock可以使用公平锁/非公平锁。

信号量Semaphore

允许多个线程同时访问,可以控制同时有几个线程能够访问

读写锁ReadWirteLock

读写分离,读读不互斥,读写互斥,写写互斥

倒计时器CountDownLatch

可以等待指定的如10个线程都完成任务,然后再do some thing

循环栅栏CyclicBarrier

可以等等指定的如10个线程都准备就绪,然后再执行任务,再等待所有线程都执行完成,然后再do some thing

线程阻塞工具类LockSupport

它可以在线程内任意位置让线程阻塞,和suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。

线程池

Executor框架提供了各种类型的线程池,主要有以下工厂方法:

public static ExecutorService newFixedThreadPool(int nThreads);
public static ExecutorService newSingleThreadExecutor();
public static ExecutorService newCachedThreadPool();
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
public static ScheduledExecutorService newSingleThreadScheduledExecutor();

newFixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

newSingleThreadExecutor:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

newCachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

newScheduledThreadPool:该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。

newSingleThreadScheduledExecutor:该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。

虽然轮子造好了,但是根据《阿里巴巴Java开发手册》,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:executors返回线程池对象的弊端:

1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

所以还是自己动手丰衣足食,先来看看核心线程池的内部实现。在ThreadPoolExecutor类中提供了四个构造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

**构造器中各个参数的含义: **

  • corePoolSize:指定了线程池中的线程数量

  • maximumPoolSize:指定了线程池中的最大线程数量

  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间,即超过corePoolSize的空闲线程,在多长时间内会被销毁

  • unit:参数keepAliveTime的时间单位,有7种取值。TimeUnit.DAYS、TimeUnit.HOURS、TimeUnit.MINUTES、TimeUnit.SECONDS、TimeUnit.MILLISECONDS、TimeUnit.MICROSECONDS、TimeUnit.NANOSECONDS

  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。

  • threadFactory:线程工厂,主要用来创建线程;

  • handler:表示当任务太多来不及处理,拒绝处理任务时的策略,有以下四种取值: ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

jdk的并发容器

aqs(AbstractQueuedSynchronizer)

fifo双向链表队列+锁+cas.我们先来简单描述下AQS的基本实现,前面我们提到过,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为”CLH”队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。

ConcurrentHashMap

内部分段,由多个HashMap组成,一般默认分为16个段,每个段一个HashMap,然后对每个段进行加锁。减小了锁的粒度,提升效率。但是做size操作性能还是差于普通HashMap。

CopyOnWriteArrayList

在写操作时,进行一次自我复制,将修改的内容写入副本,写完之后再将修改完的副本替换原来的数据。这样就不会影响读。适合读操作多于写操作的场景,提升读性能。

ConcurrentLinkedQueue

非阻塞队列,链表+cas。非分别有head指针指向队列链表头,tail指针指向队列链表尾。offer方法,使用cas来保证线程安全问题。

BlockQueue:阻塞队列

锁+生产者消费者模式。共享通道,ArrayBlockingQueue适合做有界队列,因为数组动态拓展不方便,队列中容纳的最大元素需要在队列创建时指定。LinkedBlockingQueue适合做无界队列,put和take方法根据标志等待,offer和poll不等待。

SkipList 跳表,随机数据结构

是一种可以用来快速查找的数据结构,有点类似平衡数,但是平衡数的插入和删除可能会导致整个平衡数进行一次全局的调整,并且需要全局锁来保证整个平衡树线程安全。跳表只需要调整局部即可,和加局部锁。跳表的本质是维护来多个链表,并且链表是分层的。最底层的链表维护了所有的元素,每上面一层链表都是下面一层的子集。一个新元素插入跳表完全是随机,使用随机算法插入。最底层肯定会插入,上层随机是否插入。跳表明显是一种使用空间换时间的算法。

第四章 锁的优化及注意事项

从程序的角度

1.减小锁持有时间,不必要的代码移到锁外

2.减小锁粒度,类似ConcurrentHashMap分成很多分段锁

3.读写锁分离,类似可重入读写锁

4.锁分离,读写锁的进一步延伸,类似LinkedBlockQueue的take和put方法分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个操作都对当前队列进行修改,但是基于链表在前端和尾端进行,互相不影响。

5.锁粗化,减少频繁获取锁,释放锁的时间,合并代码。

从jvm的角度

1.锁偏向,偏向第一个获取锁的线程

2.轻量级锁,使用cas来实现

3.自旋锁,类似cas进行忙循环获取锁

4,锁消除,基于逃逸分析,判断变量是否会逃逸出某一个作用域,如果不逃逸,可以将锁消除,因为不会出现其他线程操作该变量的情况,是局部变量。


如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏博主

留言