多线程 Lock 对象(六)

47 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第31天,点击查看活动详情

回顾

与 synchronized 类似的,lock 也能够达到同步的效果。

首先回忆一下 synchronized 同步对象的方式:

当一个线程占用 synchronized 同步对象,其他线程就不能占用了,知道释放这个同步对象为止

image-20221227103526974

public class SynchronizedTest {

    public static String now() {
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }

    public static void main(String[] args) {
        final Object someObject = new Object();

        //t1 线程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(now() + " " + getName() +  " 试图占有对象:someObject");
                    synchronized (someObject) {
                        System.out.println(now() + " " + getName() + " 占有对象:someObject");
                        Thread.sleep(5000);
                        System.out.println(now() + " " + getName() + " 释放对象:someObject");
                    }
                    System.out.println(now() + " " + getName() + " 线程结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.setName("t1");
        t1.start();

        //t2 线程
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(now() + " " + getName() +  " 试图占有对象:someObject");
                    synchronized (someObject) {
                        System.out.println(now() + " " + getName() + " 占有对象:someObject");
                        Thread.sleep(5000);
                        System.out.println(now() + " " + getName() + " 释放对象:someObject");

                    }
                    System.out.println(now() + " " + getName() + " 线程结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t2.setName("t2");
        t2.start();
    }
}

一、使用 Lock 对象实现同步效果

Lock 是一个接口,为了使用 Lock 对象,需要用到:

Lock lock = new ReentrantLock();

与 synchronized(someObject) 类似,lock() 方法表示当前线程占用了 lock 对象,一旦占用,其他线程就不能占用了。

与 synchronized 不同的是,一旦 synchronized 块结束,就会自动释放 someObject 的占用。lock 却必须调用 unlock() 方法进行手动释放,为了保证释放的执行,往往会把 unlock() 放在 finally 中进行。

代码实现:

public class LockTest {

    public static String now() {
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        //t1 线程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(now() + " " + getName() + " 试图占有对象:lock");
                    lock.lock();
                    System.out.println(now() + " " + getName() + " 占有对象:lock");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(now() + " " + getName() + " 释放对象:lock");
                    lock.unlock();
                }
                System.out.println(now() + " " + getName() + " 线程结束");
            }
        };
        t1.setName("t1");
        t1.start();

        //让 t1 先飞 2 秒
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2 线程
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(now() + " " + getName() + " 试图占有对象:lock");
                    lock.lock();
                    System.out.println(now() + " " + getName() + " 占有对象:lock");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(now() + " " + getName() + " 释放对象:lock");
                    lock.unlock();
                }
                System.out.println(now() + " " + getName() + " 线程结束");
            }
        };
        t2.setName("t2");
        t2.start();
    }
}

//打印结果
15:49:39 t1 试图占有对象:lock
15:49:39 t1 占有对象:lock
15:49:39 t2 试图占有对象:lock
15:49:44 t1 释放对象:lock
15:49:44 t1 线程结束
15:49:44 t2 占有对象:lock
15:49:49 t2 释放对象:lock
15:49:49 t2 线程结束

二、trylock 方法

synchronized 是不占用到手誓不罢休,会一直试图占用下去。

与 synchronized的钻牛角尖不一样,Lock 接口还提供了一个 trylock 方法。

trylock 会在指定的时间范围内试图占用,占用成功了,就可以干活了。如果时间到了,还占用不成功,扭头就走。

注意: 因为使用 trylock 有可能成功,有可能失败,所以后面 unlock 释放锁的时候,需要判断是否是占用成功了,如果没占用成功也 unlock ,就会抛出异常。

代码实现:

public class LockTest {

    public static String now() {
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        //t1 线程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                boolean locked = false;
                try {
                    System.out.println(now() + " " + getName() + " 试图占有对象:lock");
                    locked = lock.tryLock(1, TimeUnit.SECONDS);
                    if(locked){
                        System.out.println(now() + " " + getName() + " 占有对象:lock");
                        System.out.println(now() + " " + getName() + " 进行 5 秒的业务操作");
                        Thread.sleep(5000);
                    }else {
                        System.out.println(now() + " " + getName() + " 经过 1 秒钟的努力,还没有占有对象,放弃占有");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if(locked){
                        System.out.println(now() + " " + getName() + " 释放对象:lock");
                        lock.unlock();
                    }
                }
                System.out.println(now() + " " + getName() + " 线程结束");
            }
        };
        t1.setName("t1");
        t1.start();

        //让 t1 先飞 2 秒
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2 线程
        Thread t2 = new Thread() {
            @Override
            public void run() {
                boolean locked = false;
                try {
                    System.out.println(now() + " " + getName() + " 试图占有对象:lock");
                    locked = lock.tryLock(1,TimeUnit.SECONDS);
                    if(locked){
                        System.out.println(now() + " " + getName() + " 占有对象:lock");
                        System.out.println(now() + " " + getName() + " 进行 5 秒的业务操作");
                        Thread.sleep(5000);
                    }else {
                        System.out.println(now() + " " + getName() + " 经过 1 秒钟的努力,还没有占有对象,放弃占有");
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    if(locked){
                        System.out.println(now() + " " + getName() + " 释放对象:lock");
                        lock.unlock();
                    }
                }
                System.out.println(now() + " " + getName() + " 线程结束");
            }
        };
        t2.setName("t2");
        t2.start();
    }
}

//打印结果
17:00:47 t1 试图占有对象:lock
17:00:47 t1 占有对象:lock
17:00:47 t1 进行 5 秒的业务操作
17:00:49 t2 试图占有对象:lock
17:00:50 t2 经过 1 秒钟的努力,还没有占有对象,放弃占有
17:00:50 t2 线程结束
17:00:52 t1 释放对象:lock
17:00:52 t1 线程结束

三、线程交互

使用 synchronized 方式进行线程交互,用到的是同步对象的 wait,notify,notifyAll 方法。

Lock 也提供了类似的解决办法,首先通过 lock 对象得到一个 Condition 对象,然后分别调用这个 Condition 对象的:await,signal,signalAll 方法。

注意: 不是 Condition 对象的 wait,notify,notifyAll 方法,是 await,signal,signalAll 方法

代码实现:

public class LockTest {

    public static String now() {
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }

    public static void main(String[] args) {
        final Lock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();

        //t1 线程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(now() + " " + getName() + " 试图占有对象:lock");
                    lock.lock();
                    System.out.println(now() + " " + getName() + " 占有对象:lock");
                    System.out.println(now() + " " + getName() + " 进行 5 秒的业务操作");
                    Thread.sleep(5000);
                    System.out.println(now() + " " + getName() + " 临时释放对象 lock,并等待");
                    condition.await();
                    System.out.println(now() + " " + getName() + " 重新占有对象 lock,并进行 5 秒的业务操作");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(now() + " " + getName() + " 释放对象:lock");
                    lock.unlock();
                }
                System.out.println(now() + " " + getName() + " 线程结束");
            }
        };
        t1.setName("t1");
        t1.start();

        //让 t1 先飞 2 秒
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2 线程
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(now() + " " + getName() + " 试图占有对象:lock");
                    lock.lock();
                    System.out.println(now() + " " + getName() + " 占有对象:lock");
                    System.out.println(now() + " " + getName() + " 进行 5 秒的业务操作");
                    Thread.sleep(5000);
                    System.out.println(now() + " " + getName() + " 唤醒等待中的线程");
                    condition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(now() + " " + getName() + " 释放对象:lock");
                    lock.unlock();
                }
                System.out.println(now() + " " + getName() + " 线程结束");
            }
        };
        t2.setName("t2");
        t2.start();
    }
}

//打印结果
17:18:59 t1 试图占有对象:lock
17:18:59 t1 占有对象:lock
17:18:59 t1 进行 5 秒的业务操作
17:19:01 t2 试图占有对象:lock
17:19:04 t1 临时释放对象 lock,并等待
17:19:04 t2 占有对象:lock
17:19:04 t2 进行 5 秒的业务操作
17:19:09 t2 唤醒等待中的线程
17:19:09 t2 释放对象:lock
17:19:09 t2 线程结束
17:19:09 t1 重新占有对象 lock,并进行 5 秒的业务操作
17:19:14 t1 释放对象:lock
17:19:14 t1 线程结束

四、总结

本篇文章我们介绍了另外一种锁 Lock,以及相关交互的实现,它与 synchronized 区别:

1、Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现,Lock 是代码层面的实现。

2、 Lock 可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized 不行,会一根筋一直获取下去。 借助 Lock 的这个特性,就能够规避死锁,synchronized 必须通过谨慎和良好的设计,才能减少死锁的发生。

3、 synchronized 在发生异常和同步块结束的时候,会自动释放锁。而 Lock 必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

好了,本篇文章到这里就结束了,感谢你的阅读🤝