1.synchronized的功能扩展:重入锁
重入锁可以完全替代synchronized关键字。在JDK5.0的早期版本中,重入锁的性能远远的好于synchronized,但是从JDL6.0开始,JDK在synchronized上做了大量的优化,使两者的性能差距并不大。
重入锁使用java.util.concurrent.locks.ReentrantLock类来实现,我们先看一段简单的重入锁的使用案例:
/**
* @ClassName ReenterLock
* @Description 重入锁简单实现方式
* @Author JinDuoWang
* @Email wangjinduoliuxi@163.com
* @Date 9:30 2019/1/18
* @Version 1.0
**/
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);
}
}
这就是一种简单的重入锁实现,重入锁对逻辑控制的灵活性要远远好于synchronized。
在这里肯定有个疑问,为什么叫重入锁呢,这不就是个锁吗?之所以这样叫是因为这种锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程,看个示例:
// 在这种情况下,一个线程同时获得同一把锁,这是允许的,如果不允许的话同一个线程在第二次获得锁的时候,将会和自己产生死锁。
// 但是需要注意的是,如果一次线程获得多个锁,那么你在释放的时候也必须释放相同的次数,如果释放的次数多,会抛出一个java.lang.IllegalMonitorStateException异常,
// 反之,如果你加锁两次,但是你只释放了一次,那么后面的线程就永远无法进入临界区。
lock.lock();
lock.lock();
try {
i++;
} finally {
lock.unlock();
lock.unlock();
}
2.中断响应
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得了这个锁,要么就保持等待。而是用重入锁的话还会有另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,可根据需求来取消对锁的请求。
下面的代码产了一个死锁,但得益于锁中断,我们可以很轻松的解决这个死锁。
/**
* @ClassName IntLock
* @Description 死锁模拟
* @Author JinDuoWang
* @Email wangjinduoliuxi@163.com
* @Date 9:56 2019/1/18
* @Version 1.0
**/
public class IntLock implements Runnable {
public static ReentrantLock reentrantLock1 = new ReentrantLock();
public static ReentrantLock reentrantLock2 = new ReentrantLock();
int lock;
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock ==1) {
reentrantLock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
reentrantLock2.lockInterruptibly();;
} else {
reentrantLock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
reentrantLock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (reentrantLock1.isHeldByCurrentThread()) {
reentrantLock1.unlock();
}
if (reentrantLock2.isHeldByCurrentThread()) {
reentrantLock2.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);
}
}
输出内容如下:
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.wjd.reenterLock.IntLock.run(IntLock.java:43)
at java.lang.Thread.run(Thread.java:748)
11线程退出
12线程退出
可以看出只有t1真正的完成任务后退出,t2则直接退出,释放资源。
3.锁申请等待限时
除了等待外部通知以外,要避免死锁还有另一重方法,那就是限时等待tryLock(),下面我们来看一下tryLock()简单的实现方式:
/**
* @ClassName TimeLock
* @Description 等待现实
* @Author JinDuoWang
* @Email wangjinduoliuxi@163.com
* @Date 10:15 2019/1/18
* @Version 1.0
**/
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("获取锁失败;");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
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秒,如果超过5秒还没有得到锁,就会返回false。如果成功获得锁就返回true。
在本例中我们让线程休眠了6秒,让tryLock()获取不到锁,我们来看一下输出结果:
获取锁失败;
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.wjd.reenterLock.TimeLock.run(TimeLock.java:30)
at java.lang.Thread.run(Thread.java:748)
有没有感觉到tryLock用起来很爽。
tryLock我们就说到这里。
4.公平锁
在大多数的情况下锁的申请都是非公平的,也就是说线程1请求了锁A,紧接着线程2也请求了锁A,那么当锁A可用的时候,是线程1能抢到锁A还是线程2呢?这是不一定的,系统会随机挑选一个,因此不能保证其公平性。如果我们使用的是synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们设置公平性,他有一个如下的构造函数:
public ReentrantLock(boolean fair)
当fair等于true的时候,表示锁是公平的。公平锁固然是好,能保证每个线程都能够竞争到锁,但是要实现公平锁,系统必然要维护一个有序队列,因此公平锁的实现成本比较高,性能也非常低下,因此默认的情况下,都会使用非公平的,如果没有特殊的需求,也不需要使用公平锁。公平锁和非公平锁的线程调度也是不一样的,下面我们来看一下公平锁:
/**
* @ClassName FairLock
* @Description 公平锁
* @Author JinDuoWang
* @Email wangjinduoliuxi@163.com
* @Date 15:01 2019/1/18
* @Version 1.0
**/
public class FairLock implements Runnable {
// 将fair设置为true,则为公平锁
public static ReentrantLock reentrantLock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + ":获得了锁");
} finally {
reentrantLock.unlock();
}
}
}
public static void main(String[] args) {
FairLock fairLock = new FairLock();
Thread t1 = new Thread(fairLock, "THREAD-O1");
Thread t2 = new Thread(fairLock, "THREAD-O2");
t1.start();
t2.start();
}
}
在代码中,我们把fair设置为true,则指定锁为公平锁。接着t1和t2分别请求这把锁,并且在得到锁之后,进行一个输出,表示自己获得了锁,在公平锁的情况下,得到的输出通常是这样的:
THREAD-O1:获得了锁
THREAD-O2:获得了锁
THREAD-O1:获得了锁
THREAD-O2:获得了锁
THREAD-O1:获得了锁
THREAD-O2:获得了锁
THREAD-O1:获得了锁
THREAD-O2:获得了锁
THREAD-O1:获得了锁
THREAD-O2:获得了锁
代码会产生大量的输出,这里为了展示我只是截取了一部分拿来进行说明。在这个输出中明显可以看到,两个线程交替获得锁,你一次我一次非常和谐,几乎不会发生一个线程多次获得锁的可能,从而公平锁也得到了保证,我们再来看一下非公平锁,首先我们把代码修改一下,把fair设置为false或者直接不传(默认就是非公平的),修改完以后我们在来看一下输出的结果:
前面还有很大一片THREAD-01的输出
THREAD-O1:获得了锁
THREAD-O1:获得了锁
THREAD-O1:获得了锁
THREAD-O1:获得了锁
THREAD-O1:获得了锁
THREAD-O1:获得了锁
THREAD-O2:获得了锁
THREAD-O2:获得了锁
THREAD-O2:获得了锁
THREAD-O2:获得了锁
THREAD-O2:获得了锁
后面还有很大一片THREAD-02的输出
可以看到,根据系统的调度,一个线程会倾向于再次获取已持有的锁,这种分配方式是高效的,但是根本毫无无公平性。
对上面的ReentrantLock的几个重要的方法整理如下。
方法名 |
介绍 |
lock() |
获取锁,如果锁已经被占用,则等待。 |
lockInterruptibly() |
获得锁,但是优先相应中断。 |
tryLock() |
尝试获得锁,如果成功返回true,如果失败返回false。该方法不等待,立即返回。 |
tryLock(long time, TimeUnit unit) |
在给定的时间内,尝试获得锁。 |
unlock() |
释放锁。 |
就重入锁来看,他主要集中在Java层面。在重入锁的实现中,主要包含三个要素:
第一,是原子状态。原子状态使用CAS操作(会在之后的博文中讲解)来存储当前锁的状态,判断是否已被别的锁持有。
第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就会从等待的队列中唤醒一个线程,继续工作。
第三,是阻塞原语park()和unpark(),用来挂起和回复线程。没有获得锁的线程将会被挂起。
Ps:有关park()和unpark()的详细介绍会在后面介绍。