在这段时间里看了Java相关的一些书籍,感觉对于Java的理解不再是对某一个关键字,方法的特性的记忆,而是对于其内在的原理开始有了思考。
Java并发编程的艺术这本书很久以前就被人推荐了,虽然书已经写得很不错了,但是能力有限…..对于一些内容的理解只能说是理解…..
于是准备对这本书的大概内容以及自己现在的理解记录,然后不断改进吧…..
第一章 并发编程的挑战
介绍了并发编程中几种常见的问题:
上下文切换: 任务在执行完一个时间片后会切换到下一个任务,在切换前会先记录上一个任务的状态。则任务从保存到再加载的过程为一个上下文切换。
由于线程的创建和上下文的切换都存在开销,则在一定去看下,并行的速度甚至可能要慢与串行。(比如不超过百万次的累加情况下)
减少上下文切换的方法:- 无锁并发编程:多线程竞争锁时,会引起上下文切换,则通过一些办法避免使锁。
- CAS算法(CompareAndSet):Atomic包内的CAS算法更新数据,不需加锁。
一种区别于Sycnchronized悲观锁的乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CompareAndSet是指,内部存在的3个存在数,内存值V,旧的预期值A,要修改的新值B,当且仅当V等于A时,将内存值修改为B,否则什么都不做。
乐观锁和悲观锁不存在优劣的区别,而是在于使用的情况哪种更适用。 - 使用最少线程:避免创建不需要的线程,使大量线程处于等待状态。
- 协程:在单线程里实现多任务的调度。并且单线程里面维持多个任务间的切换。
死锁:多个进程循环等待它方占有的资源而无限期的僵持的状态。
避免死锁的办法:- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占有多个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来代替内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里。
源限制的挑战:在进行并发编程时,程序执行速度受限于计算机硬件资源或软件资源。
引发的问题:受限于资源,本来将某段串行的代码已经并发执行,现在恢复成了串行执行,加上上下文切换和资源调度的时间,反而会更慢。
解决方法:- 硬件资源受限:通过集群并行执行程序,单机资源有限,则让程序在多机上运行。(比如服务器集群?)
- 软件资源受限:使用资源池将资源复用。(比如数据库连接池,线程池。都减少了重新建立连接或者重新创建线程的消耗
资源限制的情况下进行并发编程:根据不同资源的限制调整程序的并发度。
第二章 Java并发机制的底层实现原理
Java中大部分并发容器和框架都依赖于volatile和原子操作的实现原理。将介绍volatile,synchronized和原子操作的实现原理。
volatile的应用:
是轻量级的synchronized(使用和执行成本更低,不会引起上下文切换和调度),在多处理器开发中保证了共享变量的“可见性”。一个字段被声明成volatile,Java线程内存模型确保所有线程读这个变量的值是一致的。
通过将相应代码转化为汇编指令,可得相应的两件行为:
1.将当前处理器缓存行写到系统内存。(早期处理器采用LOCK#,锁住总线,由于其开销大,现采用锁定该块区域的缓存并写回内存)
2.使其他CPU内缓存了该地址的数据无效。(处理器采用嗅探技术保证它的内部缓存,系统缓存和其他处理器的缓存在总线上保持一致)Synchronized的实现原理与应用:
主要介绍为了减少获取锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
Java中的每一个对象都可以作为锁:- 普通同步方法:锁是当前实例对象。
- 静态同步方法:锁是当前类的Class对象。
- 同步方法块:锁是Synchonized括号里面配置的对象。
Synchonized在JVM中的实现原理,基于进入和退出Monitor对象实现方法同步和代码块同步(细节上有所不同)。
每一个对象都有一个monitor关联,当monitor被持有,则处于锁定状态。monitorenter指令在编译后插入到同步代码块的开始位置,monitorexit插入到方法的结束处和异常处,两两必相对应。线程运行到monitorenter指令,尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。Java对象头:主要存储对象自身的运行数据。比如:哈希码、GC分代年龄、锁状态标志、线程持久的锁、偏向线程的ID、偏向时间戳等。这部分数据的长度在32位和64为虚拟机上分别对象32bit和64bit。官方称之为“mark word”mark word 被设计为非固定的数据结构,以便在及小的空间内存储更多的信息。
synchonized用的锁是存在Java对象头内。
锁的升级与对比:
Java SE 1.6中,锁一共有4种状态,级别从低到高:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
几种状态随竞争状况逐渐升级,锁可升不可降,目前是为了提高获取锁和释放锁的效率。偏向锁:
由研究得,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。通过在线程访问同步块,在对象头和栈帧中记录锁偏向的线程ID,再进入和退出同步块不需要CAS操作加锁和解锁,只需要测试对象头的偏向锁是否指向该线程ID。成功,则有锁;失败,先查看当前是否为偏向锁:无,则CAS竞争锁;有,则尝试CAS将偏向锁指向该线程。
偏向锁的撤销:等待竞争出现才释放锁。先等待是全局安全点,再暂停拥有偏向锁线程,检查该线程是否活着。不活跃,则将对象头设置无锁;活着,则锁重新偏向其他线程,或者回复无锁,或者标记对象不适合偏向锁(锁升级),最后唤醒暂停线程。
通过以上机制,使同线程不断访问同一同步块,不需要反复执行加锁,解锁的操作。轻量级锁:
轻量级加锁:先创建锁记录(DIsplaced Mark Word),再尝试通过CAS将Mark Word替换为指向锁记录的指针。成功,则获取锁,失败则有其他线程竞争,当前线程通过自旋获取锁(一直循环查看是否已经释放了锁)。
轻量级解锁:通过CAS将锁记录替换会对象头,成功,则没有竞争;失败,则存在竞争,锁膨胀为重量级锁。
原子操作的实现原理:
原子操作:不可被中断的一个或者一系列操作。
处理器的原子操作实现:- 总线锁保证原子性:通过LOCK#指令,一个处理器输出该信号,其他处理器的请求将被阻塞,则该处理器独占共享内存。
- 缓存锁保证原子性:由于总线锁开销大,通过“缓存锁定”方式。利用缓存一致性原则,使其他处理器回写被锁定的缓存行,无效话。
Java的原子操作实现:
- 通过循环CAS实现:自旋CAS,循环进行CAS操作直到成功为止。
存在的问题:
- ABA问题:值经过A->B->A的变化,但是对于CAS检查,值是没有方式变化的。解决方法是通过对每一个值追加一个版本号,同时比较值与版本号解决。
- 循环时间长开销大:长时间的自旋CSA将带来大量执行开销。通过pause指令提高效率(?)。
- 只能保证一个共享变量的原子操作:对于多个共享变量,循环CAS无法保证操作原子性。 通过锁或者将多个共享变量合并为一个解决。
- -
- 通过锁机制实现:锁机制保证了只有获取锁的线程才可以操作锁定的内存区域。(除了偏向锁,JVM实现锁的方式都用了循环CAS)