7、线程同步机制(锁)
- java线程同步机制是java多线程基础和核心内容。前面知道导致线程安全问题的根源是硬件和编译器,但是从应用程序的角度看,线程安全的产生是由于多线程程序缺少一种东西-线程同步机制。线程同步机制是一套用于协调线程间的数据访问及活动的机制,该机制用于保障线程安全及实现这些线程的共同目标。
- java平台提供的线程同步机制包括锁、valotile关键字、final关键字、static关键字以及一些相关的API(如wait、notify)
一、锁
- 线程安全问题的产生是由于多个线程同时并发访问共享数据。而锁保障线程安全的思路就是一次只能一个线程访问共享数据,也就是将多个线程并发访问数据转化为串行访问共享数据。
(1)一个线程在访问共享数据时候,必须先去申请锁(类似许可证),这个工作成为锁的获得,一个线程获得了锁,就称该线程为相应锁的持有线程,一个锁一次只能被一个线程持有。(2)锁的持有线程可以对所保护的资源进行访问,访问结束后该线程必须释放相应的锁。
- 锁的持有线程在获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区。共享数据只能在临界区中进行访问,临界区一次只能被一个线程访问。
- 锁具有排他性,一次只能被一个线程所持有,因此锁被称为排他锁或者互斥锁。java平台的锁包括内部锁(sychronized)和显示锁(ReentrantLock)
1、锁的作用
- 锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可将性、保障有序性
(1)保障原子性
锁的互斥性保证了原子性。因为互斥性,一个锁一次只能被一个线程所持有,其他线程只能等待持有锁的线程释放锁才能有机会获取锁,这保证了一个线程在执行临界区时候,其他线程不能够访问共享数据,使得临界区的代码自然变成不可分割的特性,及具备了原子性。
(2)保障可见性
可见性保证是通过写线程冲刷处理器缓存和读线程刷新处理器缓存完成的。而锁的获得隐含了刷新处理器缓存的动作,而锁的释放隐含了冲刷处理器缓存的工作,因此锁能够保障可见性。(实际上底层是通过内存屏障来实现的)
由于锁的互斥性和可见性保障合并在一起,因此锁能都保障临界区内的代码能够读取到共享数据的最新值。
(3)保障有序性
写线程在临界区中所执行的一些列操作在读线程所执行临界区看起来像是完成按照源代码的顺序执行的,即读线程对这些操作的感知顺序与源代码顺序一致。保障了顺序性,但是这不意味着在临界区中的代码不能够被重排序,这种在临界区中的重排序并不会影响线程的安全性。(实际上底层是也是通过内存屏障来实现的)
总结:
(1)线程访问同一组数据的时候必须使用同一个锁
(2)任何一个线程,即时仅仅是读取这组共享数据而没有对其进行更新的话,也需要持有相应的锁
2、锁的几个重要概念
(1)可重入性
- 一个线程如果持有一个锁还能继续成功申请该锁,那么称这个锁是可重入的。
(2)锁的调度 - java平台内锁的调度包括公平策略和非公平策略,相应非锁被称为公平锁和非公平锁、内部锁仅支持非公平锁,外部锁支持公平锁和非公平锁。
(3)锁的粒度 - 一个锁实例多保护的数据的数量大小称为锁的粒度,数据的量大锁的粒度就粗,否则粒度细。
3、锁的开销以及可能导致的问题
- 锁的开销包括申请和释放所产生的开销,以及锁可能导致的上下文切换的开销。锁是一种比较消耗资源的同步方式。
- 锁的不正确使用可能导致一些线程活性故障(锁泄露、活锁、死锁等)
二、sychronized
- java任何一个对象都有一个之于对应的锁,这个锁称为内部锁或者监视器。内部锁通过sychronized关键字实现。
- sychronized关键字修饰的方法称为同步方法,修饰的静态方法称为同步静态方法,修饰的实例方法成为同步实例方法。同步方法的整个方法体就是一个临界区。同时sychronized还可以修饰代码块。
1、作为锁句柄的变量常用final修饰,因为要保证只有的锁一样。比如private final Object object=new Object;
2、线程对内部锁的申请和释放的动作是jvm负责代为实施的,内部锁不会造成锁泄露问题。
三、显示锁:Lock接口
-
显示锁是jdk1.5引入的,提供了一些内部锁不具备的特性,也是一种线程同步机制。
显示锁的api
1、显示锁使用步骤
1、创建Lock接口的实例(无特别要求,默认实现类ReentrantLock)
2、在访问共享数据前申请相应的显示锁,lock.lock()
3、在临界区访问共享变量
4、共享数据访问结束后释放锁。lock.unlock()
private final Lock lock = new ReentrantLock();
private int sequence = 1;
public int nextSequence() {
lock.lock();
try {
if (sequence > 100) {
sequence = 0;
} else {
sequence++;
}
return sequence;
} finally {
lock.unlock();
}
}
2、显示锁的调度
- 显示锁支持公平和非公平两种调度方式(public ReentrantLock(boolean fair)),默认是非公平调度,因为要实现公平调度需要额外的开销。
3、显示锁和内部锁的比较
(1)内部锁是基于代码块的锁,基本无灵活性可言。显示锁是基于对象的锁,可以充分发挥面向对象的灵活性。(比如显示锁的锁释放可以跨方法)
(2)内部锁简单易用,且不会造成内存泄露。显示锁会造成内存泄露,需要在finally中释放锁资源。
(3)内部锁可能会因为某种原因造成阻塞,而使其他的任务无法完成。但是显示锁可以通过tryLock解决配合if从句解决。
if (lock.tryLock()) {
//访问共享数据
try {
} finally {
lock.unlock();
}
} else {
//执行其他操作
}
(4)锁的调度方面,内部锁仅支持公平性调度,而显示锁支持公平和非公平调度。
(5)显示锁提供了一些api可以用来对锁的相关信息进行监控,而内部锁不支持
(6)在jdk1.6之前,显示锁的性能优于内部锁,jdk1.6之后,对内部锁做了很多优化,显示锁和内部的性能差不多。
四、读写锁
- 读写锁是一种改进型的排他锁,也被称为共享/排他锁.读写锁允许多个线程同时读取共享数据,但是一次只允许一个线程对共享变量进行更新。任何的线程在读取这个共享变量时候,其他线程无法更新这些变量;一个线程更新共享变量的时候,其他线程都无法访问该共享变量。
-
读锁是共享的,写锁是排他的。读锁对于读线程来说起到保护其访问的共享变量在访问期间不被修改的作用,并使多个读线程同时读这些变量而提供了并发性;而写锁保障了写线程能够独占的方式安全的更新共享变量。
读写锁 - jdk中ReadWriteLock接口对读写锁进行了抽象,默认实现类是ReentrantReadWriteLock
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void reader() {
readLock.lock();
try {
//在此区域进行读取共享数据
} finally {
readLock.unlock();
}
}
public void writer() {
writeLock.lock();
try {
//在此区域进行读、写共享数据
} finally {
writeLock.unlock();
}
}
读写锁和锁的对比:
(1)读写锁和排他锁一样也能保障原子性、可见性、有序性。但是读写锁内部实现复杂,开销比排他锁大
(2)读写锁的使用场合(只读操作比写操作频繁的多、读线程持有锁的时间比较长)
五、锁总结
- 锁是java线程同步机制中功能最强大、适用范围最广的、同时也是开销最大、可能导致的问题最大的线程同步方式。方式涉及线程安全的场合就可以考虑使用锁。
1、check-then-act操作(if-do)
2、read-modify-write操作(i++)
3、多个线程对多个共享数据进行更新时候