面向对象的编程思想是站在现实世界的角度去抽象和解决问题,他把数据和行为都看作是对象的一部分,这样可以让程序员能以符合现实世界的思维方式来编写和组织程序。
线程安全的一个恰当的定义
:当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的 。
按照线程安全的安全程度由强至弱来排序
,可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全
相对线程安全、线程兼容和线程对立。
(1)不可变:不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何线程安全的保障措施,
在java语言中,如果共享数据是一个基本数据类型,那么只要再定义时使用final关键字修饰他就可以保证它是不可变的,如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,就比如java.lang.string类的对象,她是一个典型的不可变的对象,我们调用他的substring()、replace()等方法都不会影响他原来的值,只会返回一个新构造的字符串对象。
(2)绝对线程安全:一个类要达到"不管运行时环境如何,调用者都不需要任何额外的同步措施"通常需要付出很大的,甚至有时候是不切实际的代价,在java api中标注自己是线程安全的类,大多数都不是绝对的线程安全,比如java.util.vector是一个线程安全的容器,因为它的add()、get()、size()等方法都是被synchronized修饰的,尽管这样效率很低,但确实是安全的,但是在多线程的环境下,如果不在方法调用端做额外的同步措施的话,只用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不在可用的话,再用i访问数组就会抛出异常,如果要保证这段代码正常执行下去,就必须改变代码。
(3)相对线程安全:相对线程安全就是我们所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性,大部分线程安全类都属于这种类型。
(4)线程兼容:线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,
(5)线程对立:线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码,比如thread类的suspend()和resume()方法 ,如果两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁的风险。所以这两个方法被jdk声明废弃了,
线程安全的实现方法:
(1)互斥同步(阻塞同步):同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用,而互斥是实现同步的一种手段,
最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象,如果java程序中synchronized明确指定了对象参数,那就是这个对象的reference,如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或class对象来作为锁对象,因为同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入,java的线程是映射到操作系统的原生线程之上的,如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态,状态转换需要耗费很多的处理器时间,所以synchronized是java语言的一个重量级的操作。
还有实现同步的方法是java.util.concurrent包中的重入锁(reentrantlock)来实现同步,他们的用法基本相似,都具备可重入性,区别是一个是api层面的互斥锁,另一个是原生语法层面的互斥锁,相比sychronized,reetrantlock增加了一些高级功能(等待可中断、可实现公平锁、锁可以绑定多个条件)p392页
(2)非阻塞同步:随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,也就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,那就再采取其他补偿措施,这个策略许多实现不需要把线程挂起,因此叫做非阻塞同步,
(3)无同步方案:要保证线程安全,并不一定就要进行同步,同步只是保证数据共享争用时的正确性的手段,如果一个方法本身就不涉及数据共享 ,就不需要同步措施去保证正确性,两类代码天生就是安全的,可重入代码和线程本地存储。
锁优化的措施:
(1)自旋锁与自适应自旋:由于互斥同步对性能影响最大的是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,因此如果物理机器上有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这就是所谓的自旋锁。
jdk1.4.2中就已经引入,不过默认是关闭的,在jdk1.6中就默认开启了,自旋等待虽然本身避免了线程切换的开销,但它要占用处理器时间,如果锁被占用的时间很短,自旋等待的效果就非常好,如果被占用时间过长,就会带来性能上的浪费,因此自旋等待的时间必须有一定的限度,如果超过了限定的次数还没有成功获得锁,就使用传统的方式去挂起线程,
jdk1.6引入了自适应的自旋锁,自适应意味着自旋的时间不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
(2)锁消除(P398):锁消除就是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,锁消除的判定依据来源于逃逸分析的数据支持,
(3)锁粗化:大部分情况下,总是推荐将同步块的作用范围限制的尽量小,只在共享数据的实际作用域中财进行同步,这样是为了使得需要同步的操作数尽可能变小,但是如果一些列的操作都对同一个对象反复加锁和解锁,则会导致不必要的性能损耗,例如:
public string concatstring(string s1,string s2,string s3){
stringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.sppend(s3);
return sb.tostring();
}
三个append操作都需要对同一个对象加锁,所以会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
(4)轻量级锁:首先了解hotspot虚拟机的对象头分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码、分代年龄等,官方称为“mark word”,另一部分用于存储指向方法区对象类型数据的指针
加锁的过程:在代码进入同步块的时候,如果此对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录 (lock record)的空间,用于存储锁对象目前的mark word 的拷贝,然后虚拟机将使用CAS操作尝试将对象的mark word更新为lock record的指针,如果更新动作成功,则这个线程就拥有了该对象的锁,并且对象mark word的锁标志位将转变为“00”,即表示此对象处于轻量级锁定状态,如果更新操作失败,虚拟机首先会检查对象的mark word是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进去同步块继续执行,否则说明这个锁对象已经被其他线程抢占了,如果两个以上的线程争用同一个锁,则轻量级锁就膨胀为重量级,
(5)偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能,
偏向锁的意思是这个锁会偏向于第一个获得他的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程永远不需要再进行同步,
过程: 假设当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式,同时使用CAS操作罢获取到的锁的线程的id记录在独享的mark word中,如果cas操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,当有另外一个线程尝试去获取这个锁时,偏向模式就宣告结束,
偏向锁可以提高带有同步但无竞争的程序性能,但是如果程序大多数的锁总是被不同的线程访问,那偏向模式就是多余的 ,