Java并发编程的艺术-笔记1

来源:互联网 发布:芈十四知乎 编辑:程序博客网 时间:2024/05/16 12:24

并发编程的艺术-笔记1


并发编程的挑战

线程有创建和上下文切换的开销,所以并发未必比串行的执行速度快。
vmstat可以测量上下文切换的次数。
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

避免死锁:
1.避免一个线程获得多个锁
2.避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
3.尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
4.对于数据库锁,加锁和解锁必须在一个数据库链接里,否则会出现解锁失败的情况。


并发机制的底层

可见性:当一个线程修改一个共享变量时,另一个线程能读到这个修改了的值。
锁的语义决定了临界区(锁内部的区域)代码的执行具有原子性

volatile变量使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起上下文切换和调度,一个字段被声明成volatile,Java内存模型保证所有线程看到这个变量的值是一致的。
volatile变量的特性:
1)可见性:对于一个volatile的读,总能看到(任意线程)对这个volatile最后的写入。
2)原子性:对任意单个volatile的读/写具有原子性,但类似voatile++这种复合操作不具有原子性

volatile关键字(汇编角度)两条实现原则:
1.Lock前缀指令会引起处理器缓存回写到内存
2.一个处理器的缓存回写到内存会导致其他处理器(CPU)的缓存无效,下次访问相同内存地址时,处理器强制执行缓存行填充。(多处理器下的缓存一致性协议)

synchronized关键字:
对于普通同步方法,锁是当前实例对象
对于静态同步方法,锁是当前类的Class对象
对于同步方法块,锁是synchronized括号里配置的对象

处理器实现原子操作的方式:总线锁、缓存锁

Java实现原子操作的方式:
1.CAS:比较旧值是否发生了变化,没有发生变化才交换新值,否则不交换。
2.CASsh实现原子操作存在的问题:
(1)ABA问题:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有变化,实际上却是变化了的。ABA的解决思路是使用版本号,每次更新时把版本号加1。Atomic包的AtomicStampedReference类可以解决ABA问题。
(2)循环时间长,开销大。
(3)只能保证一个共享变量的原子操作。有个取巧的方法是:使用时可以将多个共享变量合并成一个共享变量。
3.使用锁机制实现原子操作。只有获得锁的线程才能够操作锁定的内存区域。


Java内存模型

线程间的通信机制有两种:共享内存和消息传递。Java并发采用的是共享内存模型。

Java内存模型(JMM)决定一个线程对共享变量的写入何时对另一个线程可见。

JMM定义了线程和主内存间的抽象关系:线程之间的共享变量存在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
JMM通过控制主内存与每个线程的本地内存之间的交互,为Java程序提供可见性的保证。

重排序:执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序
1)编译器优化的重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应的机器指令执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

JMM属于语言级的内存模型,它确保不同编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

现代处理器都会使用写缓冲区,因此现代的处理器都会允许写-读操作进行重排序。

happens-before关系:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,保证正确同步的多线程程序的执行结果不被改变。

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。 编译器和处理器在重排序时,会遵守数据依赖性。不改变这两个操作的执行顺序,但他们不考虑不同处理器之间和不同线程之间的数据依赖性。

as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不能被改变。

多线程程序中对存在控制依赖的操作重排序,可能会改变程序的执行结果。

顺序一致性:
程序未正确同步,就会存在数据竞争。
顺序一致性模型:
1)一个线程中的所有操作必须按照程序的顺序来执行
2)所有线程都只能看到一个单一的操作执行顺序。
在顺序一致性模型中,每个操作都必须原子执行且立刻对所有线程可见。

volatile写-读的内存语义:
1)当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
2)当读一个volatile变量时,JMM会把该线程对应的本地内存无效。线程接下来将从主内存中读取共享变量。
volatile的内存语义是通过在写操作前后和读操作之后插入内存屏障来实现的,内存屏障禁止了读写操作的重排序。

锁的获取/释放内存语义和volatile读/写的内存语义相同。

volatile只保证对单个变量的读写具有原子性,锁的互斥执行的特性可以确保对临界区代码的执行具有原子性。

concuurrent包的实现机制:
1.声明共享变量为volatile
2.使用CAS的原子条件更新来实现线程之间的同步
3.配合以volatile的读写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

final内存语义:
1.final域的重排序规则:
1)构造器内对一个final域的写入,和之后把构造的对象的引用赋值给一个引用变量,这两个操作不能重排序。 对于引用变量,是对final引用的成员域的写入起作用。
2)初次读一个包含final域的对象的引用,和之后初次读final域,这两个操作不能重排序。

2.写final域的重排序规则:
1)JMM禁止编译器把final域的写重排序到构造器外.
2)编译器会在final域的写入之后,构造器return之前插入一个内存屏障,以禁止处理器器把final域的写重排序到构造器外。

3.读final域的重排序规则:
在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个内存屏障。
4.final引用保证在构造器返回后,任意线程都将能保证能看到final域正确初始化后的值;
final关键字不能保证包含final字段的对象本身的可见性;
final关键字不会导致上下文切换。

延迟初始化:推迟一些高开销的对象初始化操作
多线程环境下,使用双重检查锁定来实现延迟初始化需要考虑重排序问题。解决方案:
1.基于volatile的解决方案:将需要延迟初始化的对象声明volatile以禁止对象初始化时的重排序。
2.基于类初始化的解决方案:原理:JVM在执行类的初始化阶段(即Class被加载后,且被线程使用之前)期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

原创粉丝点击