Java并发编程

互联网的快速发展,Java开发的过程或多或少会需要进行并发编程,也会遇到一些并发编程带来的各种bug。下面从并发编程的理论、并发工具类、并发设计模式、并发模型案例,记录一下自己的学习历程。
1.并发编程理论

并发编程的来源于缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题。Java语言规范引入了Java内存模型,通过定义多项规则对编译器和处理器进行限制,主要是针对可见性和有序性。主要是通过volatile、synchronized 和 final 三个关键字,以及Happens-Before 规则。
(1)锁,锁操作是具备happens-before关系的,解锁操作happens-before之后对同一把锁的加锁操作。实际上,在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。
(2)volatile字段,volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性,其性能往往是优于锁操作的。但是,频繁地访问 volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。
(3)final修饰符,final修饰的实例字段则是涉及到新建对象的发布问题。当一个对象包含final修饰的实例字段时,其他线程能够看到已经初始化的final实例字段,这是安全的。
Happpens-Before规则:
(1)程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
(2)管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而"后面"是指时间上的先后顺序。
(3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的"后面"同样是指时间上的先后顺序。
(4)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
(5)线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
(6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
(7)对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
Java内存中的原子性是通过管程synchronized来实现互斥性,保证操作的原子性。Java中synchronized有一个隐形规则:当修饰静态方法的时候,锁定的是当前类的 Class 对象;当修饰非静态方法的时候,锁定的是当前实例对象 this。
当然synchronized一次只能锁定一个对象,怎么通过通过锁来锁定多个资源?当然我们可以将需要进行的动作一次锁定,可以通过锁定当前类或静态方法。但是这样锁的颗粒度大,造成程序性能低。用不同的锁对受保护资源进行精细化管理,能够提升性能。当然这样又会造成死锁的情况出现。要避免死锁就需要分析死锁发生的条件,有个叫 Coffman 的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁:
(1)互斥,共享资源 X 和 Y 只能被一个线程占用;
(2)占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
(3)不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
(4)循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
等待 - 通知机制是一种非常普遍的线程间协作的方式,notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程,所以除非经过深思熟虑,否则尽量使用 notifyAll()。

CPU 密集型计算:
对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率
I/O 密集型:
最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
2.并发工具类

3.并发设计模式

4.并发模型案例

完整版参考:https://www.jianshu.com/p/d417684f4f59

posted @ 2020-02-01 23:19  Johar  阅读(209)  评论(0编辑  收藏  举报