《Java并发编程的艺术》读书笔记

最近在进一步学习Java并发编程,不言而喻,这部分内容是很重要的。现在就以《并发编程的艺术》一书为主导线,开始新一轮的学习。

Java 并发编程的艺术PDF清晰完整版+源码 

进程和线程

进程是一个应用程序在处理机上的一次执行过程,线程是进程的最小基本单位(个人理解)。一个进程可以包含多个线程。

上下文切换

我们都知道,即使是单核处理器也支持多线程,CPU通过时间片分配算法来给每个线程分配时间让线程得以执行,因为时间片非常短,所以在用户角度来讲,会感觉多个线程是在同时执行。那什么是上下文切换呢?举个例子,当线程A执行到某一步时,此时CPU将时间让给了线程B进行执行,那么在进行切换的时候,系统一定要保存此时此刻线程A所执行任务的状态,比如执行到哪里、运行时的参数等,那么当下一次CPU将时间让给线程A进行执行时,才能正确的切换到A,并继续执行下去。所以任务从保存到再加载的过程就是一次上下文切换。

虽然上下文切换可以让我们觉得可以“同时”做很多事,但是上下文切换也是需要系统开销的。在《Java并发编程的艺术》中,作者举例演示了串行和并发执行累加操作,在结果中可以看得出,累加操作不同的次数会对不同的结果,所消耗的时间也有差别的。如果累加操作的次数没有超过百万次,那么串行执行结果消耗的时间会比并行执行的时间要少。所以在有些情况下我们需要尽可能的减少上下文切换的次数,使用的方法有:无锁并发编程,CAS算法,使用最少线程和使用协程。(这里笔者也只知道有这几种方法,至于具体如何使用以及在何种场景下使用还未深入研究)。

volatile与synchronized volatile

volatile是轻量级的synchronized,它保证了在多处理器开发中,共享变量的可见性,并且volatile不会引起上下文切换和调度。可见性的意思是当一个线程修改了某个变量的值,另外一个线程可以读到这个变量修改后的值,如果一个变量被volatile修饰,那么Java内存模型确保所有线程看到这个变量的值是一致的。

synchronized

Java中每一个对象都可以作为锁,具体表现为:

对于普通的同步方法,锁是当前实例对象

对于静态的同步方法,锁是当前类的Class对象

对于同步方法块,锁是synchronized括号里配置的对象

当一个线程访问同步代码块时,必须要先得到锁,退出或抛出异常时,必须释放锁。对于上述三种情况,表现形式为:

1 /** 2 * 普通同步方法,锁是当前实例对象 3 */ 4 public synchronized void test1(){ 5 //TODO something 6 } 7 8 /** 9 * 静态同步方法,锁是当前类的Class对象 10 */ 11 public static synchronized void test2(){ 12 //TODO something 13 } 14 15 /** 16 * 同步方法块,锁是synchronized括号中的对象,这里是a 17 */ 18 public void test3(Integer a){ 19 synchronized (a){ 20 //TODO something 21 } 22 }

Java内存模型

Java中所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。

Java线程之间的通信由Java内存模型(JMM)控制。JMM定义了线程和主内存的关系:线程之前的共享变量存储在主内存中,每个线程都有一个私有的本地内存(也叫工作内存),本地内存中存储了该线程读写共享变量的副本。本地内存是JMM的抽象概念,不真实存在,包涵了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。Java内存模型结构图: 

《Java并发编程的艺术》读书笔记

从上图可以看出,线程A要与线程B进行通信的话,必须要经过两个步骤:

线程A把本地内存A中更新过的共享变量刷新要主内存中去,

线程B到主内存中获取更新之后的共享变量。

如下图:

《Java并发编程的艺术》读书笔记

重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

数据依赖性

定义:如果两个操作同时访问一个变量,且这两个操作中有一个为写操作。此时这两个操作之间就存在数据依赖性。

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

as-if-serial语义

语义:不管怎么重排序,单线程程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/13823.html