目录
1.并发编程
三种性质:
- 可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存可导致可见性问题
- 原子性:一个或多个CPU执行操作不被中断。线程切换可导致原子性问题
- 有序性:编译器优化可能导致指令顺序发生改变。编译器优化可能导致有序性问题。
> 有序性问题指的是在多线程环境下多核,由于执行语句重排序后,重排序的这一部
分没有一起执行完,就切换到了其它线程,导致的结果与预期不符的问题。这就是编
译器的编译优化给并发编程带来的程序有序性问题
三个问题:
- 安全性问题:线程安全
- 活跃性问题:死锁、活锁、饥饿
- 性能问题:
- 使用无锁结构:TLS线程局部存储(ThreadLocal),Copy-On-Write,乐观锁;Java
的原子类,Disruptor无锁队列
- 减少锁的持有时间:让锁细粒度。如ConcurrentHashmap;再如读写锁,读无锁写有锁
1.1 volatile
C语言中的原意:禁用CPU缓存,从内存中读出和写入。
Java语言的引申义:
- Java会将变量立刻写入内存,其他线程读取时直接从内存读(普通变量改变后,什么时候写
入内存是不一定的)
- 禁止指令重排序
解决问题:
- 保证可见性
- 保证有序性
- 不能保证原子性
2. 锁(预防死锁): 【重点】
- 互斥:不能破坏
- 占有且等待:同时申请所有资源
- 不可抢占:sychronized解决不了,Lock可以解决
- 循环等待:给资源设置id字段,每次都是按顺序申请锁
2.1 锁的实现机制
Java对象存储在堆Heap内存。一个Java对象概括起来分为对象头、对象体和对齐字节JMM。
- 对象头中的Mark Word标记字主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode。
- Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例。
- 数组长度也是占用64位-8字节的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分。
- 对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型。
- 对齐字是为了减少堆内存的碎片空间,这是因为 JVM 虚拟机要求被管理的对象的大小都是8字节的整数倍。那么在某些情况下,就需要填充区域对不足的对象区域进行填充。
2.2 锁的转换
当对一个对象加锁其实是修改这个对象的markword信息,当new一个对象可能有两种状态,偏向锁已经启动和未启动 。普通new出一个对象加上synchronized,如果可偏向标识已经启动升级为偏向锁;如果偏向锁没有启动,直接到自旋锁,升级为轻量级锁,线程处于忙等状态。如果竞争进一步加剧则会升级为重量级锁。synchronized实现的同步锁,在JDK1.6之前叫做重量级锁。重量级锁会造成线程排队(串行执行),且会使CPU在用 户态和核心态之间频繁切换,所以代价高、效率低。
为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:【记住锁升级流程】
- 初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是001,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)
- 当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。对象的mark word是101
- 当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。对象的markword是00。
- 如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式来登记和管理排队的线程。对象的mark word是10。
- synchronized修饰的代码块:通过反编译.class文件,通过查看字节码可以得到:在代码块中使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令指明同步代码块的结束位置需要注意monitorexit有2个,是因为退出同步代码块有2种情形,正常退出和异常退出
- synchronized 修饰的方法:查看字节码可以得到:在同步方法中会包含A