13.线程安全与锁优化

来源:互联网 发布:没u盘怎么装windows 编辑:程序博客网 时间:2024/06/07 05:32
1. 线程安全
当多个线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 或者在调用方进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那这个对象是线程安全的 。
1.1 Java语言中的线程安全
按照线程安全的“安全程度” 由强至弱,将Java语言中各种操作共享的数据分为以下5类: 不可变、绝对线程安全、 相对线程安全、 线程兼容和线程对立。
A.不可变
不可变(Immutable) 的对象一定是线程安全的, 无论是对象的方法实现还是方法的调用者, 都不需要再采取任何的线程安全保障措施。
共享数据是一个基本数据类型, 那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。 如果共享数据是一个对象, 那就需要保证对象的行为不会对其状态产生任何影响才行。保证对象行为不影响自己状态的途径有很多种, 其中最简单的就是把对象中带有状态的变量都声明为final,
这样在构造函数结束之后, 它就是不可变的。
B.绝对线程安全
C.相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全, 它需要保证对这个对象单独的操作是线程安全的, 我们在调用的时候不需要做额外的保障措施, 但是对于一些特定顺序的连续调用, 就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在Java语言中, 大部分的线程安全类都属于这种类型, 例如Vector、 HashTable、 Collections的synchronizedCollection() 方法包装的集合等。
D.线程兼容
线程兼容是指对象本身并不是线程安全的, 但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用, 我们平常说一个类不是线程安全的, 绝大多数时候指的是这一种情况。
Java API中大部分的类都是属于线程兼容的, 如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
D.线程对立
线程对立是指无论调用端是否采取了同步措施, 都无法在多线程环境中并发使用的代码。
1.2 线程安全的实现方法
A.互斥同步
互斥同步(Mutual Exclusion&Synchronization) 是常见的一种并发正确性保障手段。 同步是指在多个线程并发访问共享数据时, 保证共享数据在同一个时刻只被一个(或者是一些, 使用信号量的时候) 线程使用。 而互斥是实现同步的一种手段, 临界区(Critical Section) 、 互斥量(Mutex) 和信号量(Semaphore) 都是主要的互斥实现方式。
1).synchronized
synchronized关键字经过编译之后, 会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。在虚拟机规范对monitorenter和monitorexit的行为描述中, 有两点是需要特别注意的。 首先, synchronized同步块对同一条线程来说是可重入的, 不会出现自己把自己锁死的问题。 其次, 同步块在已进入的线程执行完之前, 会阻塞后面其他线程的进入。
Java的线程是映射到操作系统的原生线程之上的, 如果要阻塞或唤醒一个线程, 都需要操作系统来帮忙完成, 这就需要从用户态转换到核心态中, 因此状态转换需要耗费很多的处理器时间。 所以synchronized是Java语言中一个重量级(Heavyweight) 的操作。
2).ReentrantLock
ReentrantLock增加了一些高级功能, 主要有以下3项: 等待可中断、 可实现公平锁, 以及锁可以绑定多个条件。
等待可中断是指当持有锁的线程长期不释放锁的时候, 正在等待的线程可以选择放弃等待, 改为处理其他事情, 可中断特性对处理执行时间非常长的同步块很有帮助。
公平锁是指多个线程在等待同一个锁时, 必须按照申请锁的时间顺序来依次获得锁; 而非公平锁则不保证这一点, 在锁被释放时, 任何一个等待锁的线程都有机会获得锁。 synchronized中的锁是非公平的, ReentrantLock默认情况下也是非公平的, 但可以通过带布尔值的构造函数要求使用公平锁。
锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象, 而synchronized中, 锁对象的wait() 和notify() 或notifyAll() 方法可以实现一个隐含的条件, 如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁, 而ReentrantLock则无须这样做, 只需要多次调用newCondition() 方法即可。
3).选择
使用JDK 1.6或以上部署的话, 性能因素不是选择ReentrantLock的理由, 虚拟机在未来的性能改进中肯定也会更加偏向于原生的synchronized, 所以还是提倡在synchronized能实现需求的情况下, 优先考虑使用synchronized来进行同步。
B.非阻塞同步
基于冲突检测的乐观并发策略, 通俗地说, 就是先进行操作, 如果没有其他线程争用共享数据, 那操作就成功了; 如果共享数据有争用, 产生了冲突, 那就再采取其他的补偿措施, 这种乐观的并发策略的许多实现都不需要把线程挂起, 因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization) 。
需要硬件保证一个从语义上看需要多次操作的行为只通过一条处理器指令就能完成, 常用的有:
测试并设置(Test-and-Set) 。
获取并增加(Fetch-and-Increment) 。
交换(Swap) 。
比较并交换(Compare-and-Swap, 下文称CAS) 。加载链接/条件存储(Load-Linked/Store-Conditional, 下文称LL/SC) 。
Java的支持:J.U.C包里面的整数原子类,如AtomicInteger;
C.无同步方案
如果一个方法本来就不涉及共享数据, 那它自然就无须任何同步措施去保证正确性,
可重入代码(Reentrant Code) : 这种代码也叫做纯代码(Pure Code) , 可以在代码执行的任何时刻中断它, 转而去执行另外一段代码(包括递归调用它本身) , 而在控制权返回后, 原来的程序不会出现任何错误。
线程本地存储(Thread Local Storage) : 如果一段代码中所需要的数据必须与其他代码共享, 但是这些共享数据的代码能保证在同一个线程中执行,我们就可以把共享数据的可见范围限制在同一个线程之内, 这样, 无须同步也能保证线程之间不出现数据争用的问题。Web交互模型中的“一个请求对应一个服务器线程” (Thread-per-Request) 的处理方式,就属于这种情形。一个变量要被某个线程独享,可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。
2. 锁优化
2.1 自旋锁与自适应自旋
如果物理机器有一个以上的处理器, 能让两个或以上的线程同时并行执行, 我们就可以让后面请求锁的那个线程“稍等一下” , 但不放弃处理器的执行时间, 看看持有锁的线程是否很快就会释放锁。 为了让线程等待, 我们只需让线程执行一个忙循环(自旋) , 这项技术就是所谓的自旋锁。
在JDK 1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再固定了, 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果在同一个锁对象上, 自旋等待刚刚成功获得过锁, 并且持有锁的线程正在运行中, 那么虚拟机就会认为这次自旋也很有可能再次成功, 进而它将允许自旋等待持续相对更长的时间, 比如100个循环。 另外, 如果对于某个锁, 自旋很少成功获得过, 那在以后要获取这个锁时将可能省略掉自旋过程, 以避免浪费处理器资源。
2.2 锁消除
锁消除是指虚拟机即时编译器在运行时, 对一些代码上要求同步, 但是被检测到不可能存在共享数据竞争的锁进行消除。 锁消除的主要判定依据来源于逃逸分析的数据支持。
2.3 锁粗化
一系列的连续操作都对同一个对象反复加锁和解锁, 甚至加锁操作是出现在循环体中的, 那即使没有线程竞争, 频繁地进行互斥同步操作也会导致不必要的性能损耗。
虚拟机探测到有这样一串零碎的操作都对同一个对象加锁, 将会把加锁同步的范围扩展(粗化) 到整个操作序列的外部。
2.4 轻量级锁
轻量级锁并不是用来代替重量级锁的, 它的本意是在没有多线程竞争的前提下, 减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁, 在整个同步周期内都是不存在竞争的” , 这是一个经验数据。 如果没有竞争, 轻量级锁使用CAS操作避免了使用互斥量的开销, 但如果存在锁竞争, 除了互斥量的开销外, 还额外发生了CAS操作, 因此在有竞争的情况下, 轻量级锁会比传统的重量级锁更慢。
2.5 偏向锁
偏向锁也是JDK 1.6中引入的一项锁优化, 它的目的是消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。
锁会偏向于第一个获得它的线程, 如果在接下来的执行过程中, 该锁没有被其他的线程获取,
则持有偏向锁的线程将永远不需要再进行同步。
它同样是一个带有效益权衡(Trade Off) 性质的优化,如果程序中大多数的锁总是被多个不同的线程访问, 那偏向模式就是多余的。 在具体问题具体分析的前提下, 有时候使用参数-XX: -UseBiasedLocking来禁止偏向锁优化反而可以提升性能。