当多个线程访问一个对象时,如果不考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用地方进行额外的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
各种操作共享的数据分类5类:
互斥是实现同步的一种手段。需要阻塞和唤醒,使得性能降低,也称阻塞同步,是悲观锁。无论共享数据是否会发生竞争都需要加锁。
最基本互斥手段是synchronized。会在同步块的前后形成monitorenter和monitorexit,在执行monitorenter时尝试去获取锁,如果已被自己持有或者未被持有则锁计数器加1,执行monitorexit就会减1,直到为0才释放锁。如果获取锁失败则会处于阻塞状态。
除了synchronnized外还有JUC的重入锁(ReentrantLock),都具备线重入特性,只是代码写法上有区别,一个为API层面的互斥锁(lock和unlock方法配合try/finally语句实现),一个是原生语法层面的互斥锁。RenetrantLock多了3个高级功能:
乐观锁。其并发策略时先进行操作,如果没有线程用共享数据,那操作就成功;如果有争用,那么产生冲突,就采取其它措施补偿(常见的补偿是不断重试直到成功为止)。因此就不要把线程挂起。
乐观锁需要保证操作和冲突检测具有原子性,如果使用互斥同步来保证就失去意义了,因此需要硬件来完成这件事。如CAS指令。
CAS有三个操作数:内存地址A、旧的预期值O、新值N。当且仅当A符合旧预期值O时才进行更新V为新值N,并返回旧值O,如果不符合则不进行更新。该操作是一个原子操作。
保证线程安全不一定就需要同步,二者无因果关系。如果一个方法不涉及共享数据,那么就无须任何同步。例如下面两类代码是天生线程安全的:
锁优化技术:自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等、
互斥同步最大的性能消耗就是线程的挂起和恢复,都需要转入内核态完成。大部分共享数据的锁定状态只持续很短的时间,因此为了这段时间取挂起和恢复不值得,所以可以让后面请求的锁“等待一下”,但不放弃处理器的执行时间,只需执行忙循环(自旋),这就是所谓的自旋锁。
自旋不能代替阻塞。如果自旋的时间开销超过了线程挂起和恢复的时间开销,那么就白白消耗处理器资源。默认自旋10次,可-XX:PreBlockSpin更改。JDK1.6后引入自旋锁,自旋时间不是固定的,而是由上一次在同一个锁上的自旋时间以及锁的拥有者状态来决定。如果上一次自旋成功且线程正在运行中,则会认为此次自旋可能成功,因此会让自旋持续更长时间,如果一个锁的自旋很少成功则以后获取可能直接省略自旋直接挂起。
锁消除是指在即使编译时,对一些代码上要求同步,但是被检测到不存在共数据竞争的锁进行消除。例如StringBuffer的append有同步块,其引用不会逃逸到concatString外,其他线程无法访问到他,因此锁可以被安全消除。
public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
原则上是推荐将同步的作用范围变小,只在共享数据的实际作用域才发生同步。如果在一系列操作对同一个对象反复加锁解锁,甚至加锁出现在for循环里,那么即使没竞争,频繁的互斥同步也会导致性能损耗。例如上面的append,虚拟机就会把加锁范围变成第一个append扩展到最后一个append。
轻量级锁是相对使用操作系统互斥量来实现的传统锁而言的,传统锁是重量级锁。
如果一开始线程获取锁时,对象头标志未锁定,则用CAS操作使对象头标志为轻量锁,则拥有了该对象的锁。如果更新失败,则判断对象的mark word是否指向当前线程的栈帧,是则表明已经拥有这个锁,直接进入同步代码块,否则说明该锁被其它线程占了。如果两条以上的线程争同一个锁,那么轻量级锁就变成重量级锁。
轻量级锁提升同步性能的依据是“大部分锁,在整个同步周期是不存在竞争的”,这是一个经验数据。没有竞争,则使用CAS操作就避免了使用互斥的开销。
锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,锁没被其它线程获取,则持有偏向锁的线程不需要再进行同步。如果已被持有偏向锁,则根据锁对象是否被锁定转换为轻量级锁或未锁定状态。
原文:https://www.cnblogs.com/yjou/p/11270307.html