第一章 线程简介
多线程问题:
- 安全性问题
- 由于多线程的交替执行,造成结果不对,称之为竞态条件,多个线程并发执行,可能会修改或访问其他线程正在使用的变量,即共享变量。
- 活跃性问题
- 无限循环,造成后面代码无法执行
- 性能问题
- 线程切换会有上下文切换操作,开销极大:保存和恢复上下文,丢失局部性。
多线程优势:
- 发挥多处理器的能力
- 容易建模
- 异步任务的处理更简单
第二章 线程的安全性
对象的状态:存储在状态变量中的数据。
线程安全性
多线程并发访问某个类时,不管什么情况,该类都能表现出正确的行为。
- 无状态的类一定是线程安全的
什么时候线程不安全:
1.原子性遭到破坏:
竞态条件
当某个计算结果取决于多线程的交替执行时,就会发生竞态条件
- 先检查后执行,即通过一个可能失效的观测结果决定下一步动作
- 延迟初始化
2.复合操作:
即便是多个原子操作的复合操作,线程也不一定是安全的 存在竞态条件时,就会破坏线程安全性
加锁机制
1.内置锁,即同步代码块,synchronized关键字
性能低 java对象作为锁
2.重入——内置锁是可重入的
获取内置锁的线程可多次获取锁对象
一种实现方法是:为锁关联一个计数器和锁的持有者线程,持有+1,释放-1.
3.当执行较长的计算或者可能无法快速完成的操作时,不要持有锁。
死锁和饥饿
死锁:A锁要B锁,B锁要A锁,你等我 我等你 AB永远都获得不到锁
饥饿:一直占用不到要获取的资源,但是有可能会被执行,和死锁最大的区别就在于死锁一定死掉
悲观锁和乐观锁
悲观锁认为不同线程间会发生并发问题,所以每个线程占用共享资源时都加锁,synchronized关键字
乐观锁则认为不同线程间发生并发问题的几率极小,不会加锁,但乐观锁是CAS锁,先比较再赋值,如果检测到了冲突 则回滚数据,不会发生数据并发问题。Automic包下的基本数据类型
线程中断interrupt
Thread.interrupt()方法并不能让线程自己中断,他只是给线程一个中断标记,以让线程在运行的时候根据该标记判断线程是否应该终止
当线程调用sleep方法时,会抛出Interrupt异常,当调用interrupt方法时,给线程一个中断标记,此时因为线程在睡眠触发了异常,那么进入cathch代码块,并清除了线程的中断标记,所以要想在此还想中断线程,就需要再调用一次Interfrupt方法
多线程优化
1.减少锁的占用时间
尽量减少锁执行的代码量,如果有些代码不需要加锁不涉及资源的竞争 就没有必要存在于加锁范围内,所以应该减少使用在方法上的synchronized使用
2.锁细化(锁的代码尽量短)
ConcurrentHashMap的实现原理,就是将数据分段,在put方法时,根据key值查找对应的 分段,然后对该分段进行加锁,而不是全局加锁,默认可以支持16个分段。 也就是说最大可支持16个线程并发。 但是缺点是当需要对全局加锁的时候就比较麻烦。需要对16个分段依次加锁 修改然后释放锁, 比如ConcurrentHashMap的size方法
3.使用读写锁
在读远远大于写的时候,可以使用读写锁
4.锁分离
LinkeBlockingQueue 中的put和take实现 是用的两个不同的锁,由于LinkeBlockingQueue 是链表结构,操作头和尾的时候理论上并不冲突,做到take和put的并行操作 至于原理是为什么 需要进一步研究 ArrayBlockingQueue 就不是这么实现的必须加同一把锁 源码很明了
5.锁粗化
上面讲了要锁细化,现在又说锁粗化,很矛盾。 锁粗化是指:当很多锁对同一资源进行了加锁,还不如在同一个锁中进行操作。 所以需要看情况加锁啊
虚拟机对锁优化的贡献
1.锁偏向
不太理解它的作用和机制
说是若一个线程获得了锁,则下次获得锁就不用做任何操作,减少了申请锁的消耗
背景:jdk1.6对synchronize锁进行了优化,hotspot虚拟机的作者发现在锁竞争的时候很少会出现真的竞争,且总是由同一线程多次获得。
锁的对象头中会记录一些关于锁的数据信息,这个对象是指被作为锁的那个对象,synchronize加载方法上就是当前对象实例,静态方法中就是class对象,代码块中就是指定的那个对象
当一个线程尝试获取锁时,先要去MarK World中查找是否有偏向锁指向了本线程(对象头记录线程了id)
- 若无 则再去判断此时锁的粒度,是否是偏向锁的记录是否为1
- 若是 则尝试使用CAS方式将偏向锁指向本线程
- 若不是 则尝试使用cas方式获取锁
- 若有 则说明线程已经持有锁 不需cas操作 放行即可
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
2.轻量级锁
若偏向锁失败,对象头部会有一个指针指向线程堆栈内部,以判定线程是否持有对象的锁 如果轻量级锁加锁失败,则膨胀为重量级锁,需要阻塞等待
3.自旋锁
膨胀后,为了避免线程挂起(阻塞),尝试自旋
4.锁消除
当一个场景必然不会出现资源竞争时,加了锁,那就没有必要,JVM会将此种加锁干掉 比如在方法内部使用StringBuffer,Vector 原理为:逃逸分析
volatile的实现原理
Java代码: instance = new Singleton();//instance是volatile变量 汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。