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