Java性能优化系列之三--并发程序设计详解

1、并发程序设计模式:

  (1)、Future-Callable模式:FutureTask类实现了Runnable接口,可以作为单独的线程运行,其Run方法中通过Sync内部类调用Callable接口,并维护Callable接口的返回值。当调用FutureTask.get()的时候将返回Callable接口的返回对象。Callable接口是用户自定义的实现,通过实现Callable接口的call()方法,指定FutureTask的实际工作内容和返回对象。Future取得的结果类型和Callable返回的类型必须一致,这是由定义FutureTask的时候指定泛型保证的。Callable要采用ExecutorSevicesubmit方法提交,返回的future对象可以取消任务。

 (2)、Master-Worker格式:其核心思想是系统由两类进程协作工作:Master进程和Worker进程。Master进程负责接收和分配任务,Worker负责处理子任务。当各个子任务处理完成后,将结果返回给Master进程。由Master进程进行归纳会汇总,从而得到系统的最终结果。

  (3)、保护暂停模式:其核心思想是仅当服务进程准备好时,才提供服务。设想一种场景,服务器会在很短时间内承受大量的客户端请求,客户端请求的数量可能超过服务器本身的即时处理能力。为了不丢弃任意一个请求,最好的方式就是将这个客户端进行排列,由服务器逐个处理。

  (4)、不变模式:为了尽可能的去除这些由于线程安全而引发的同步操作,提高并行程序性能 ,可以使用一种不可变的对象,依靠对象的不变性,可以确保在没有同步操作的多线程环境中依然保持内部状态的一致性和正确性。

  (5)、Java实现不变模式的条件:

       1)、去除setter方法以及所有修改自身属性的方法。

       2)、将所有属性设置为私有,并用final标记,确保其不可修改。

       3)、确保没有子类可以重载修改它的行为。

       4)、有一个可以创建完整对象的构造函数。

    Java中,不变模式的使用有:java.lang.String类。以及所有的元数据类包装类。

(6)、生产者-消费者模式:生产者进程负责提交用户请求,消费者进程负责具体处理生产者进程提交的任务。生产者和消费者之间通过共享内存缓冲区进行通信。通过Java提供和饿BlockingQueue可以实现生产者消费者模式。

2、JDK多任务执行框架:

 (1)、简单线程池实现:线程池的基本功能就是进行线程的复用。当系统接受一个提交的任务,需要一个线程时,并不着急立即去创建进程,而是先去线程池查找是否有空余的进程,若有则直接使用线程池中的线程工作。如果没有,则再去创建新的进程。待任务完成后,不是简单的销毁进程,而是将线程放入线程池的空闲队列,等待下次使用。使用线程池之后,线程的创建和关闭通常由线程池维护,线程通常不会因为会执行晚一次任务而被关闭,线程池中的线程会被多个任务重复使用。

 (2)、Executor框架:Executor框架提供了创建一个固定线程数量的线程池、返回一个只有一个线程的线程池、创建一个可根据实际情况进行线程数量调整的线程池、可调度的单线程池以及可变线程数量的可调度的线程池。

 (3)、自定义线程池:使用ThreadPoolExecutor接口:ThreadPoolExecutor的构造函数参数如下:

corePoolSize :指的是保留的线程池大小

maximumPoolSize: 指的是线程池的最大大小

keepAliveTime :指的是空闲线程结束的超时时间

Unit: 是一个枚举,表示 keepAliveTime 的单位

workQueue: 表示存放任务的队列。

ThreadFactory:创建线程的时候,使用到的线程工厂

handler: 当线程达到最大限制,并且工作队列里面也已近存放满了任务的时候,决定如何处理提交到线程池的任务策略

    上述的几种线程池的内部实现均使用了ThreadPoolExecutor接口。我们可以自定义提交但是未被执行的任务队列被执行的顺序,常见的有直接提交的队列、有界的任务队列、*的任务队列、优先任务队列,这样可以在系统繁忙的时候忽略任务的提交先后次序,总是让优先级高的任务先执行。使用优先队列时,必须让target实现Comparable接口。

(4)、优化线程池大小:NThreads=Ncpi*Ucpu*(1+W/C)Java中使用:Runtime.getRuntime().availableProcesses()获取可用的CPU数量。

3、JDK并发数据结构:

  (1)并发ListVector或者CopyOnWriteArrayList是两个线程安全的List实现。

CopyOnWriteArrayList很好的利用了对象的不变性,在没有对对象进行写操作之前,由于对象未发生改变,因此不需要加锁。而在试图改变对象的时候,总是先获得对象的一个副本,然后对副本进行修改,最后将副本写回。CopyOnWriteArrayList适合读多写少的高并发场合。而Vector适合高并发写的场合。

2)并发SetsynchronizedSet适合高并发写的情景、CopyOnWriteSet适合读多写少的高并发场合。

3)并发MapConcurrentHashMap是专门为线程并发而设计的HashMap,它的get操作是无锁的,其put操作的锁粒度小于SynchronizedHashMap,因此其整体性能优于SynchronizedHashMap

4)并发Queue:在并发队列上,JDK提供了两种实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列。如果需要一个能够在高并发时,仍能保持良好性能的队列,可以使用ConcurrentLinkedQueue对象。而BlockingQueue的主要适用场景就是生产者消费者模式中的实现数据共享。BlockingQueue接口主要有两种实现:ArrayBlockingQueue是一种基于数组的阻塞队列实现,也就是说其内部维护着一个定长数组,用于缓存队列中的数据对象。LinkedBlockingQueue则使用一个链表构成的数据缓冲队列。

4、并发控制方法:

1)、Java中的内存模型与Volatile:在Java中,每一个线程有一块工作内存区,其中存放着被所有线程共享的主内存中的变量的值的拷贝。当线程执行时,它在自己的内存中操作变量。为了存取一个共享的变量,一个线程通常要先获取锁定并且清除它的内存缓冲区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区;当线程解锁时保证该工作内存区中变量的值写回到共享内存中。

2)、Volatile关键字:声明为Volatile的变量可以做以下保证:

      1)、其他线程对变量的修改,可以随即反应在当前进程中。

      2)、确保当前线程对Volatile变量的修改,能随即写回到共享主内存中,并被其他线程所见

      3)、使用Volatile声明的变量,编译器会保证其有序性。

      4)、doublelong类型的非原子处理:如果一个double类型或者long类型的变量没有被声明为volatile类型,则变量在进行readwrite操作的时候,主内存会把它当成两个32位的read或者write操作。因此,在32为操作系统中,必须对double或者long进行同步

    原因在于:使用Volatile标志变量,将迫使所有线程均读写主内存中的对应变量,从而使得Volatile变量在多线程间可见。

(3)、同步关键字-Synchronized,其本质是一把锁:Synchronized关键字可以作用在方法或者代码块中。当作用的是成员方法时,默认的锁是该对象this,这个时候一般在共享资源上进行Synchronized操作。该关键字一般和wait()和notify()方法一起使用,调用这两个方法的时候一般指的是资源本身。由于所有的对象都能当成资源,因此这两个方法是从Object继承而来的,而不是Thread或者Runnable才具有的方法。

(4)、ReentrantLock锁:比Synchronized的功能更强大,可中断、可定时。所有使用内部锁实现的功能,都可以使用重入锁实现。重入锁必须放入finally块中进行释放,而内部锁可以自动释放。 重入锁有着更强大的功能,比如提供了锁等待时间(boolean tryLock(long time.TimeUnit unit))、支持锁中断(lockInterruptibly())和快速锁轮询(boolean tryLock())以及一套Condition机制,这个机制类似于内部锁的wait()notify()方法。

(5)、ReadWriteLock:读写分列锁。如果 系统中读操作次数远远大于写操作,而读写锁就可以发挥巨大的作用。

(6)Condition对象:await()方法和signal()方法。Condition对象需要和重入锁(ReentrantLock)配合工作以完成多线程协作的控制。

(7)Semaphore信号量:信号量为多线程写作提供了更为强大的控制方法。广义上讲,信号量是对锁的扩展。无论是内部锁(Synchronized)还是重入锁(ReentrantLock),一次都只允许一个进程访问一个资源。而信号量却可以指定多个线程同时访问某一个资源。

(8)ThreadLocal线程局部变量:ThreadLocal是一种多线程间并发访问变量的解决方案。与synchronized等加锁方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此并不是一种数据共享的解决方案。

5、同步工具类:

 (1CountDownLatch(闭锁):确保一个服务不会开始,直到它依赖的其他服务都准备就绪。CountDownLatch作用犹如倒计时计数器,调用CountDownLatch对象的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。比如有10个运动员的田径比赛,,有两个裁判ABA在起点吹哨起跑,B在终点记录记录并公布每个运动员的成绩。刚开始的时候,运动员们都趴在跑道上(A.await())等到裁判吹哨。A吹哨耗费了5秒,此时调用A.countDown()方法将等待时间减为4秒。当减为0的时候,所有的运动员开始起跑。这个时候,B裁判开始工作。启动一个初始值为10的定时器,每当有一个运动员跑到重点的时候,就将计数器减一,代表已经有一个运动员跑到终点。当计时器为0的时候,代表所有的运动员都跑到了终点。此时可以根据公布成绩了。

 (2CylicBarrier(关卡):

      1)、类似于闭锁,它们能够阻塞一组线程直到某些事件发生

      2)、与同步锁的不同之处是一个可以重用,一个不可以重用

     3)、所有线程必须同时到达关卡点,才能继续处理。

    类似组团旅游,导游就是一个关卡。表示大家彼此等待,大家集合好后才开始出发,分散活动后又在指定地点集合碰面,这就好比整个公司的人员利用周末时间集体郊游一样,先各自从家出发到公司集合后,再同时出发到公园游玩,在指定地点集合后再同时开始就餐。

   (3Exchanger:使用在两个伙伴线程之间进行数据交换,这个交换对于两个线程来说都是安全的。

   讲解Exchanger的比喻:好比两个毒贩要进行交易,一手交钱、一手交货,不管谁先来到接头地点后,就处于等待状态了,当另外一方也到达了接头地点(所谓到达接头地点,也就是到到达了准备接头的状态)时,两者的数据就立即交换了,然后就又可以各忙各的了。

exchange方法就相当于两手高高举着待交换物,等待人家前来交换,一旦人家到来(即人家也执行到exchange方法),则两者立马完成数据的交换。

5、关于死锁:

(1)死锁的四个条件:

   1)、互斥条件:一个资源只能被一个线程使用:

   2)、请求与保持条件:一个线程因请求资源而阻塞时,对已获得则资源保持不放。

   3)、不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺。

   4)、循环等待条件:若干个线程已经形成了一种头尾相接的循环等待资源关系。

(2)常见的死锁:静态顺序死锁、动态顺序死锁、协作对象间的死锁、线程饥饿死锁。

(3)如何尽量避免死锁:

1)、制定锁的顺序,来避免死锁

2)、尝试使用定时锁(lock.tryLock(timeout)

3)、在持有锁的方法中进行其他方法的调用,尽量使用开放调用(当调用方法不需要持有锁时,叫做开放调用)

4)、减少锁的持有时间、减小锁代码块的粒度。

5)、不要将功能互斥的Task放入到同一个Executor中执行。
6代码层面对锁的优化机制:

    1、避免死锁

    2、减少锁持有时间,代码块级别的锁,而不是方法级别的锁

    3、减小锁粒度,ConcurrentHashMap分段加锁

    4、读写锁代替独占锁

    5、锁分离,例如LinkedBlockingQueue的尾插头出的特点,用两把锁(putLock takeLock)分离两种操作。

    6、重入锁和内部锁

   重入锁(ReentrantLock)和内部锁(Synchronized):所有使用内部锁实现的功能,都可以使用重入锁实现。重入锁必须放入finally块中进行释放,而内部锁可以自动释放。

   重入锁有着更强大的功能,比如提供了锁等待时间(boolean tryLock(long time.TimeUnit unit))、支持锁中断(lockInterruptibly())和快速锁轮询(boolean tryLock())以及一套Condition机制,这个机制类似于内部锁的wait()notify()方法。

    7、锁粗化:虚拟机在遇到一连串连续的对同一个锁不断进行请求和释放从操作的时候,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数。

7Java虚拟机层面对锁的优化机制:

1、自旋锁:由于线程切换(线程的挂起和恢复)消耗的时间较大,则使线程在没有获得锁时,不被挂起,而转而执行一个空循环。在若干空循环后,线程如果获得了锁,而继续执行,若线程依然不能获得锁,而才被挂起。

2、锁消除:JVM通过对上下文的扫描,去除不可能存在共享资源竞争的锁,这样可以节省毫无意义的请求锁时间。比如单线程中或者非共享资源的常使用的StringBufferVector

3、锁偏向:若某一个锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需进行相关的同步操作,从而节省了操作时间。

8Java无锁实现并发的机制:

1)非阻塞的同步/无锁:ThreadLocal,让每个进程拥有各自独立的变量副本,因此在并行计算时候,无须相互等待而造成阻塞。CVS算法的无锁并发控制方法。

2)原子操作:java.util.concurrent.atomic包。

==================================================================================================================

最后附上这篇博客中涉及到的Java并发编程中的类的思维导图,以便大家理清思路:

Java性能优化系列之三--并发程序设计详解

Java性能优化系列之三--并发程序设计详解,布布扣,bubuko.com

Java性能优化系列之三--并发程序设计详解

上一篇:linux中shell条件表达式总结


下一篇:Java性能优化系列之四--Java内存管理与垃圾回收机制详解