目录html
本篇参考许多著名的书籍,造成读书笔记,便于加深记忆。java
前文传送门:Java并发读书笔记:JMM与重排序编程
当一个变量被多个线程读取,且至少被一个线程写入时,若是读写操做不遵循happens-before
规则,那么就会存在数据竞争的隐患,若是不给予正确的同步手段,将会致使线程不安全。api
Brian Goetz在《Java并发编程实战》中是这样定义的:安全
当多个线程访问一个类时,若是不用考虑这些线程在运行时环境下的调度和交替执行,而且不须要额外的同步及在调用方代码没必要作其余的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。多线程
周志明在《深刻理解Java虚拟机》中提到:多个线程之间存在共享数据时,这些数据能够按照线程安全程度进行分类:并发
不可变的对象必定是线程安全的,只要一个不可变的对象被正确地构建出来,那么它在多个线程中的状态就是一致的。例如用final关键字修饰对象:app
JavaAPI中符合不可变要求的类型:String类,枚举类,数值包装类型(如Double)和大数据类型(BigDecimal)。工具
即彻底知足上述对于线程安全定义的。性能
知足该定义其实须要付出不少代价,Java中标注线程安全的类,实际上绝大多数都不是线程安全的(如Vector),由于它仍须要在调用端作好同步措施。Java中绝对线程安全的类:CopyOnWriteArrayList
、CopyOnWriteArraySet
。
即咱们一般所说的线程安全,Java中大部分的线程安全类都属于该范畴,如Vector
,HashTable
,Collections
集合工具类的synchronizedCollection()
方法包装的集合等等。就拿Vector举例:若是有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的状况下都会出现ConcurrentModificationException
,也就是fail-fast
机制。
对象自己并非线程安全的,能够经过在调用段正确同步保证对象在并发环境下安全使用。如咱们以前学的分别与Vector和HashTable对应的ArrayList
和HashMap
。
对象经过synchronized关键字修饰,达到同步效果,自己是安全的,但相对来讲,效率会低不少。
不管调用端是否采起同步措施,都没法正确地在多线程环境下执行。Java典型的线程对立:Thread类中的suspend()和resume()方法:若是两个线程同时操控一个线程对象,一个尝试挂起,一个尝试恢复,将会存在死锁风险,已经被弃用。
常见的对立:System.setIn()
,System.setOut()
和System.runFinalizersOnExit()
。
互斥同步也被称作阻塞同步(由于互斥同步会由于线程阻塞和唤醒产生性能问题),它是实现线程安全的其中一种方法,还有一种是非阻塞同步,以后再作学习。
互斥同步:保证并发下,共享数据在同一时刻只被一个线程使用。
其中使用synchronized
关键字修饰方法或代码块是最基本的互斥同步手段。
synchronized
是Java提供的一种强制原子性的内置锁机制,以synchronized
代码块的定义方式来讲:
synchronized(lock){ //访问或修改被锁保护的共享状态 }
它包含了两部分:一、锁对象的引用 二、锁保护的代码块。
每一个Java对象均可以做为用于同步的锁对象,咱们称该类的锁为监视器锁(monitor locks),也被称做内置锁。
能够这样理解:线程在进入synchronized以前须要得到这个锁对象,在线程正常结束或者抛出异常都会释放这个锁。
而这个锁对象很好地完成了互斥,假设A持有锁,这时若是B也想访问这个锁,B就会陷入阻塞。A释放了锁以后,B才可能中止阻塞。
//普通同步方法 public synchronized void do(){}
//静态同步方法 public static synchronized void f(){}
//锁对象为TestLock的类对象 synchronized (TestLock.class){ f(); }
明确:synchronized方法和代码块本质上没啥不一样,方法只是对跨越整个方法体的代码块的简短描述,而这个锁是方法所在对象自己(static修饰的方法,对象是当前类对象)。这个部分能够参考:Java并发之synchronized深度解析
释放锁的状况:
不释放锁的状况:
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但二者实现细节不一样。
代码块同步使用monitorenter
和monitorexit
两个指令实现,JVM的要求以下:
monitorenter
指令会在编译后插入到同步代码块的开始位置,而monitorexit
则会插入到方法结束和异常处。monitor
与之关联,且当一个monitor
被持有以后,他会处于锁定状态。线程执行到monitorenter
时,会尝试获取对象对应monitor
的全部权。
monitorexit
时,锁计数器减一,计数为零则锁释放。获取对象锁失败,则当前线程陷入阻塞,直到对象锁被另一个线程释放。
重进入意味着:任意线程在获取到锁以后可以再次获取该锁而不会被锁阻塞,synchronized
是隐式支持重进入的,所以不会出现锁死本身的状况。
这就体现了锁计数器的做用:得到一次锁加一,释放一次锁减一,不管得到仍是释放多少次,只要计数为零,就意味着锁被成功释放。
ReentrantLock
位于java.util.concurrent(J.U.C)
包下,是Lock接口的实现类。基本用法与synchronized
类似,都具有可重入互斥的特性,但拥有扩展的功能。
Lock接口的实现提供了比使用synchronized方法和代码块更普遍的锁操做。容许更灵活的结构,具备彻底不一样的属性,而且可能支持多个关联的Condition对象。
RenntrantLock官方推荐的基本写法:
class X { //定义锁对象 private final ReentrantLock lock = new ReentrantLock(); // ... //定义须要保证线程安全的方法 public void m() { //加锁 lock.lock(); try{ // 保证线程安全的代码 } // 使用finally块保证释放锁 finally { lock.unlock() } } }
ReentrantLock表现为API层面的互斥锁,经过lock()
和unlock()
方法完成,是显式的,而synchronized表现为原生语法层面的互斥锁,是隐式的。
当持有线程长期不释放锁的时候,正在等待的线程可以选择放弃等待或处理其余事情。
ReentrantLock锁是公平锁,即保证等待的多个线程按照申请锁的时间顺序依次得到锁,而synchronized是不公平锁。
一个ReentrantLock对象能够同时绑定多个Condition对象。
JDK1.6以前,ReentrantLock在性能方面是要领先于synchronized锁的,可是JDK1.6版本实现了各类锁优化技术,后续性能改进会更加偏向于原生的synchronized。
参考数据:《Java并发编程实战》、《Java并发编程的艺术》、《深刻理解Java虚拟机》