JDK并发包(concurrent) - 重入锁(ReentrantLock)

重入锁是可以完全替代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个一组被输出的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值