Java性能优化二:并发程序设计优化,开发必备优化技巧!

一、JDK 并发数据结构:

1、并发 List : Vector 或者 CopyOnWriteArrayList 是两个线程安全的 List 实现。

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

2、并发 Set : synchronizedSet 适合高并发写的情景、 CopyOnWriteSet 适合读多写少的高并发场合。

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

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

二、并发程序设计模式:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

四、同步工具类:

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

2 、 CylicBarrier (关卡):

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

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

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

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

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

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

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

五、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 数量。

六 、并发控制方法:

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

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

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

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

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

    d、 double 和 long 类型的非原子处理:如果一个 double 类型或者 long 类型的变量没有被声明为 volatile 类型,则变量在进行 read 和 write 操作的时候,主内存会把它当成两个 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 完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此并不是一种数据共享的解决方案。

七、关于死锁:

1、死锁的四个条件:

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

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

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

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

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

3、如何尽量避免死锁:

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

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

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

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

e、不要将功能互斥的 Task 放入到同一个 Executor 中执行。

八 、 代码层面对锁的优化机制:

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

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

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

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

好了,本篇文章就分享到这里了。还有一部分代码优化,将会在下一篇文章讲到。有兴趣的新手伙伴们可以关注收藏起来,以后需要的时候可以多看看。如果有正在学java的程序员,可来我们的java技术学习扣qun哦:59789,1510里面免费送java的视频系统教程!

猜你喜欢

转载自blog.csdn.net/weixin_43660525/article/details/85685147
今日推荐