Java 并发编程(三)可重入锁 ReentrantLock

本贴最后更新于 1609 天前,其中的信息可能已经天翻地覆

ReentrantLock 的使用

ReentrantLock 可以完全替代 synchronized,提供了一种更灵活的锁.

ReenTrantLock 必须手动释放锁,为防止发生异常,必须将同步代码用 try 包裹起来,在 finally 代码块中释放锁.

public class T {

    ReentrantLock lock = new ReentrantLock();

    // 使用ReentrantLock的写法
    private void m1() {
        // 尝试获得锁
        lock.lock(); 
        try {
            System.out.println(Thread.currentThread().getName());
        } finally {
            // 
            lock.unlock(); 
        }
    }

    // 使用synchronized的写法
    private synchronized void m2() {
        System.out.println(Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start(); 
        new Thread(t::m2, "t2").start(); 
    }
}

ReentrantLock 获取锁的方法

尝试锁 tryLock()

使用 tryLock() 方法可以尝试获得锁,返回一个 boolean 值,指示是否获得锁.

可以给 tryLock 方法传入阻塞时长,当超出阻塞时长时,线程退出阻塞状态转而执行其他操作.

public class T{

    ReentrantLock lock = new ReentrantLock();

    void m1() {
        lock.lock(); // 相当于 synchronized
        try {
            for (int i = 0; i < 5; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 使用完毕后,必须手动释放锁
            // 不同于synchronized,抛出异常后,不会自动释放锁,需要我们在finally中释放此锁
        }
    }

    void m2() {
        // 尝试获取锁,返回true拿到了
        if (lock.tryLock()) {
            // lock.tryLock(5, TimeUnit.SECONDS) // 等5s内还没拿到就返回false
            System.out.println("m2...");
            lock.unlock();
        } else {
            System.out.println(" m2 没拿到锁");
        }

    }

    public static void main(String[] args) {
        T r1 = new T();
        new Thread(r1::m1, "t1").start(); // m1 已经执行,被t1占有锁this
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r1::m2, "t2").start(); // 锁已经被其他线程占用,m1执行完毕后,不会执行
    }

}

程序运行结果如下:

0
 m2 没拿到锁
1
2
3
4

可中断锁 lockInterruptibly

使用 lockInterruptibly 以一种可被中断的方式获取锁.获取不到锁时线程进入阻塞状态,但这种阻塞状态可以被中断.调用被阻塞线程的 interrupt() 方法可以中断该线程的阻塞状态,并抛出 InterruptedException 异常.

interrupt() 方法只能中断线程的 阻塞状态.若某线程已经得到锁或根本没去尝试获得锁,则该线程当前没有处于 阻塞状态,因此不能被 interrupt() 方法中断.

public class T{
    
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);// 线程一直占用锁
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }, "t1");
        t1.start();

	// 线程2抢不到lock锁,若不被中断则一直被阻塞
        Thread t2 = new Thread(() -> {
            try {
                lock.lockInterruptibly(); // t2 尝试获取锁
                System.out.println("t2 start");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                System.out.println("t2 等待中被打断");
            } finally {
                lock.unlock(); // 没有锁定进行unlock就会抛出 IllegalMonitorStateException 
            }
        }, "t2");
        t2.start();

        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打断线程2的等待
        t2.interrupt();

    }

}

程序运行结果如下:

t1 start
t2 等待中被打断
Exception in thread "t2" 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 c_020.ReentrantLock4.lambda$main$1(ReentrantLock4.java:41)
	at java.lang.Thread.run(Thread.java:748)

并不是所有处于阻塞状态的线程都可以被 interrupt()方法中断,要看该线程处于具体的哪种阻塞状态.阻塞状态包括普通阻塞,等待队列,锁池队列.

  • 普通阻塞: 调用 sleep()方法的线程处于普通阻塞,调用其 interrupt()方法可以中断其阻塞状态并抛出 InterruptedException 异常
  • 等待队列: 调用锁的 wait()方法将持有当前锁的线程转入等待队列,这种阻塞状态只能由锁对象的 notify 方法唤醒,而不能被线程的 interrupt()方法中断.
  • 锁池队列: 尝试获取锁但没能成功抢到锁的线程会进入锁池队列:
    • 争抢 synchronized 锁的线程的阻塞状态不能被中断.
    • 使用 ReentrantLock 的 lock()方法争抢锁的线程的阻塞状态不能被中断.
    • 使用 ReentrantLock 的 tryLock()和 lockInterruptibly()方法争抢锁的线程的阻塞状态不能被中断.

公平锁

在初始化 ReentrantLock 时给其 fair 参数传入 true,可以指定该锁为 公平锁,
synchronized 是不公平锁。

CPU 默认的进程调度是 不公平的,也就是说,CPU 不能保证等待时间较长的线程先被执行.但 公平锁 可以保证等待时间较长的线程先被执行。

公平锁,先获取锁的人,在锁被释放时,优先获得锁。
不公平锁,无论先后,线程调度器将会随机给某个线程锁,不用计算线程时序,效率较高。

public class T extends Thread {

    private static ReentrantLock lock = new ReentrantLock(true);// 指定锁为公平锁

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获取锁");
                sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock(); // 公平锁 t1 unlock 后,等待时间长的一定是 t2 所以下次一定是 t2 执行
            }
        }
    }

    public static void main(String[] args) {
        T t1 = new T();
        new Thread(t1).start();
        new Thread(t1).start();

    }
}

程序运行结果如下:

Thread-1获取锁
Thread-2获取锁
Thread-1获取锁
Thread-2获取锁
Thread-1获取锁
Thread-2获取锁

生产者/消费者模式

经典面试题:写一个固定容量的容器,拥有 put 和 get 方法,以及 getCount 方法。能够支持 2 个生产者线程以及 10 个消费者线程的阻塞调用。如果调用 get 方法时,容器为空,get 方法就需要阻塞等待;如果调用 put 方法时,容器满了,put 方法就需要阻塞等待

1、使用 synchronizedwait()/notify() 实现

public class MyContainer1<T> {

    private final LinkedList<T> list = new LinkedList<>();
    private final int MAX = 10;
    private int count = 0;

    public synchronized void put(T t) {
        while (list.size() == MAX) { // 如果容量最大,释放锁等待    //【这里为什么使用while,而不是使用if???】
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 否则 put
        list.add(t);
        ++count;
        this.notifyAll(); // 通知消费者线程,可以消费了
        // 【这里为什么调用 notifyAll 而不是 notify ?】
    }

    public synchronized T get() {
        T t = null;
        while (list.size() == 0) { // 如果容量为空,释放锁等待  
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 否则获取
        t = list.removeFirst();
        count--;
        this.notifyAll(); // 通知生产者线程生产
        return t;
    }

    public static void main(String[] args) {
        MyContainer1<String> c = new MyContainer1<>();
        //启动消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    System.out.println(c.get());
                }
            }, "c" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //启动生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    c.put(Thread.currentThread().getName() + " " + j);
                }
            }, "p" + i).start();
        }
    }
}

为什么使用 while 而不是使用 if ?
在与 wait()的配合中,百分之 99 的程序都是与 while 而不是 if 结合使用。

上述代码中,在容器已满的情况下,put 方法会 wait 等待,当容器中的元素被消费者消费了一部分,就会唤醒所有 put 方法,

put 方法会继续向下执行,直接执行 list.add(t),那么多个生产者线程执行 list.add()就有可能出现数据一致性的问题。

如果使用 while 则会循环判断,就避免了这些问题。

不是有锁吗?为什么会需要循环判断?
wait 之后,锁就会失去,再次被唤醒时,并且得到锁之后,是从 list.add()开始执行的,会无判断直接加入到容器中。

为什么调用 notifyAll 而不是 notify ?
因为 notify 有可能再次叫醒一个生产者线程

2、使用 Condition 对象实现

用 Lock 和 Condition 实现,可以精确唤醒某些线程

public class MyContainer2<T> {

    private final LinkedList<T> list = new LinkedList<>();
    private final int MAX = 10;
    private int count = 0;

    private Lock lock = new ReentrantLock();// 锁对象
    // 绑定在锁上的一个条件,阻塞在该条件上的线程为生产者线程
    private Condition producer = lock.newCondition();
    // 绑定在锁上的一个条件,阻塞在该条件上的线程为消费者线程
    private Condition consumer = lock.newCondition();


    public  void put(T t) {
        lock.lock();
        try {
            while (list.size() == MAX) {
                // 当前线程应该是生产者线程,因此将当前线程阻塞在producerCondition上
                producer.await();
            }
            list.add(t);
            ++count;
            // 唤醒所有阻塞在consumerCondition的线程,这些被唤醒的线程都应该是消费者线程
            consumer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public  T get() {
        T t=null;
        lock.lock();
        try {
            while (list.size() == 0) {
                // 当前线程应该是消费者线程,因此将当前线程阻塞在consumerCondition上
                consumer.await();
            }
            t = list.removeFirst();
            count--;
            // 唤醒所有阻塞在producerCondition的线程,这些被唤醒的线程都应该是生产者线程
            producer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }

    public static void main(String[] args) {
        MyContainer2<String> c = new MyContainer2<>();
        //启动消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    System.out.println(c.get());
                }
            }, "c" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //启动生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    c.put(Thread.currentThread().getName() + " " + j);
                }
            }, "p" + i).start();
        }
    }
}
  • 并发
    75 引用 • 73 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...