线程安全
记得有一次电话面试,被问到什么是线程安全?这个问题看似觉得一定能够答上来,但是我却没有答好。有一本书的作者将线程安全定义如下:
当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的
再看看维基百科的定义
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成
总结一下:在多线程下,对于共享变量,不需要进行额外的同步,能够保证程序执行获得正确的结果
Java中的线程安全
一提到保障线程安全,可能就马上想到sychronized
关键字进行同步,加上互斥锁,使用可重入锁等,这些都是显式加锁机制。其实,将变量定义为不可变也能够达到线程安全的目的。
在《深入理解Java虚拟机》一书中,可将各种操作共享数据分为五类:不可变,绝对线程安全,相对线程安全,线程兼容和线程独立。
不可变
不可变的对象一定是线程安全的,典型的String对象,对这些对象的操作不用加锁,访问速度也很快。此外,如果定义一个final关键字修饰的基本变量,也是不可变的。如果要使得对象不可变,那么要保证对象的行为不会对状态产生影响,可将状态变量设置为final。基本变量的包装类也是不可变的。
绝对线程安全
如定义所描述。注意,Vector是一个线程安全的容器,它的方法都被加上sychronized
关键字,但是,并不能表示它是完全线程的,它也需要额外的同步手段才能运行正确。
相对线程安全
包装对象的单独操作是线程安全的,在调用时不需要额外的同步保护,但是对于特性的连续调用,需要使用同步手段。例如:Vector,HashTable
线程兼容
对象本身不是线程安全的,需要调用端使用同步保证线程安全,Java绝大多数类是这种情况。如ArrayList, HashMap等
线程对立
无论是否采用同步措施,都无法在多线程环境下并发使用的代码,这种排斥多线程的行为很少出现,应尽量避免。
线程安全的实现
虚拟机提供了同步与锁机制来实现线程安全
互斥同步
最常见的线程安全手段,保证在多线程情况下,共享数据在同一时刻纸杯一条线程使用。互斥是实现同步的一种手段,临界区,互斥量和信号量是实现互斥的方式。最基本的互斥同步的手段是synchronized
关键字,在编译时,它会在同步块前后形成monitorenter和monitorexit两个字节码指令,这两个字节码需要一个参数,即需要锁住和解锁的对象。如果synchronized
显示指定了,那么给该对象的锁计数器加1或减1;如果没有指定,则要判断synchronized
修饰的是实例方法还是类方法(static),前者锁定实例对象,后者锁定类对象。当执行monitorenter字节指令时,会尝试获取锁,如果获取失败,则会阻塞。
注意,synchronized
关键字会阻塞其他线程,而阻塞这个操作是有操作系统帮助完成,因此,需要切换到内核态,比较耗时,因此,synchronized
是一个重量级的操作
java.util.concurrent
包的ReettranLock
来实现同步,功能上与synchronized
一致,区别在于表现为API级别的。此外,他还具有一些高级功能:
等待可中断,表示线程等待一段时候后还没有得到锁,可放弃等待去处理其他事
可实现公平锁,多个线程等待同一个锁,先等先得
锁绑定多个条件(Condition)
下面针对锁,举一个经典的例子:
|
|
最后,对于synchronized
和ReettranLock
,在jdk1.6后,性能其实差不多的,推荐优先使用原生态的synchronized
,如果特殊要求,可用后者。
非阻塞同步
上面互斥同步属于阻塞同步,线程要独占资源,其他线程要被挂起,对于性能上回造成很大的消耗(用户态内核态的来回切换,线程状态的保存等),这种属悲观并发。 除了这种方法,还有乐观并发的非阻塞同步。其主要思想是:
每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS就是一种乐观锁思想的应用. 这是一个CPU的指令,需要三个操作数,V(内存实际值),A(旧的预期值)和B(新的值),当且仅当A==V时,采取更新V为B。以上操作为原子。缺点:1) ABA问题; 2)只能保证一个共享变量 3)长时间自旋对CPU压力太大。
锁优化
由上可知,锁机制虽然保证了同步,但是对性能的消耗很大,因此,为了提高同步时使用锁的性能,jdk1.6对锁进行了很多优化
自旋锁和自适应自旋
自旋锁:当获取不到锁时,不进入阻塞状态,不放弃处理执行时间,进行忙循环(自旋),这样避免了线程挂起和恢复的开销。要求必须是多核处理器,并且适合锁被占用的时间很短,自旋一会就能马上获得锁的场景。默认自旋10次
自适应的自旋锁:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态决定
锁消除
虚拟机根据变量是否逃逸来判断是可不可以去掉不必要的同步机制。
锁粗化
如果一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁和解锁出现在循环体中,即使没有竞争,也会带来不必要的性能消耗。
轻量级锁
首先需要看看对象的内存布局,对象头会存储对象运行时的数据,如HashCode,GC年龄和锁标志等,官方称为mark word;还有一部分指向方法区对象类型的指针,数组额外存长度。
当代码进入同步块时:
如果对象没有被锁定(即标志位时01),虚拟机首先在当前线程栈帧中建立名为锁记录的(Lock Record)的空间,存储对象mark word的拷贝。
虚拟机使用CAS操作来更新mark word为指向栈帧中Lock Record指针
如果2成功,表示该线程拥有该对象的锁,将对象的mark word标志设为00(轻量级锁)
如果2失败,检查mark word是否指向当前的栈帧,如果是,表示已经拥有锁,进入同步块,否则被抢占了,膨胀成重量级锁,标志位变成10,之后来的线程都要阻塞
它的优点是基于绝大多数的共享数据都是没有竞争的互斥访问的,所以不需要加重量级锁,使用cas造作避免了使用互斥量的开销。
偏向锁
锁会偏向于第一个获取它的线程,如果接下来的执行过程,没有其他线程获取,持有偏向锁的线程无需同步就可直接进入。原理是通过CAS把线程ID写入mark word中,当下次进入发现ID相同时,则无需同步,否则撤销偏向,根据情况进入轻量等