1 并发编程面临的问题
1.1 上下文切换
- 实现: CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,时间片一般是几十毫秒(ms)
- 上下文切换也会影响多线程的执行速度
- 测试工具
- Lmbench3 可以测量上下文切换的时长。
- vmstat可以测量上下文切换的次数
- 如何减少上下文切换
- 无锁并发编程
- CAS算法
- 使用最少线程
- 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
1.2 死锁
- 避免死锁的几个常见方法
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
- 死锁产生的4个必要条件
- 互斥条件
- 资源是独占,且排他使用。
- 不可剥夺条件
- 进程获取到资源,在使用完毕之前,不能被其他进程强行剥夺,只能由持有该资源的进程释放。
- 请求和保持条件
- 一个进程因请求资源阻塞时,对已获得的资源保持不放。
- 循环等待条件
- 若干进程之间形成一种头尾相接的循环等待资源关系。
- 互斥条件
1.3 资源限制
- 在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源
- 问题
- 并发编程中,将代码执行速度加快原则是将代码中串行部分变成并发执行。如果某个并发执行受限于资源,你继续并发执行会更慢,因为加了 上下文切换和系统调度时间。
- 如何解决资源限制的问题
- 硬件资源限制
- 考虑使用集群并行执行程序 如 Hadoop
- 软件资源限制
- 考虑使用资源池将资源复用 如线程池
- 硬件资源限制
2 乐观锁,悲观锁
- 线程同步的不同角度,在java和数据库都有应用
2.1 悲观锁
- 概念:对于同一个数据的并发操作,认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
- 实现
- Java中,synchronized关键字和Lock的实现类
- MySQL的读锁、写锁、行锁等
- 适合场景
- 写操作多的场景
2.2 乐观锁
- 概念::对于同一个数据的并发操作,认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
- 如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
- 实现
- java CAS算法
- 取的时候获取 version 号,更新带上version
- 适合场景
- 读操作多的场景
3 Java并发底层实现原理
- java代码->编译器编译成字节码(平台通用)->类加载器 加载到JVM-> JVM执行字节码->汇编指令在CPU上执行。
- java并发依赖JVM实现和CPU的指令。
3.1 volatile
- volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,不会引起线程上下文的切换和调度
- 可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
public class Main {
private int m = 2;
private static volatile String istr = "istrValue";
protected static volatile String istr2 = "istrValue2";
private final String finalX = "finalXValue";
public static void main(String[] args) {
istr = "21312";
istr2 = "12312";
}
}
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
- volatile是如何来保证可见性
- JVM层面 标志生成内存屏障
- 004A 对应istr字段描述符ACC_VOLATILE,ACC_STATIC,ACC_PRIVATE,ACC_VOLATILE 标明是是volatile
- 汇编指令 lock前缀指令相对于 内存屏障的指令
- volatile 修饰的共享变量,写操作会多出 lock前缀的指令
- lock前缀的指令在多核处理器会引发两件事
- 将当前处理器缓存行数据写回到系统内存。
- 它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性
- 一个处理器的缓存写回到系统内存操作,会使在其他处理器里缓存了该内存地址的数据无效
- 处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。
- 将当前处理器缓存行数据写回到系统内存。
- JVM层面 标志生成内存屏障
3.2 synchronized
public class SynchronizedEXample {
static volatile int i = 0;
static Object o = new Object();
public static synchronized void method1(){
synchronized (SynchronizedEXample.class) {
i++;
}
}
public static synchronized void method2(){
synchronized (o) {
i++;
}
}
}
//method1 字节码指令
ldc指令int、float或String型常量从常量池推送至栈顶
astore_0 栈顶ref对象数值存入第1局部变量
public static synchronized void method1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=2, args_size=0
0: ldc #2 // class com/mg/bfbcdys/chapter02/SynchronizedEXample
2: dup
3: astore_0
4: monitorenter
5: getstatic #3 // Field i:I
8: iconst_1
9: iadd
10: putstatic #3 // Field i:I
13: aload_0
14: monitorexit
15: goto 23
18: astore_1
19: aload_0
20: monitorexit
21: aload_1
22: athrow
23: return
-
synchronized使用
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchonized括号里配置的对象
-
方法同步和代码块同步异同
- 同: JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
- Monitor依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步
- 依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”
- 如果同步代码过于简单,上下文切换时间可能比用户代码执行时间要长。
- synchronized 最初就是这种实现,JDK6之前效率低的原因
- JDK6中引入“偏向锁”和“轻量级锁”
- Monitor依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步
- 异:方法同步和代码块同步实习细节不一样
- 方法同步
- ACC_SYNCHRONIZED 标识
- 代码块同步
- monitorenter和monitorexit指令实现
- monitorenter 编译后插入到同步代码块的开始位置
- monitorexit 插入到方法结束处和异常处
- JVM要保证每个monitorenter必须有对应的monitorexit与之配对
- monitorenter和monitorexit指令实现
- 方法同步
- 同: JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
-
Synchronized 为什么能实现线程同步
-
1 Java对象头
- synchronized 悲观锁,在操作同步资源要先加锁,这把锁就是存在java对象头中
- 对象头组成
- MarK Word
- 存储对象的HashCode,分代年龄,锁标记位,非固定数据结构。
- 长度
- 非数组对象2个字宽
- 数组对象3个字宽
- 32位虚拟机 一字宽4字节,即32bit
- 64位虚拟机 一字宽8字节,即64bit
-
32位
-
64位
-
2 Monitor c++实现
- Monitor 关键属性
- _cxq(ObjectWaiter *): 存放待获取锁对象 队列
- _owner(Thread *):指向持有ObjectMonitor对象的线程
- _WaitSet(ObjectWaiter *):存放处于wait状态的线程队列
- _EntryList(ObjectWaiter *):存放处于等待锁block状态的线程队列
- 当一个线程尝试获取锁,如果锁被占用,则将该线程封装应ObjectWaiter对象插入_cxq队尾,暂停当前线程,当持有锁的线程释放锁之前,将_cxq元素,放入到EntryList, 并唤醒EntryList队首线程
- 同步块 调用Object#wait,线程对应ObjectWaiter从_EntryList移除加入到WaitSet,然后释放锁
- 当wait的线程被notify 之后,会将对于的ObjectWaiter从WaitSet移到_EntryList
- Monitor 描述
- 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
- 线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
- 每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表
- 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
- Monitor 关键属性
-
Synchronized 锁的状态
- 总结
- 偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作
- 轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能
- 重量级锁是将除了拥有锁的线程以外的线程都阻塞
- 偏向锁
- 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,,为了让线程获得锁的代价更低而引入了偏向锁
- 加锁:一个线程访问同步块并获取锁时,CAS把对象头存储锁偏向的线程ID(会在对象头和栈帧中的锁记录里存储锁偏向的线程ID),以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
- 成功,表示线程已经获得了锁
- 失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1
- 撤销:只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。需要等待全局安全点。其他线程会暂停拥有偏向锁的线程,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程任然活着,拥有偏向锁的线程继续执行,解锁之前,遍历偏向锁的记录,要么重新偏向于其他线程,要么恢复成无锁,或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
- 关闭偏向锁
- java 6,7默认开启,但是它在应用程序启动几秒之后激活
- -XX:BiasedLockingStartupDelay=0 可以关闭延迟
- 如果确定应用程序所有锁都有竞争状态
- -XX:-UseBiasedLocking=false 偏向锁,程序默认进入轻量级锁状态
- java 6,7默认开启,但是它在应用程序启动几秒之后激活
- 轻量级锁
当锁是无锁或者偏向锁,被另外线程访问,锁升级轻量级锁,其他线程自旋获取锁,不会阻塞。
-
加锁:代码进入同步块,如果同步对象锁是无锁并且不是偏向锁(锁标志01状态,偏向锁为0),虚拟机首先将在当前线程的栈帧建立锁记录(Lock Record)的空间,将锁对象头中的MarK Word 复制到锁记录中,官方称Displaced Mark Word
- 线程CAS将对象头 Mark Word更新为指向Lock Record的指针。
-
- Atomic::cmpxchg_ptr(新值, 目标地址, 原值)
- 如果目标地址是原值更新为新值,返回原值
- 否则,不做更新,并且返回新值
- Atomic::cmpxchg_ptr(新值, 目标地址, 原值)
- 如果成功,当前线程获取锁,并且设置对象Mark Word的锁标志位设置为“00”
- 如果失败,表示所对象不是无锁状态
- 首先检查对象的Mark Word是否指向当前线程的栈帧,如果是继续执行
- 否则说明其他线程竞争锁,若当前只有一个等待线程,则该线程通过自旋进行等待
- 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁(设置对象头锁标志10)。
-
解锁: 使用原子的CAS操作将Displaced Mark Word替换回到对象头的Mark World
- 成功 没有竞争发生, 锁标志更新为01
- 失败 如果是偏向锁,或者轻量级锁,继续替换, 否则膨胀重量级锁为后调用重量级锁的exit方法
- 重量级锁
- 锁标志"10" 等待的线程进入阻塞状态
- 总结
-
4原子操作实现原理
- 处理器如何实现原子操作
- 处理器提供了很多Lock前缀的指令来实现,如:
- 测试和修改指令:bts、btr、btc
- 交换指令xadd、cmpxchg
- 以及其他一些操作数和逻辑指令 add or
- 被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它
- 处理器提供了很多Lock前缀的指令来实现,如:
- java 如何实现原子操作
- 通过锁和循环CAS的方式来实现原子操作
- 使用循环CAS实现原子操作
- JVM中CAS利用处理器提供的cmpxchg(),循环进行CAS操作直到成功为止
- 锁方式来实现原子操作
- 偏向锁、轻量级锁,重量级锁
- 使用循环CAS实现原子操作
- 通过锁和循环CAS的方式来实现原子操作
- JVM 对CAS的支持
- 在支持CAS的平台上,运行时JVM编译成相应的机器指令
- 在不支持CAS指令,JVM将使用自旋锁
- CAS(Compare And Swap)
- CAS算法涉及到三个操作数
- 需要读写的内存值 V。
- 进行比较的值 A。
- 要写入的新值 B。
- 当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作)
- AtomicInteger 的 incrementAndGet 实现
- valueOffset 的理解:理解对象内存布局,就是这个对象分配内存偏移量,相当于对象内变量的地址。
- 对象头 虚拟机用3个字宽,非数组2个字宽,一个字宽4字节
- UseCompressedOops 压缩后 AtomicInteger.valueOffset 64位JVM 12字节 没压缩 16字节。 32位JVM 8字节
- compareAndSwapInt 方法中
- 在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值
- 后续JDK通过CPU的cmpxchg指令
- 存在的问题
- 1 ABA问题
- 思路 变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”
- JDK1.5 AtomicStampedReference类来解决ABA问题o,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等
- 2 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 3 只能保证一个共享变量的原子操作
- 取巧 如两个共享变量i=2,j=a 合并就是ij=2a,然后cas来操作ij
- JDK1.5 AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作
- 1 ABA问题
- CAS算法涉及到三个操作数
//AtomicStampedReference compareAndSet 方法
public boolean compareAndSet(V expectedReference, //预期引用
V newReference, //更新后的引用
int expectedStamp, // 预取的标志
int newStamp) // 更新后的标志
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object o, long offset, int expected) {
int var5;
do {
var5 = this.getIntVolatile(o, offset);
} while(!this.compareAndSwapInt(o, offset, var5, var5 + expected));
return var5;
}
public class AtomicIntegerExample {
static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
System.out.println(ClassLayout.parseClass(AtomicInteger.class).toPrintable());
Unsafe unsafe = getUnsafeInstance();
System.out.println("=================");
long offset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
atomicInteger.incrementAndGet();
int va = unsafe.getInt(atomicInteger,offset);
System.out.println("va: " + va);
System.out.println("atomicInteger:"+atomicInteger);
}
public static Unsafe getUnsafeInstance() throws Exception {
// 通过反射获取rt.jar下的Unsafe类
Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeInstance.setAccessible(true);
// return (Unsafe) theUnsafeInstance.get(null);是等价的
return (Unsafe) theUnsafeInstance.get(Unsafe.class);
}
}
// 输出
java.util.concurrent.atomic.AtomicInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int AtomicInteger.value N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
=================
va: 1
atomicInteger:1
// -XX:-UseCompressedOops 去掉指针压缩后的输出
java.util.concurrent.atomic.AtomicInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 16 (object header) N/A
16 4 int AtomicInteger.value N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
=================
va: 1
atomicInteger:1
参考