10.J.U.C-同步控制之重入锁(ReentrantLock)

前言

       ReeterLock和synchronized具有相同的内存语义;

      0.与sysnchronized相比,重入锁具有显示的操作过程,开发人员必须指定何时加锁,何时释放锁。因此,重入锁更加灵活;   

       1.与synchronized相比,ReentrantLock功能更加强大;

       2.ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合;

      3.ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。

       4.ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。

        5.ReentrantLock支持中断处理,且性能较synchronized会好些。

  

重入锁

public class ReeterLock implements Runnable{
	public static ReentrantLock lock = new ReentrantLock();
	public static int i = 0;        //临界区资源
	
	public void run() {
		for(int j=0;j<10000000;j++) {
			//使用重入锁保护临界区资源,确保多线程对i操作的安全性
			lock.lock(); 
			try {
				i++;
			} finally {
				lock.unlock(); //退出临界区的时候,必修记得释放锁。否则其它线程就不能访问临界区了
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ReeterLock r1=new ReeterLock();
		Thread t1=new Thread(r1);
		Thread t2=new Thread(r1);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}
}

         为什么会叫重入锁呢?Re-Entrant-Lock?因为这个锁是可以反复进入的,这个的反复仅仅局限于一个线程

lock.lock();    //1
lock.lock();    //2
try{
   i++;
}finally{
   lock.unlock();  //1
   lock.unlock();  //2
}

       在上述的情况下,一个线程连续两次获得同一把锁,这是允许的。如果不允许发生这种操作,那么同一线程在获得第二次锁的时候,就会和自己产生死锁。

       但需要注意的是,如果同一个线程多次获得同一把锁,那么释放锁的时候,也必须释放相同的次数。当然,如果释放的次数多了,就会抛出java.lang.IllegalMonitorStateException异常。反之,如果释放少了,那么这个线程还持有这该锁,其他线程无法进入该临界区;

获取锁

//非公平锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
//lock方法
public void lock() {
        sync.lock();
}

       Sync为ReentrantLock的一个内部类,它继承自AQS(AbstractQueuedSynchronizer);它有两个子类:公平锁FairSync和非公平锁NonfairSync;

       ReentrantLock里面大部分的功能都是委托给Sync及其子类来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,它是非公平锁的默认实现;

        下面是非公平锁的lock方法:

    final void lock() {
        //尝试获取锁
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //获取失败,调用AQS的acquire(int arg)方法
            acquire(1);
    }
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

        这个方法首先调用tryAcquire(int arg)方法,在AQS中讲述过,tryAcquire(int arg)需要自定义同步组件自己实现;

        非公平锁的tryAcquire(int arg)是这样实现的:

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }

    final boolean nonfairTryAcquire(int acquires) {
        //当前线程
        final Thread current = Thread.currentThread();
        //获取同步状态
        int c = getState();
        //state == 0,表示没有该锁处于空闲状态
        if (c == 0) {
            //获取锁成功,设置为当前线程所有
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);   //CAS操作
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            //锁不处于空闲状态
            //判断持有锁的线程是否current线程
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

释放锁

    //ReentrantLock提供的unlock方法
    public void unlock() {
        sync.release(1);
    }
    //unlock内部使用Sync的release(int arg)释放锁,release(int arg)是在AQS中定义的:
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

       与获取同步状态的tryAcquire(int arg)方法一样,释放同步状态的tryRelease(int arg)同样是需要自定义同步组件自己实现;

       下面是非公平锁自己实现的tryRelease方法;

    protected final boolean tryRelease(int releases) {
        //减掉releases
        int c = getState() - releases;
        //如果释放的不是持有锁的线程,抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //state == 0 表示已经释放完全了,其他线程可以获取同步状态了
        if (c == 0) {
            free = true;                     //表示锁释放成功
            setExclusiveOwnerThread(null);   //将这个锁持有线程设置为null
        }
        setState(c);
        return free;
    }

重入锁的中断响应

       对于synchronized来说,如果一个线程在等待锁,那么结果就只有两种情况,第一获得这把锁继续执行,第二保持等待。而使用重入锁,则有第三种情况,即线程可以被中断。就是在等到锁的过程中,程序可以根据需要取消对锁的请求。
       中断提供了类似的机制。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,取消对锁的请求。这对解决死锁是很有帮助的。

public class IntLock implements Runnable {
	public static ReentrantLock lock1 = new ReentrantLock();
	public static ReentrantLock lock2 = new ReentrantLock();
	int lock;
	
	/**
	 * 控制加锁顺序,方便构造死锁
	 * @param lock
	 */
	public IntLock(int lock) {
		this.lock = lock;	
	}
	
	@Override
	public void run() {
		try {
			if (lock == 1) {
                                //申请锁,但与lock()不同的是
                                //在等待锁的过程中,它允许被中断(Thread.interrupt())
                                //只是会抛出异常
				lock1.lockInterruptibly();
				try {
					Thread.sleep(500);
				} catch (Exception e) {}
				lock2.lockInterruptibly();
			}else {
				lock2.lockInterruptibly();
				try {
					Thread.sleep(500);
				} catch (Exception e) {}
				lock1.lockInterruptibly();
			}
		} catch (Exception 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和t2启动后,t1先占用lock1,再占用lock2;
                //t2则先占用lock2,再占用lock1;这样很容易形成t1和t2的互相等待,即死锁;
		t1.start();
		t2.start();
		Thread.sleep(100); //主线程休眠,t1和t2处于死锁状态
		//中断其中一个线程
		t2.interrupt();  //t2线程被中断,t2会放弃对lock1的申请,同时释放lock2;t1顺利执行完;
	}	
}

锁申请等待限时

       除了“重入锁的中断响应”外,要避免死锁,还有一种方法,那就是限时等待。给定一个等待时间,如果还没有获得想要的锁,那么线程就自动放弃申请。

public class TimeLock implements Runnable {
	public static ReentrantLock lock = new ReentrantLock();

	@Override
	public void run() {
		try {
                        //tryLock()最多等待5秒,超过5秒还没得到锁,就返回false
                        //tryLock()也可以不带参数,这种情况下不等待
			if (lock.tryLock(5, TimeUnit.SECONDS)) {
				Thread.sleep(6000);        //占用锁6秒,所以另外一个线程请求锁将失败
			}else {
				System.out.println("get lock failed");
			}
		}catch (Exception e) {
			e.printStackTrace();
		}finally {
			if (lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
		
	}
	
	public static void main(String[] args) {
		TimeLock r1 = new TimeLock();
		Thread t1 = new Thread(r1);
		Thread t2 = new Thread(r1);
		t1.start();
		t2.start();
	}
}

公平锁

       在大多数情况下,锁的分配是非公平的;

       而公平的锁,它会按照时间的先后顺序。保证先来先得。公平锁的一个特点是:它不会产生饥饿现象,只要你排队,最终还是可以获得锁的;

       对于非公平锁而言,一个线程会倾向于再次获得已经持有的锁。

       要实现公平锁则系统必须要维护一个有序队列,因此公平锁的实现成本比较高,性能也比较差,在多线程访问的情况下,公平锁的吞吐量较;

        因此默认情况下,锁是非公平的。

        当然,如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的;而重入锁允许我们对其公平性进行设置。
//构造函数:当fair为true时,表示锁是公平的
public ReetrantLock(boolean fair)

         公平锁的tryAcquire(int arg)实现如下:

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

        比较非公平锁和公平锁获取锁的过程,会发现两者唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors(),定义如下:

    //主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回true,否则返回false
    public final boolean hasQueuedPredecessors() {
        Node t = tail;  //尾节点
        Node h = head;  //头节点
        Node s;
        
        //头节点 != 尾节点
        //同步队列第一个节点不为null
        //当前线程是同步队列第一个节点
        return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
    }

总结

ReetrantLock的几个重要的方法如下:

  • lock():申请锁,如果锁已经被占用,则等待;
  • lockInterruptibly():申请锁,并且在获得锁之前,响应中断响应;
  • tryLock():申请锁,成功则返回true,不等待;
  • tryLock(long time,TimeUnit unit):申请锁,并等待time时间;
  • unlock():是否锁;

实现重入锁,从代码层面得从三个方面看:

  1. 原子状态:原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被其他线程持有;
  2. 等待队列:所有没有请求到锁的线程,都会进入等待队列等待。是否锁后,系统会唤醒一个线程去持有锁;
  3. 阻塞原语park()和unpark():用来挂起和恢复线程。没有得到锁的线程会被挂起;关于这点,可以参考线程阻塞工具类LockSupport类;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值