线程安全的实现方法
互斥同步
这是个常见的并发正确性保障手段,同步是指多个线程并发访问数据时,该数据同一时刻只有一个线程使用,而互斥则是实现同步的方式之一,临界区,互斥量,信号量都是实现互斥的方式。互斥是方法,同步是目的。
在java中,实现同步的方式是synchronized关键字。这个关键字需要一个对象参数,来指明要锁住的对象,如果没有明确指定,那就根据synchronized修饰的实例方法或者类方法来取得相应的对象实例或者Class对象作为锁对象。获取到锁对象时,把锁的计数器加一,退出时把锁的计数器减一。前面说过java的线程是映射到操作系统的原生线程上的,如果阻塞和唤醒其他线程,都需要操作系统帮忙。需要从用户态转换到核心态,这种状态转换会很消耗时间,因此这个关键字是重量级操作。当然虚拟机自身也做了锁优化。
出了synchronized这个关键字,还可以使用java.util.concurrent包下的重入锁来实现同步。特性都一样,都具有线程重入性,但是代码上有些区别,一个是API曾变的互斥锁,lock(),unlock()配合try finally使用,一个是原生语法上的,但是相对于synchronized,多了些特性:
1.等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃,改为处理其他事情。
2.公平锁,指的是多个线程等待同一个锁时,必须按照申请锁的时间顺序来获取锁,但是非公平的不是按照时间申请顺序来的,他是随机的,synchronized是非公平的,ReentrantLock默认也是非公平的,但是可以通过构造函数来改变。
3.锁绑定多个条件:一个ReentrantLock对象可以绑定多个Condition对象,但是synchronized的wait和notify是一对一的
性能对比:
在jdk1.5,synchronized关键字效率比ReentrantLock要低,随之线程数目的增多,但是在1.6之后,2者就差不多了。
非阻塞同步
上面的都是阻塞同步,阻塞同步是悲观的并发策略,认为不加锁,一定会出现问题。而非阻塞则是乐观加锁,先操作。如果没有线程冲突,成功,有冲突,不断重试。但是这个需要硬件指令的原子操作才能完成。现代处理器出现了 CAS(比较并交换)这是一个原子操作。来帮助实现了非阻塞加锁。就是需要写入时,先比较这个值是否变化,若是没变化就写,有变化不写。
在jdk1.5之后,java才可以,比如原子类之类的,里面的加减操作都是原子操作。就是,对这个变脸不加锁,谁用都可以,但是你要写入的时候,就要对比,是不是你原来读取的旧值,是的话就写入,不是,就不写入,当然这是针对自身的值变化操作,比如自增之类的。当然这个还是有bug的,比如ABA问题。前面的文章写过了,这里就不多说了。但是JUC包加了一个带标记的原子引用类,来控制变量的版本来保证CAS的正确性,当然,如果可能发生ABA,那么还不如用互斥同步操作来的高效。
锁优化
在JDK1.6有个重大改进,就是虚拟机实现了大量的锁优化技术,比如,适应性自旋,锁清楚,锁粗化,轻量级锁,偏向锁等。
自旋锁和自适应锁
我们在前面说到,同步互斥对性能最大的影响是阻塞的实现,因为阻塞需要将操作转到内核去完成,这些操作很耗时,也对系统的并发性能有压力。但是虚拟机开发团队发现,共享数据的锁只会持续很短的一段时间。为了这么短的时间去挂起和恢复线程划不来。自旋锁就是如果物理机器上有1个以上的处理器,能让2个以上的线程并行执行,我们可以让后面请求锁的线程稍等一下,但是不放弃处理器的执行时间,看看持有锁的线程是否释放锁,我们只需要让线程执行一个忙循环(自旋),这就是自旋锁。
自旋锁不能代替同步阻塞,1,处理器数量,2,占用处理器时间。但是如果锁占用时间极短,那么自旋效果就很好了,如果时间长,就不好了,因此要设置一个值,来判断自旋次数,如果在次数内,没话说,超过次数了,那么就只能按照传统的方式去挂起线程了,默认是10次,可以通过参数修改。
在JDK1.6之后引入了自适应自旋锁。自适应那就是说时间不固定了,由前一次同一个锁上的自旋时间以及锁的拥有者的状态决定的,如果同一个锁对象,自旋刚成功过,那么虚拟机认为这次自旋也可能成功,进而允许自旋锁自旋更长的时间。如果一个锁自旋很少成功,那么虚拟机可能自动忽略这个自旋过程。
锁清除
就是对一些代码上要求同步,但是被检测到不可能存在共享数据存在数据竞争的锁进行消除。
锁粗化
原则上,建议同步块的作用范围越小越好,但是也有例外。就是对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中。
轻量级锁
传统的互斥锁就是重量级锁,轻量级锁不是来代替重量级锁的,他的本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。
这就涉及到对象头了,对象头分为2部分信息,第一部分是存储对象自身运行时数据,一部分是指向方法区对象类型的指针。第一部分有个锁标志位,当代码进入到同步块时,如果该对象没有被锁定,虚拟机将在当前线程的栈帧里面建立一个锁空间,用于存放目前对象的对象头的第一部分的copy称为Lock Record,然后将这个对象头的第一部分跟新为指向Lock Record的指针,且锁标志位跟新为00,,如果失败就是被其他线程抢占了,如果有2个以上的线程争取一个锁,那么轻量级锁无效。
其实就是在无竞争的情况下用CAS来消除互斥量。
偏向锁
意思就是如果该锁没有被其他线程获取,那持有偏向锁的线程永远不需要同步。
小结
偏向锁、轻量级锁、用于不同的并发场景:
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争