Java并发编程实战学习

极客并发编程实战学习总结

  • 并发程序出现的原因
  • 并发编程bug的源头
  • java内存模型
  • 并发编程需要解决的核心问题
  • 分工
  • 同步和互斥的万能钥匙--管程
  • 互斥锁
  • 同步
  • JAVA线程
  • JUC包中常用的并发工具

并发程序出现的原因

1.计算机系统发展过程中,CPU,缓存和硬盘存在一个核心矛盾--三者的速度差异。
为了合理利用CPU的高性能,平衡三者的速度差异。计算机体系系统,操作系统,编译程序都作出了优化。
1.CPU增加了高速缓存,以平衡和内存进行数据交换的速度差异
2.操作系统,增加了进程,线程,以分时复用CPU
3.编译器优化指令执行顺序,使得缓存能够合理应用
 硬件工程师对CPU和内存的优化,以及编译系统对程序执行顺序的优化,导致程序的执行和我们的预想出现一定的偏差,这也是并发编程出现BUG的源头。

并发编程bug的源头

	可见性:为了提高CPU执行效率,每个线程缓存了它说需要的数据到CPU缓存中,导致一个线程对共享数据的修改,另一个线程无法立即看到。
	有序性:编译优化导致没有前后关联的语句执行顺序被切换,带来的有序性问题。
	原子性:多个CPU指令在执行过程中不可中断及为原子性。在某些业务处理中,我们需要某些操作能够形成一个不可分割的单元,但是线程的切换可能导致执行动作被中断,出现原子性问题。在实际环境中还需要特别注意导致原子性问题的一个场景是:高级编程语言中的一条语句,可能对应多条CPU指令,线程切换可能发生在某个CPU指令完成,一个看似整体的语句,可能被分割成多次执行。

解决原子性问题最常用的手段是synchronized和Java并发包中的一系类锁,我们会在后面介绍到。为了解决可见性和有序新问题,我们需要了解Java内存模型。

java内存模型

为了解决并发中的可见性和有序性问题,引入了java内存模型。
  1. volatile
    volatile关键字作用两个
    - 是保证cpu缓存中的变量能够及时刷新为内存中的最新值
    - 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  2. final
    final关键字的作用其实主要还是在指令优化的时候对重排序进行优化

  3. synchronized
    java默认的互斥关键字,保证同一时刻只能有一个线程访问临界区

  4. Happens-Before原则
    Happens-Before是指前一个操作对后一个操作的可见性
    1)单线程中,前一个操作Happens-befores后续的任意操作
    2)volatile变量规则:对volatile变量的写操作Happens-Befores对 volatile变量的读操作
    3)传递性:A Happens-Before B,且B Happens-Before C,那么A Happens-Before C
    4)一个锁的解锁Happens-Before后续对这个锁的加锁
    5)线程start()原则,A线程start()子线程B,子线程B能够看到主线程在启动子线程B前的操作
    6)线程Join()原则:如果在线程A中,调用线程B的join()并成功返回,那么线程B中的任意操作Happens-Before于该join()操作的返回

    了解java内存模型对我能后面利用java内存模型的特性来解决并发编程的问题有很大的帮助。例如java并发包中锁的状态也是利用一个volatile变量来标记的,因此了解java内存模型对后面的学习极其重要。

并发编程需要解决的核心问题

	分工:如何拆分任务交给线程处理
	同步:解决线程间如何协作
	互斥:同一时刻只能有一个线程访问共享资源

分工

在java中开启多线程最多的场景是用于一个线程处理一个任务,很少有分工后还需要对计算的结果合并在得到一个汇总结果。当然Java也为我们提供了fork-join工具,可以理解为是单机版本的MapReduce。因为应用场景较少,这里不再展开。

同步和互斥的万能钥匙–管程

java参考MESA管程模型来实现线程的互斥和同步。不论是java语言自带的同步互斥原语还是JUC中的Lock、Conditon都只会是管程的MESA模型的实现方式。
下图是MESA管程模型的图示,参考医院看病的流程理解管程比较容易,多个病人(线程)都找医生(共享变量V)看病,看到医生前,需要获得进入就诊室的允许(获取到锁),未得到允许的病人(线程)在某个地方排队等待(入口等待队列),看病过程,医生可能要求一些病人去拍片,一些去查血,那么病人需要到对应的检查的地方排队(加入条件队列),等待检查医生喊号(线程收到了notify通知)。检查完了,再回来看医生,有需要在就诊室外等待医生喊号(重新获取锁的过程)。

Java并发编程实战学习_第1张图片
使用MESA模型需要注意的地方:
1.使用wait/notify/notifyall之前必须获得锁
2.因为MESA管程的特点,调用wait方法的时候必须在while循环中。
while(条件不满足)
{
wait();
}
3.除非特别的有把握,尽量使用notifyall,而不是notify,如果满足一项的三个条件,也可使用notify
(1)所有等待线程拥有相同的等待条件
(2)所有等待线程被唤醒后,执行相同的操作
(3)只需要唤醒一个线程

根据管程模型的特点,实现同步,需要先获取到对应的锁。因此我能先介绍锁相关的知识

互斥锁

java使用锁来解决并发编程中的原子性问题,和互斥问题
锁的实现:
1.synchronized:java提供的默认实现方式
2.JUC包中大量的Lock的实现
ReentrantReadWriteLock
ReentrantLock

使用锁需要注意的问题:
一:受保护资源和锁的对应的关系是:N:1,即一把锁可以保护多个资源,但是不能出现 一个资源用多把锁进行保护
二:死锁问题
死锁产生的原因
1.互斥,共享资源X和Y只能被一个线程占用
2.占有且等待:线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
3.不可抢占:其他线程不能强行抢占线程T1占有的资源
4.循环等待:线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待
避免死锁的方式
避免死锁的发生,互斥无法避免,但是可以破话其他三个导致死锁的原因来避免死锁发生:
1破坏占有且等待:一次申请所需要的资源
2破坏不可抢占:使用JUC包中的Lock,并加上超时时间
3破坏循环等待:申请的资源按照一定的顺序排序,按照排序结果进行资源锁定
三:不可变对象/可重用对象不适合作为锁
String
Integer
Boolean
不可变对象每次设置值会重新构建一个新对象,导致锁和资源的对应关系被破坏。
可重用对象如果经被其他线程加锁并不释放,导致当前线程无法执行

四:使用锁的最佳实践
1.永远只在更新对象的成员变量时加锁
2.永远只在访问可变的成员变量时加锁
3.永远不在调用其他对象的方法时加锁(被调用方法可能和调用方法可能对同一资源进行加锁,导致)

同步

同步的实现方式:
1.wait/notify/notifyall/join java默认的同步实现方式
2.JUC通过Condition对象的await和signal来实现同步
同步需要注意的事情:
1.使用wait/notify/notifyall之前必须获得锁
2.因为MESA管程的特点,调用wait方法的时候必须在while循环中。
while(条件不满足)
{
wait();
}
3.除非特别的有把握,尽量使用notifyall,而不是notify,如果满足一项的三个条件,也可使用notify
4.interrupt标志位
当线程处于WAITING/TIMED_WAITING的时候,其他线程调用线程的interrupt方法会导致当前线程抛出InterruptException异常在触发InterruptedException异常的同时,JVM会同时把线程的中断标志位清除,如果捕获InterruptException异常后还有其他地方需要判断线程是否中断一定要重新设置中断标志位

JAVA线程

用一张图来说明Java生命周期和之前的转换

Java并发编程实战学习_第2张图片
Java线程数量设置
CPU密集型 线程数=1+CPU核数
IO密集型 线程数=CPU核数*[1+(I/O耗时/CPU耗时)]
线程池:使用需要注意的事情1.选用有界对内,2.指定操作队列上线后的线程拒绝策略。

优雅的终止线程:
采用两阶段终止模式,第一阶段:发出终止指令,第二阶段,相应终止指令
首先调用线程的interrupt()方法唤醒睡眠状态的线程,设置自定义的中断标记位,被中断线程在合适的地方检查中断标志位,介绍方法,从而结束线程。使用自定义中断标志位,而不是线程自带的线程中断标志,原因在于我们很可能在线程的run()方法中调用第三方类库提供的方法,而我们没有办法保证第三方类库
正确处理线程的中断异常,如第三方类库在捕获到Thread.sleep()方法抛出的中断异常
后,没有重新设置线程的中断状态,那么就会导致线程不能够正常终止。

JUC包中常用的并发工具

本文不打算介绍JUC具体的实现逻辑,但是还是需要提到的一点是,AQS和CAS是Java JUC包的基石
锁类
ReentrantReadWriteLock
ReentrantLock
同步类
CountDownLatch
CountDownLatch主要用来解决一个线程等待多个线程的场景
CyclicBarrier
CyclicBarrier是一组线程之间互相等待,CyclicBarrier的计数器是可以循环利用的
原子类
基本数据类型
AtomicBoolean
AtomicInteger
AtomicLong
引用类型
AtomicReference
AtomicStampedReference
可以解决ABA问题
AtomicMarkableReference
可以解决ABA问题
数组
对象属性更新器
累加器
LongAdder
只用于累加,性能高于Atomic类
并发容器
List
CopyOnWriteArrayList
写的时候会复制一份数组出来,读完全无锁
Map
ConcurrentHashMap
key无续
ConcurrentSkipListMap
key有序
Set
CopyOnWriteArraySet
ConcurrentSkipListSet
Queue
阻塞队列
ArrayBlockingQueue
有界队列
LinkedBlockingQueue
有界队列
SynchronousQueue
只能放一个元素,生产者必须等消费者消费后才能生产
PriorityBlockingQueue
支持按优先级出队
DelayQueue
支持延迟出队
非阻塞队列
ConcurrentLinkedQueue

你可能感兴趣的:(Java并发编程实战学习)