背景:
cpu、内存、I/O设备不断迭代,但是在快速发展过程中,有一个核心矛盾一直存在,就是三者的速度差异,为了提高计算机性能,合理利用cpu,做出了以下方案:
- cpu增加了缓存,均衡与内存的速度差异
- 操作系统增加了进程、线程,以分时复用cpu
- 编译程序优化了指令执行效率,使缓存更加合理的利用
出现的问题:
但是这也是并发程序异常的根源之处
- 缓存导致了可见性问题(一个线程对共享变量的修改,另外一个线程能立即看到)
多核cpu,每个cpu都有自己的缓存,都会从各自的缓存中读值,另外值得注意的是两个线程不是同时启动的,有一个时差,数据量越大,错误率越高。 - 线程切换带来了原子性问题
操作系统会进行基于时间片的任务切换,而做任务切换,可以发生在任何一条cpu指令执行完(不是高级语言的一条语句),这样也就带来了无法保证原子性的问题 - 编译优化带来的有序性问题
经典案例:利用双重检查创建单例对象
Singleton instance = new Singleton();
new操作为:(分配一块内存M,在内存M上初始化对象Singleton,将M的地址赋值给instance变量),但是实际上优化后会先将M的地址赋值给instance变量,那如果在此时进行了线程切换,实际上并没有初始化instance,但是变量的引用不为空。
【如果对instance进行volatile声明,可以禁止指令重排序,避免发生】
【对于有volatile语义声明的变量,写入时线程执行完后会强制将值刷新到内存中,读时线程也会强制重新把内存中的内容写到自己的缓存,这就是写入屏障问题?也是happen-before问题?】
如何解决问题:
- 解决可见性和有序性—按需禁用缓存和编译优化
java内存模型规范了jvm如何按需禁用缓存和编译优化的方法,对编译器和处理器进行限制,包括三个关键字volatile、synchronized和final,六项happens-before规则
happens-before规则(A happends-before B:A的操作结果对B是可见的)- 程序的顺序性规则:按照程序的顺序,前面的操作(修改变量的值)后面是可见的(顺序执行,限制了编译器的优化)
- volatile变量规则:对一个volatile变量的写操作对这个变量后续的读操作是可见的
- 传递性:A happends-before B ,且B happends-before C ,那么A happends-before C
- 锁的规则:java中synchronized是对管程的实现,对一个锁的解锁对后续的这个锁的加锁是可见的。在解锁时,jvm需要强制刷新缓存。
- 线程start()规则(启动):线程A中启动线程B,B能看到A在启动B前的操作
- 线程join()规则(终止):如果在线程A中,调用线程B的join()并成功返回,那么线程B中的操作Happends-Before于该join()操作的返回。(通过Thread.isAlive()检测到线程是都终止执行)
- 线程中断规则:线程interrupt()方法的调用先行于被中断线程的代码检测到中断时间的发生
- 解决原子性—同一时刻只有一个线程执行—互斥锁
-
synchronized—加锁的本质是在锁对象的对象头中写入当前线程的id
重要的点:我们锁的是什么?我们保护的又是什么?
锁的是:当修饰静态方法的时候,锁定的是当前类的Class对象
当修饰非静态方法的时候,锁定的是当前实例对象this
当修饰代码块,锁定的是传入的对象
受保护资源和锁之间的关联关系是N:1的关系
一个保护资源被多把锁保护,会出现并发问题(不能保证同一时刻只有一个线程执行)
一把锁可以保护多个资源
保护没有关联关系的多个资源:一把锁会导致串行,性能差。用不同的锁对受保护资源进行精细化管理,能提升性能,这种锁叫做细粒度锁。
保护有关联关系的多个资源:使用this锁不能覆盖所有受保护的资源,要使用Class作为共享的锁,要选择粒度更大的锁(串行化,性能差)
【性能优化:账户A向账户B转账,this为A加锁,账户B的实例为B加锁—但是会导致死锁问题】 -
性能提升(细粒度锁)—导致死锁—一组互相竞争资源的线程因互相等待,导致“永久”阻塞
如何预防死锁(解决死锁问题最好的办法就是规避死锁)
死锁出现的4个必备条件:互斥,占有且等待,不可抢占,循环等待(破换一个就可避免)、
占有且等待:一次性申请所有的资源(使用一个角色来管理临界区,同时申请资源和同时释放资源)
不可抢占:如果申请不到,就主动释放占有资源(java.util.concurrent下提供的Lock可以解决)
循环等待:按序申请资源(为资源进行排序,申请时按从小到大顺序申请) -
“等待-通知”—对规避死锁的性能提升—线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。
实现:synchronized配合wait()、notify()、notifyAll()
当线程拿锁进入临界区后,由于条件不满足,调用wait()方法,进入等待队列,释放持有的锁,当线程的条件满足时,调用notifyAll(),通知等待队列中的线程,条件曾经满足过(因为通知时条件满足,但是被通知的线程还要去获得锁去执行,这两个时间点不会重合)
-
未完待续
安全性、活跃性以及性能问题
管程:并发编程的万能钥匙
java线程上:java线程的生命周期
java线程中:创建多少线程才是合适的?
java线程下:为什么局部变量的线程是安全的?
如何用面向兑现思想写好并发程序?