Java的锁机制
参考
https://zhongfucheng.bitcron.com/post/duo-xian-cheng/javasuo-ji-zhi-liao-jie-yi-xia
http://cmsblogs.com/?page_id=111
Java显示锁lock
- 获取lock的几种方法
lock : 在锁上等待,直到获取锁;
tryLock:立即返回,获得锁返回true,没获得锁返回false;
tryInterruptibly:在锁上等待,直到获取锁,但是会响应中断,这个方法优先考虑响应中断,而不是响应锁的普通获取或重入获取。 - condition 用法 ???
1 同步锁 Synchronized 关键字
2 互斥锁 重入锁ReentrankLock 实现concurrent包中的lock接口
3 读写锁–ReadWriteLock接口及其实现类ReentrantReadWriteLock
4 Semaphore信号量
http://jison.site/2016/09/05/JavaThread3/#%E7%BA%BF%E7%A8%8B%E9%80%9A%E4%BF%A1
http://coolxing.iteye.com/blog/1236909
http://smallbug-vip.iteye.com/blog/2275743
1 同步锁 Synchronized 关键字
同步(synchronized)和监视器机制(主要是Object类的wait(), notify(), notifyAll()方法).
在并发的情况下,多个线程一起访问同一个资源时会出现线程同步的问题。
很多程序的bug都是由于线程不同步而造成的,解决线程不同步的方法最常用的是使用Java的锁机制对代码进行加锁,而加锁的方法不同和加锁的范围不同又分几种情况。除此之外还可以使用特殊域变量(volatile)实现线程同步。
那么在什么时候需要用到线程同步呢?前面说了,多个线程访问同一个资源的时候就需要对代码进行同步操作了。
比如对文件的读写操作,多个线程同时对某个变量进行修改,对数据库的增删改查的访问等等。
使用synchronized关键字进行加锁
在Java里面,每个类实例对应一把锁,在使用synchronized对同代码块或者方法进行加锁之后,多个线程同时访问加锁代码时,必须要先获取该实例对象的锁才可以执行这一段代码,并且在这段代码执行过程中,其它线程无法获取这个对象的锁,也就无法进入这段同步代码块或者同步方法中了,确保了并发时多线程的同步问题,这就是Java对象锁的排他性。
使用synchronized关键字对代码块进行加锁
在并发编程中,使用synchronized关键字对代码加锁是很消耗性能的一件事,而有时候我们只需要对一部分代码进行同步操作就可以了,这样相对来说程序性能会好一点,这样的操作我们称为同步代码块。
使用synchronized关键字对实例方法进行加锁
对实例方法加锁只需要在方法签名上加一个synchronized修饰符即可。当一个类中有多个被synchronized关键字修饰的同步方法时,并且同一时间有一个线程进入其中一个同步方法时,别的线程就只能这个线程将这个同步方法执行完毕,才能进入执行这个同步方法或者该类下别的实例同步方法。
使用synchronized关键字对类进行加锁
2 互斥锁 重入锁ReentrankLock 实现concurrent包中的lock接口
所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作, Lock和synchronized机制的主要区别:
synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放. 而Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 以下代码演示了在不同的块结构中获取和释放锁:
Java代码 收藏代码
public class LockTest {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
invokeMethod();
}
private static void invokeMethod() {
lock.unlock();
}
}
为了确保锁被释放, 通常会采用如下的代码形式:
Java代码 收藏代码
Lock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
// access the resource protected by this lock
} finally {
// 释放锁
lock.unlock();
}
|--void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
|--void unlock(): 执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
Lock提供了一个非块结构的获取锁尝试–tryLock(), 一个获取可中断锁的尝试–lockInterruptibly()和一个获取超时失效锁的尝试–tryLock(long time, TimeUnit unit).
|–boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和lock()的区别在于, tryLock()只是”试图”获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 通常采用如下的代码形式调用tryLock()方法:
Java代码 收藏代码
Lock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
此用法可确保如果获取了锁, 则会释放锁; 如果未获取锁, 则不会试图将其释放.
Lock的newCondition()方法可以获得与该锁绑定的Condition对象, Condition的详细介绍如下.
条件–Condition
调用Condition对象的相关方法, 可以方便的挂起和唤醒线程. Object对象的wait(), notify(), notifyAll()方法当然也可以做到这一点, 但是Object对象的这些方法存在很不方便的地方–如果多个线程调用了obj的wait()方法而挂起, 那么我们无法做到调用obj的notify()和notifyAll()方法唤醒其中特定的一个线程. 而Condition对象就可以做到这一点. 具体的代码请参见我的上一篇博客http://coolxing.iteye.com/blog/1236696中的解法二部分.
void await(): 调用Condition对象的await()方法将导致当前线程被挂起, 并释放该Condition对象所绑定的锁. Condition对象只能通过Lock类的newCondition()方法获取, 因此一个Condition对象必然有一个与其绑定的Lock锁. 调用Condition对象的await()方法的前提是: 当前线程必须持有与该Condition对象绑定的锁, 否则程序可能抛出异常.
void signal(): 唤醒一个在该Condition对象上挂起的线程. 如果存在多个线程等待这个Condition对象的唤醒, 则随机选择一个. 线程被唤醒之前, 必须重新获取到锁(与该Condition对象绑定的Lock对象).
void signalAll(): 唤醒所有在该Condition对象上挂起的线程. 所有被唤醒的线程将竞争与该Condition对象绑定的锁, 只有获取到锁的线程才能恢复到运行状态.
3 读写锁–ReadWriteLock接口及其实现类ReentrantReadWriteLock
ReentrantReadWriteLock中定义了2个内部类, ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock, 分别用来代表读取锁和写入锁. ReentrantReadWriteLock对象提供了readLock()和writeLock()方法, 用于获取读取锁和写入锁.
读取锁允许多个reader线程同时持有, 而写入锁最多只能有一个writter线程持有.
读写锁的使用场合: 读取共享数据的频率远大于修改共享数据的频率. 在上述场合下, 使用读写锁控制共享资源的访问, 可以提高并发性能.
如果一个线程已经持有了写入锁, 则可以再持有读写锁. 相反, 如果一个线程已经持有了读取锁, 则在释放该读取锁之前, 不能再持有写入锁.
可以调用写入锁的newCondition()方法获取与该写入锁绑定的Condition对象, 此时与普通的互斥锁并没有什么区别. 但是调用读取锁的newCondition()方法将抛出异常.
线程通信
当多个线程在系统中运行时,线程之间的切换具有一定的随机性。程序一般无法准确的在多个线程之间进行切换,但是Java也提供了一些机制来使多个线程之间能够协调的运行,这就是线程通信。
根据实现线程同步的方式的同步,线程通信的方式也有所不同。
synchronized关键字 使用Object类的wait()、notify()和notifyAll()方法
使用synchronized关键字修饰的同步代码块和同步方法的线程之间的通信可以使用Object类的wait()、notify()和notifyAll()方法进行通信。
wait()方法有三个重载的形式,Java6的文档上是这么写的:
wait()方法,在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
wait(long timeout)方法,在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
wait(long timeout, int nanos)方法,在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
而notify()方法和notifyAll()方法则是唤醒在此对象监视器上等待的线程,不同的是notify()方法是唤醒单个线程,而notifyAll()方法则是唤醒所有线程。
对于同步代码块而言,this就是同步监视器。
对于同步方法而言,synchronized后括号里面的对象就是同步监视器。
使用Lock和ReadWriteLock接口的实现类手动加锁的线程通信使用Condition实现线程通信
使用Lock和ReadWriteLock接口的实现类手动加锁的线程通信不像synchronized关键字同步的线程存在隐式的同步监视器,所以也就不能使用wait()、notify()和notifyAll()方法来进行通信了。
但是,就跟前面介绍的时候说的,Lock手动加锁和synchronized关键字加锁有相似之处。使用Condition实现线程通信和使用wait()、notify()和notifyAll()方法来进行通信也是有相似之处的。
Lock接口声明了一个newCondition()方法,这个方法可以用来获得一个Condition对象,这个对象就相当于使用synchronized加锁时的同步监视器,里面有三个类似wait()、notify()和notifyAll()的方法,分别是await()、signal()和signalAll()方法,功能也是一样的。
从Java6的文档上可以看到这样的声明:
await()方法,造成当前线程在接到信号或被中断之前一直处于等待状态。
signal()方法,唤醒一个等待线程。
signalAll()方法, 唤醒所有等待线程。
4 Semaphore信号量
https://www.jianshu.com/p/fa084227c96b
信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的拓展。无论是synchronize还是重入锁,一次都只运行一个线程访问一个资源,而信号锁则可以指定多个线程,同时访问某一个资源。
就是Semaphore仅仅是对资源的并发访问的任务数进行监控,而不会保证线程安全,因此,在访问的时候,要自己控制线程的安全访问。
像下面的代码, MyRunnable被加锁的代码块一次会被5个线程执行:
public class MyRunnable implements Runnable {
private Semaphore mSemaphore;
public MyRunnable(Semaphore semaphore) {
mSemaphore = semaphore;
}
@Override
public void run() {
try {
mSemaphore.acquire();
Thread.sleep(2000);
System.out.println("thread " + Thread.currentThread().getId() + " working");
mSemaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 19; i++) {
new Thread(new MyRunnable(semaphore)).start();
}
Thread t = new Thread(new MyRunnable(semaphore));
t.start();
t.join();
Semaphore.acquire()方法尝试获得一个准入许可。如无法获得,线程就会等待。而Semaphore.release()则在线程访问资源结束后,释放一个许可。
Semaphore有下面的一些常用方法:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
可重入锁ReentrankLock 与 可重入读写锁ReentrankReadWirteLock 比较
ReentrankLock是一种排他锁,即同一时间只能有一个线程进入。而读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读写锁,使得并发性比一般的排它锁有了很大提升。因为大多数应用场景都是读多于写的,因此在这样的情况下,读写锁可以提高吞吐量。下图描述了关于读写锁的三个特性:公平性、重入性和锁降级。
死锁
理解:两个线程同时获取对方的锁,等待对方释放锁,进入死循环,于是发生死锁
死锁条件 四个都成立才发生死锁
- 资源独占:一个资源在同一时刻只能被分配给一个进程。
- 不可剥夺:资源申请者不能强行地从资源占有者的手上夺取资源。
- 保持申请:进程在占有部分资源后还可以申请新的资源,而且在申请新的资源时并不释放他已经占有的资源。
- 循环等待:存在某一进程等待序列,可以使其中的进程出现如图所示的申请队列。
如何解决死锁
破坏死锁条件中的一个即可
https://juejin.im/entry/593e5db6ac502e006c0d6264
加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。加锁时限
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。
需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。
此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)
这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。
3.死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。
当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。
那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
三、自旋锁
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/21/2602015.html
自旋锁它是为为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
自旋锁一般原理
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。由此我们可以看出,自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:死锁和过多占用cpu资源。
自旋锁适用情况
自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。另外格外注意一点:自旋锁不能递归使用。