本章将学习重入锁的以下方法:
- lock(): 获得锁,如果锁已被占用,则等待;
- lockInterruptbly(): 获得锁,但优先响应中断;
- tryLock(): 尝试获得锁,如果成功,返回true,反之返回false,该方法不会等待,执行则立即返回,可用tryLock(long time,TimeUnit unit)重载方法设置等待时间;
- unlock(): 释放锁。
一、重入锁的理解
- 定义:
重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。 - 需要解决两个问题:
线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取;
锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放 - 重入锁实现重入性:
每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。
简单的例子来加深不可重入锁和重入锁的理解:
不可重入锁
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
使用该锁:
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。
可重入锁
接下来,我们设计一种可重入锁(注意有线程持有者+计数器的设计思想)
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;//计数器
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;//计数器++
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。
我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量lockedCount并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同一进程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才将标记为isLocked设置为false。
可重入锁的概念和设计思想大体如此,Java中的可重入锁ReentrantLock设计思路也是这样.
二、lock()的使用:
如果理解了上面重入锁的例子,那么lock的使用是非常的简单的,只需要在要加锁的地方加上锁就可以了,但要注意的是,加锁lock()和释放锁unlock()要成对出现,否则就会造成死锁状态。
package stop_demo;
import java.util.concurrent.locks.ReentrantLock;
public class Lock_demo implements Runnable{
private static ReentrantLock lock =new ReentrantLock();
private static int i =0;
@Override
public void run() {
try {
lock.lock();//加锁
for (int j=0; j < 1000; j++) {
i++;
}
System.out.println(Thread.currentThread().getName()+" "+i );
} finally{
lock.unlock();//撤销该条语句下一条线程就会无法获得锁了
}
}
public static void main(String[] args) throws InterruptedException {
Lock_demo demo=new Lock_demo();
new Thread(demo).start();
new Thread(demo).start();
Thread.sleep(1000);
System.out.println("i= "+i);
}
}
还可以多个锁结合使用:
try {
lock.lock();//加锁
lock.lock();//加锁
for (int j=0; j < 1000; j++) {
i++;
}
} finally{
lock.unlock();//撤销该条语句下一条线程就会无法获得锁了
lock.unlock();//撤销该条语句下一条线程就会无法获得锁了
}
可以看得出,lock()的使用,比synchronized()用起来无论是逻辑上,还是代码的简洁上,都比synchronized()好用,实际上,lock()的性能远比超过synchronized(),当然在jdk1.5以后,synchronized()的性能得到了较大的优化了,目前两者区别不大,不过还是建议平时多用lock(),用着用着就会更加习惯了。
三、lockInterruptbly()的使用
先看代码,然后执行:
package stop_demo;
import java.util.concurrent.locks.ReentrantLock;
public class Lock_demo implements Runnable{
private static ReentrantLock lock1 =new ReentrantLock();
private static ReentrantLock lock2 =new ReentrantLock();
private int lockNum;//纯粹是为了代码里面判断走哪一条
public Lock_demo(int lockNum){
this.lockNum=lockNum;
}
@Override
public void run() {
try {
if (lockNum==1) {
lock1.lockInterruptibly();//a行
try {
System.out.println(Thread.currentThread().getName()+"在工作。。。");
Thread.sleep(2000);//睡两秒 sleep不会释放锁,但是锁是使用lockInterruptibly()上锁的,所以会优先考虑中断情况的,所以如果这条线程被中断了,那么就不会往下执行了
System.out.println(Thread.currentThread().getName()+"在执行其他代码。。。。。。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
lock2.lockInterruptibly();//b行
System.out.println(Thread.currentThread().getName()+"在工作。。。");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"在执行其他代码。。。。。。。。。。");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
System.out.println("有线程中断操作了,该线程是: "+Thread.currentThread().getName());
// e.printStackTrace();
}finally{
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Lock_demo demo1=new Lock_demo(1);
Lock_demo demo2=new Lock_demo(2);
Thread thread1=new Thread(demo1,"线程1");
Thread thread2=new Thread(demo2,"线程2");
thread1.start();
thread2.start();
thread1.interrupt();//中断线程1 ------c行
Thread.sleep(1000);
}
}
执行后输出:
可以看到,在c行主线程中,中断线程1后,a行就会判断,线程1中断了,然后就会抛出异常,finally中进行锁的释放,让线程2去执行,该过程中,实际完成工作的仅仅是线程2,线程1是没有工作的(看输出就知道线程1没有输出,证明不往下执行了,中断了该线程了)。值得注意的是,当线程已经被中断了(isInterrupted()返回true)。则线程使用lock.lockInterruptibly(),直接会被要求处理InterruptedException。也就是直接抛异常了,所以后面的工作是无法进行的。
总结:
- lock()代替synchronized();
- lockInterruptibly()类似于线程的isInterrupted()+lock(),也就是如果线程没有中断,则上锁,如果线程中断了,那么就中断该线程,不走了。