《Java并发编程的艺术》一

169 阅读5分钟

第一章-并发编程的挑战

进行并发编程,一方面会有提高代码执行效率的优点,另一方面,也会面临一系列的挑战,如果处理不当,会导致并发编程的执行效率反而低于顺序执行。

1.上下文切换

操作系统中有个时间片的概念,通过设定时间片,不断切换OS中不同进程之间的执行。对于线程而言也一样,OS通过分配给每个线程一个时间片,当线程执行到达时间片之后,阻塞该线程,保存当前线程的运行状态以及运行地址,然后运行其他线程。OS通过时间片轮换算法,调度多个线程之间的执行切换。当一个线程共被阻塞到重新开始执行,这个过程被成为上下文切换。上下文切换会有一定的切换开销,因此为了效率问题,要尽量避免上下文切换带来的额外开销。

避免上下文切换的主要方法有:无锁并发编程CAS算法使用最少线程以及使用协程

2.死锁

死锁的概念,简单来说,就是相互等待。造成死锁最少需要两个参与者才能形成,每个参与者都等待着被其他参与者占用的资源,才能进行下一步执行,最后的状态就是每个参与者的执行都无法进行,陷入死等。

产生死锁的四个必要条件:

1. 互斥条件:一个资源每次只能被一个进程使用。

2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

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

4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁,只需要破坏这四个条件中任何一个即可。

3.资源受限

理想状态下,当一个进程分为10个线程并发跑,其执行时间应该大概接近原来执行时间的1/10【考虑上下文切换的开销】。但实际情况并非如此,多线程并发执行的效率还会受物理资源的限制。比如每个线程需要10M/s的网络带宽,但此时网络资源只有20M/s,即使开了10个线程,也只能每次并发执行两个线程。

对于资源受限问题,我们需要对症下药,比如将程序放在Hadoop上跑,资源管够,或者放在多机上跑等。

第二章-Java并发机制的底层实现原理

简述:Java代码编译后,会编程Java字节码,字节码被类加载器加载到JVM中,JVM执行字节码,转化为汇编指令,在CPU上执行。因此,Java并发机制依赖于JVM的实现和CPU指令。

这里我们了解下Java并发编程中常用的两个关键字:volatile和synchronized。

1.volatile

简述:volatile可以说是轻量级的synchronized。volatile的功能:在多处理器开发中保证了共享变量的“可见性”。即一个线程修改共享变量,另一个线程可以直接读到修改后的变量的值。volatile 不会引起上下文切换,而synchronize会,因此在资源使用上,volatile成本更低。

举个例子:

instance = new Singleton();//instance是一个volatile变量

转换为汇编代码为:

0x01a3d20ed: movb $0x0,0x1104800 (%esi); 
0x01a3d23ed: lock addl $0x0, (%esi);

简单理解一下lock命令在汇编指令中,会做的两件事情:

1.将当前处理器缓存行回写到系统缓存中

2.该回写操作会让其他CPU里缓存了该内存地址的数据无效。

有1和2的保障,会是该变量在变更后,其他线程读取该变量时,高速缓存中的内存地址的数据显示无效,从而会去内存中读取最新的变量的值。由此,可以在多线程中通过volatile字段标识一个字段是否可被其他线程“可见”

2.synchronized

synchronized的原理:Java中的每个对象都可以作为锁,主要有三种形式:普通同步方法,直接锁该方法所属的实例对象;静态同步方法,锁的是当前类的Class对象;同步方法块,锁的是方法块中的对象。

从JVM实现规范可以看出,JVM是基于进入和退出Monitor对象来实现方法和方法块的同步。代码块同步是使用monitorenter和monitorexit指令实现的,前者在编译后插入到同步代码块的开始位置,后者则是插入到结束位置或者异常处。任何一个对象都会有对应的monitor对象,当monitor被持有后,该对象就处理被锁住的状态。当代码执行到monitorenter指令时,就会尝试获取monitor对象。

synchonized用的锁,是放在Java对象头中的。Java对象将在第三章详细介绍。

Java SE1.6为了减少获得锁和释放锁带来的性能损耗,引入“偏向锁”和“轻量级锁”的概念。按照量级从低到高依次为:无状态锁、偏向锁、轻量级锁和重量级锁四种。锁可以升级,但不能降级。


3.原子操作

原子操作即为“不可中断的一个或一系列操作”。我们看下CPU如何实现原子操作:

32为IA32处理器通过给缓存加锁或者总线加锁来实现多处理器之间的原子操作。

1.总线加锁

处理器提供一个总线苏#LOCK,当一个处理器在总线上发出这个信号时,其他处理器的请求将被阻塞,测试该处理器将独占共享内存。

2.缓存加锁

主要使用缓存一致性机制来保证。

3.Java如何实现原子操作:循环使用CAS实现原子操作/用锁

CAS实现原子操作的三大问题:

1)ABA问题

2)循环时间长,开销大

3)只能保证一个共享变量的原子操作