重入锁是可以完全替代synchronized关键字的,在jdk 5.0的早期版本中,重入锁的性能远远高于synchronized的,但是从JDK 6.0开始,jdk在synchronized上做了大量优化,使得两者的性能差距并不是很大。
重入锁使用java.util.concurrent.locks.ReentrantLock类来实现的,先看个例子
package com.example.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++){
lock.lock();
try{
i ++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock reenterLock = new ReenterLock();
Thread t1 = new Thread(reenterLock);
Thread t2 = new Thread(reenterLock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
上述代码中,在进行i的累加时,使用了重入锁。由此可以看出,重入锁和synchronized锁,有着显示的操作过程,也就是必须手动加锁和手动释放锁。也因为这样,重入锁的灵活性要比synchronized要好。在使用完重入锁时,一定要记得释放,不然就会导致死锁。
锁就叫锁呗,为啥非要加个”重入“两个字呢,那是因为这种锁,在一个线程中是可以反复进入的,例如
lock.lock();
lock.lock();
try{
i ++;
} finally {
lock.unlock();
lock.unlock();
}
这样也是可以的,但是加锁的数量与释放锁的数量一定要一致,如果加锁多,释放锁少就会出现死锁,锁无法释放,其他线程也无法获得。如果释放的锁多了,就会报一个错,如图:
重入锁除了,可以重入之外,还提供了一些其他的高级功能。
1. 中断响应
对于synchronized来说,如果一个线程在等待锁,那么他的结果只有两种,第一,获得锁进入临界区,第二,继续等待。而重入锁提供了另外一种可能,那就是线程可以被中断,线程可以根据需要取消对锁的请求。比如,你和朋友约好一起去打球,如果你等了半个小时,朋友还没有到。突然接到一个电话,说由于突发情况,不能如约,那么你一定就扫兴打到回府。中断就是提供了一种类似这样的机制,如果一个线程在等待锁,但是突然被告知不用等了。这种情况对死锁有一定的帮助。例如:
package com.example.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1){
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getId() + " :线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
线程t1,t2启动后,t1先抢占了lock1锁,再占用lock2锁,t2先占用lock2锁,再请求lock1锁。因此很容易形成相互等待,也就是死锁。这里获取锁使用的是lockInterruptibly()方法,这是一个可被中断的锁。在并发包里,如果方法名上看到类似Interruptibly这样的单词,基本上都是表示,可被中断的,如果前面加上Un即表示不可中断的。main方法的最后一行,将t2线程进行了中断,t2放弃了对lock1的锁申请,也释放了lock2,使得t1得到了lock2,顺利的执行完成。
2. 锁申请等待限时
除了避免等待外部通知之外,避免死锁还有一种方法,就是限时等待。还是约朋友去打球,如果你到场后等了几个小时,朋友还没有去,那你一定会扫兴的离开。对于线程也是一样,我们无法判断线程为什么迟迟拿不到锁,也许是产生了饥饿,也许是产生了死锁。但是如果给定一个时间,让线程主动放弃,那么对系统来说,是非常有必要的。例如:
package com.example.thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)){
Thread.sleep(6000);
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()){}
lock.unlock();
}
}
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
Thread t1 = new Thread(timeLock);
Thread t2 = new Thread(timeLock);
t1.start();
t2.start();
}
}
这里加锁用了tryLock()方法,这个方法接收两个参数,一个表示计时时长,一个表示计时单位。这里设置了5秒,表示线程在获取这个锁时,最多等待5s的时间。
tryLock()方法也可以不传递参数,不传递任何参数表示不等待锁,如果线程在获取锁时,发现锁被占用,此时直接返回false,这种情况不会引起线程等待,更不会产生死锁,下面代码演示了这种情况
package com.example.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
if (lock == 1){
while (true){
if (lock1.tryLock()){
try {
try {
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + " : My Job done");
return;
} finally {
lock2.unlock();
}
}
}finally {
lock1.unlock();
}
}
}
} else {
while (true){
if (lock2.tryLock()){
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (lock1.tryLock()){
try {
System.out.println(Thread.currentThread().getId() + " : My job done");
return;
} finally {
lock1.unlock();
}
}
}finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
代码很简单自己看。。。。。。
3.公平锁
大多数情况下,锁的申请都是不公平的,比如,线程A申请了锁,线程B也申请了这个锁,当锁可用时,是A用,还是B用,这个是随机的,并不是先等待先使用。而公平锁则是,谁先申请的,谁先使用。那么就是需要有个队列存储这些先后顺序,所以想要获得公平就要牺牲掉性能。synchronized就是非公平锁。而重入锁允许我们对其进行设置。他提供了个如下的构造函数:
public ReentrantLock(boolean fair)
当fair为true时,表示锁是公平锁,上代码:
package com.example.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class FairLock implements Runnable {
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException{
FairLock r1 = new FairLock();
Thread t1 = new Thread(r1, "Thread-1");
Thread t2 = new Thread(r1, "Thread-2");
t1.start();
t2.start();
}
}
对上面ReentrantLock的几个重要方法做个总结:
- lock():获取锁,如果锁被占用则等待。
- lockInterruptibly():获取锁,但优先响应中断。
- tryLock():尝试获得锁,成功返回true,不成功返回false,该方法不等待,直接返回
- tryLock(long time, TimeUnit unit):给定时间内尝试获取锁,超时返回false
- unlock():释放锁。
在重入锁的实现中,主要包含三个要素:
1. 原子态。原子态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程所有。
第二,等待队列。所有线程没有请求到锁时,都会进入等待队列中进行等待。待有线程释放锁后,系统就能2. 从等待队列中,唤醒一个线程,继续工作。
3. 是阻塞源于park()和unpark(),用来挂起和恢复线程,没有得到锁的线程将会被挂起。(后面会介绍敬请期待)。
4. 重入锁的好搭档:Condition
如果你理解了Object.wait()和Object.notify()方法的话,那么就很容易理解Condition了。不理解的可以翻回去看多线程基础知识
Condition接口提供的基本方法如下:
- await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获取锁,并继续执行。或者当线程被中断时,也能跳出等待。
- awaitUninterruptibly()方法,与await()方法基本相同,但是他并不会在等待过程中响应中断,
- signal()方法用于唤醒一个在等待的线程,signalAll()则是唤醒所有等待中的线程。
上代码:
package com.example.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition reenterLockCondition = new ReenterLockCondition();
Thread t1 = new Thread(reenterLockCondition);
t1.start();
Thread.sleep(2000);
lock.lock();
condition.signal();
lock.unlock();
}
}
与wait()和notif()方法需要在synchronized锁内一样,condition也要在Lock锁之内才行
4. 允许多个线程同时访问:信号量(Semaphore)
信号量为多线程之间的协作提供了更为强大的支持。广义上说是对锁的扩展,无论是内部锁synchronized还是重入锁,ReentrantLock,一次都只允许一个线程访问资源,而信号量却可以指定多个线程,同时访问某个资源
Semaphore的构造方法有两个Semaphore(int permits),Semaphore(int permits, boolean fair),第一个只指定了有多少个线程可以同时访问,第二个还制定了访问是否公平
Semaphore类的主要方法:
- acquire():尝试获取一个准入许可,若无则等待直到有线程释放,或者被中断。
- acquireUninterruptibly(int permits):和acquire()一样,只是不响应中断
- tryAcquire():尝试获取一个准入许可,不等待,直接返回false或者返回true
- tryAcquire(long timeout, TimeUnit unit):可以设定等待时间和时间的单位
- release():释放一个许可
上代码:
package com.example.thread;
import sun.text.resources.FormatData_en_IN;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* Created by mazhenhua on 2017/3/8.
*/
public class SemapDemo implements Runnable {
final Semaphore semp = new Semaphore(5);
@Override
public void run() {
try {
semp.acquire();
// 模拟耗时操作
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + " : done!");
semp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemapDemo demo = new SemapDemo();
for (int i = 0; i < 20; i++){
exec.submit(demo);
}
}
}
运行上面的代码,你会发现,输出是5个一组被输出的。