并发编程-2
管程
共享问题
程序中有两个线程 自增 5000 次和自减 5000 次,结果有可能不是 0
结果分析:
结果是正数,负数,0。 因为 Java 中 对静态变量的自增,自减并不是原子操作,要彻底的理解,必须从字节码来进行分析。
例如: i++; i– 单行代码产生的JVM 字节码指令是两个操作。
单线程是按顺序执行的不会产生上述的问题。
多线程会有上下文的切换,引起指令的交错,导致了多线程访问共享内存的线程安全问题。
临界区:
- 一个程序运行多个线程本身是没有问题的
- 问题出在线程访问共享资源
- 多个线程读共享资源其实也是没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同,而导致结果无法预测,称之为竞态条件
synchroized
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案,Synchronized Lock
- 非阻塞式的解决方案,原子变量
Synchronized 阻塞式的解决共享问题,即俗称【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程持有【对象锁】,其他线程在想获取这个【对象锁】 时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意: 虽然java 中互斥和同步都可以采用 synchronized 关键字来完成,但他们还是有区别的。
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到某个点
synchronized 实际是用对象锁保证了 临界区内代码的原子性,临界区的代码对外是不可分割的,不会被线程切换所打断。
面向对象的思想改进
方法上的 synchronized
- 成员方法上
- 静态方法上
- 不加 synchroized 的方法: 就是不排队,(好比翻窗户进去的) 不能保证原子性
线程八锁
线程安全分析
局部变量线程安全分析
常见线程安全类
String、Integer 、 StringBuffer、 Random、 Vector、 HashTable、 java.util.concurrent 包下的类
这里说的线程安全是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为,他们每个方法是原子的,注意他们多个方法的组合不是原子的,
不可变类
Java String类为什么是final的?
为了实现字符串池
为了线程安全
为了实现String可以创建HashCode不可变性
final的用途,在分析String为什么要用final修饰,final可以修饰类,方法和变量,并且被修饰的类或方法,被final修饰的类不能被继承,即它不能拥有自己的子类,被final修饰的方法不能被重写, final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。
在了解final的用途后,在看String为什么要被final修饰:主要是为了”安全性“和”效率“的缘故。
Monitor(锁)
- 对象头
- 对象成员变量
hashcode: 哈希码 age: 分代年龄 biased_lock:0 : 代表是不是偏向锁 01:代表加锁状态
java synchroized 锁底层的原理: monitor
monitor 被翻译为监视器 或管程
每个 java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
Synchronized
原理之 Synchroized
轻量级锁: 当有竞争时还会升级为重量级锁。
小故事
Synchronized 原理学习
轻量级锁:
使用场景: 如果一个对象虽然有多个线程访问但是多个线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchroized
轻量级锁,加锁,锁重入,解锁
锁膨胀
自旋优化:
重量级锁的优化,多核cpu 才有意义
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
偏向锁
轻量级锁没有竞争时(就自己这个线程),每次重入仍然需要执行CAS 操作。
Java 6 中引入了偏向锁来做进一步优化;只有第一次使用CAS 将线程ID,设置到对象的Mark Word 头,之后发现这个线程ID 是自己的就表示没有竞争,不用重新CAS。 以后只要不发生竞争,这个对象就归线程所有。
偏向状态:
- 偏向锁的延迟特性,不是立即加锁的。
- 测试偏向锁,前 54 位是线程id
撤销偏向锁
- 调用了 hashcode
因为存不下,就会撤销了
- 其他线程使用对象
会将偏向锁升级为轻量级锁
- 调用wait/notify
wait/notify 只有重量级锁有,调用时会把锁(不管是偏向还是轻量)都会升级为重量级锁。
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍然有机会偏向T2,重偏向会重置对象的ThreadID
当撤销偏向锁阀值超过 20 次后,jvm 会觉得,我是不是偏向错了,于是会给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阀值超过 40 次,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
锁消除
java 解释 + 编译
因为有 JIT 即时编译器,对字节码进一步优化,对热点代码的优化,其中会对局部变量进行判断是否逃离了作用的范围,如果加锁代码块没有逃离(不可能被共享),加锁没什么意义, 可以会对锁进行消除。
这个配置默认是打开的,也是可以去掉的。