java线程学习(六):JDK并发包之重入锁-ReentrantLock

本章将学习重入锁的以下方法:

  1. lock(): 获得锁,如果锁已被占用,则等待;
  2. lockInterruptbly(): 获得锁,但优先响应中断;
  3. tryLock(): 尝试获得锁,如果成功,返回true,反之返回false,该方法不会等待,执行则立即返回,可用tryLock(long time,TimeUnit unit)重载方法设置等待时间;
  4. unlock(): 释放锁。

一、重入锁的理解

  1. 定义:
    重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。
  2. 需要解决两个问题:
    线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取;
    锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放
  3. 重入锁实现重入性:
    每个锁关联一个线程持有者和计数器,当计数器为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()要成对出现,否则就会造成死锁状态。

扫描二维码关注公众号,回复: 4841397 查看本文章
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。也就是直接抛异常了,所以后面的工作是无法进行的。

总结:

  1. lock()代替synchronized();
  2. lockInterruptibly()类似于线程的isInterrupted()+lock(),也就是如果线程没有中断,则上锁,如果线程中断了,那么就中断该线程,不走了。

猜你喜欢

转载自blog.csdn.net/shenhaiyushitiaoyu/article/details/85067876