java多线程编程核心技术(第四章)-读书笔记

java多线程编程核心技术

第四章、Lock的使用

4.1 使用ReentrantLock类

ReentrantLock可以达到synchronized同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能。

4.1.1 使用ReentrantLock实现同步:测试

调用ReentrantLock对象的lock()方法获取锁,调用unlock()方法释放锁。

调用lock()方法的线程会持有“对象监视器”,其他线程只有等待锁被释放时再次争抢。

代码示例:

/**
 * 测试多线程使用ReentrantLock同步
 */
public class Test42ReentrantLock {
    public static void main(String[] args) {
        MyService42 myService42 = new MyService42();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                myService42.methodA();
            }
        };
        t1.setName("A");
        Thread t2 = new Thread() {
            @Override
            public void run() {
                myService42.methodB();
            }
        };
        t2.setName("B");
        t1.start();
        t2.start();
        /*
        调用lock()代码的线程就获得了"对象监视器",其他线程只能等待锁被释放后再执行
        运行结果
        methodA begin threadName=A time=1572234673305
        methodA end threadName=A time=1572234676306
        methodB begin threadName=B time=1572234676306
        methodB end threadName=B time=1572234679307
         */
    }
}
class MyService42 {
    private Lock lock = new ReentrantLock();
    public void methodA() {
        try {
            lock.lock();
            System.out.println("methodA begin threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("methodA end threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            lock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void methodB() {
        try {
            lock.lock();
            System.out.println("methodB begin threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("methodB end threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            lock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4.1.3 使用Condition实现等待/通知:错误用法与解决

关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模型,类ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。使用ReentrantLock有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Conditin(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

Reentrant结合Condition类是可以实现“选择性通知”的,而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上,不够灵活。

condition的await()方法调用之前,必须先调用lock()方法获取锁(否则会抛出IllegalMonitorStateException异常),这一点和synchronized相同。

4.1.4 正确使用Condition实现等待/通知

Object类中的wait()方法相当于Condition类中的await()方法。

Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。

Object类中的notify()方法相当于Condition类中的signal()方法。

Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

代码示例:

/**
 * Condition的正确用法
 */
public class Test43Condition {
    public static void main(String[] args) throws InterruptedException {
        MyService43 myService43 = new MyService43();
        Thread t = new Thread() {
            @Override
            public void run() {
                myService43.await();
            }
        };
        t.start();
        Thread.sleep(3000);
        myService43.signal();
        /*
        调用condition的await和signal方法前必须要获得所属lock的锁,否则会抛异常IllegalMonitorStateException
        运行结果:
        await 时间为1572317198190
        signal 时间为1572317201194
         */
    }
}
class MyService43 {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void await() {
        try {
            lock.lock();
            System.out.println("await 时间为" + System.currentTimeMillis());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 注意,异常情况下也需要自己释放锁
            lock.unlock();
        }
    }
    public void signal() {
        try {
            lock.lock();
            System.out.println("signal 时间为" + System.currentTimeMillis());
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}
4.1.5 使用多个Condition实现通知部分线程

如果想单独唤醒部分线程该怎么办呢?这时就有必要使用多个Condition对象类,也就是Condition对象可以唤醒部分指定线程,有助于提升程序运行的效率。

4.1.8 实现生产者/消费者模式:多对多交替打印

代码示例:

/**
 * 基于lock、condition的交替打印示例
 */
public class Test44AlternatePrint {
    public static void main(String[] args) {
        MyService44 myService44 = new MyService44();
        Thread[] a = new Thread[10];
        Thread[] b = new Thread[10];
        for (int i = 0; i < 10; i++) {
            a[i] = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        myService44.set();
                    }
                }
            };
            b[i] = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        myService44.get();
                    }
                }
            };
            a[i].start();
            b[i].start();
        }
        /*
        可以看出,【打印-】和【打印=】是交替出现的
        运行结果:
        打印=
        有可能==连续
        打印-
        有可能--连续
        打印=
        有可能==连续
        打印-
         */
    }
}
class MyService44 {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean hasValue = false;
    public void set() {
        try {
            lock.lock();
            while (hasValue == true) {
                System.out.println("有可能--连续");
                condition.await();
            }
            System.out.println("打印-");
            hasValue = true;
            // 这里如果使用signal,则会出现由于仅通知同类线程而造成程序卡住的情况
            condition.signalAll();
            // 如果不注释掉下面两行,则从这里的输出可以看出,condition.signalAll()也是需要执行完lock.unlock才会真正释放锁
//            Thread.sleep(5000);
//            System.out.println("unlock...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void get() {
        try {
            lock.lock();
            while (hasValue == false) {
                System.out.println("有可能==连续");
                condition.await();
            }
            System.out.println("打印=");
            hasValue = false;
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
4.1.9 公平锁与非公平锁

锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

代码示例:

/**
 * 公平锁与非公平锁
 */
public interface Test45FairUnFairLock {
    public static void main(String[] args) {
        // 测试公平锁
        testFair(true);
        // 测试非公平锁
        testFair(false);
        /*
        从结果来看,公平锁更容易呈现有序状态,非公平锁更容易出现乱序的结果
        输出结果:
        test fair:true
        ThreadName=Thread-0获得锁定
        ThreadName=Thread-1获得锁定
        ThreadName=Thread-2获得锁定
        ThreadName=Thread-3获得锁定
        ThreadName=Thread-4获得锁定
        test fair:false
        ThreadName=Thread-0获得锁定
        ThreadName=Thread-2获得锁定
        ThreadName=Thread-1获得锁定
        ThreadName=Thread-3获得锁定
        ThreadName=Thread-4获得锁定
         */
    }

    public static void testFair(boolean isFair) {
        System.out.println("test fair:" + isFair);
        MyService45 myService45 = new MyService45(isFair);
        Thread[] tArr = new Thread[5];
        for (int i = 0; i < 5; i++) {
            tArr[i] = new Thread() {
                @Override
                public void run() {
                    myService45.serviceMethod();
                }
            };
            tArr[i].setName("Thread-" + i);
        }
        for (int i = 0; i < 5; i++) {
            tArr[i].start();
        }
        for (int i = 0; i < 5; i++) {
            try {
                tArr[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyService45 {
    private ReentrantLock lock;
    public MyService45(boolean isFair) {
        lock = new ReentrantLock(isFair);
    }
    public void serviceMethod() {
        try {
            lock.lock();
            System.out.println("ThreadName=" + Thread.currentThread().getName() + "获得锁定");
        } finally {
            lock.unlock();
        }
    }
}
4.1.10 方法getHoldCount()、getQueueLength()和getWaitQueueLength()的测试
  • ReentrantLock的方法int getHoldCount()的作用是查询当前线程保持此锁定的个数,也就是调用lock()方法的次数,即当前锁的锁重入的次数。
  • ReentrantLock的方法int getQueueLength()的作用是返回正等待获取此锁定的线程估计数。比如有5个线程都试图获取同一个锁lock,1个线程首先执行lock()方法获取了锁,那么在调用getQueueLength()方法后返回值是4,说明有4个线程同时在等待lock的释放。
  • ReentrantLock的方法int getWaitQueueLength(Condition condition)的作用是返回等待与此锁相关的Condition的线程估计数。比如有5个线程都调用了同一个condition对象的await()方法,则调用此方法的返回值就是5。
4.1.11 方法hasQueueThread()、hasQueueThreads()、hasWaiters的测试
  • ReentrantLock的方法boolean hasQueueThread(Thread thread)的作用是查询指定的线程是否正在等待获取此锁。
  • ReentrantLock的方法boolean hasQueueThreads()的作用是查询是否有线程正在等待获取此锁定。
  • ReentrantLock的方法boolean hasWaiters(Condition condition)的作用是查询是否有线程正在等待与此锁定有关的condition。
4.1.12 方法isFair()、isHeldByCurrentThread()和isLocked()的测试
  • ReentrantLock的方法boolean isFair()的作用是判断是不是公平锁。
  • ReentrantLock的方法boolean isHeldByCurrentThread()的作用是查询当前线程是否持有这个锁。
  • ReentrantLock的方法boolean isLocked()的作用是查询此锁是否由任意线程持有。
4.1.13 方法lockInterruptibly()、tryLock()和tryLock(long timeout,TimeUnit unit)的测试
  • ReentrantLock的方法void lockInterruptibly()的作用是:首先获取锁,在持有锁的过程中,如果线程被interrupt,则线程中断并抛出异常。而我们常用的lock方法遇到interrupt不会受任何影响。
  • ReentrantLock的方法boolean tryLock()的作用是,仅尝试一次获取锁的操作,如果成功获取返回true,未能获取锁则返回false。
  • ReentrantLock的方法boolean tryLock(long timeout, TimeUnit unit)的作用是,在给定时间内一直尝试获取锁,成功获取锁则返回true,如果在给定时间内都没能获取锁,则返回false。
4.1.14 方法awaitUninterruptibly()的使用
  • Condition的方法void awaitUninterruptibly()的作用是,当调用此方法后,无法用interrupt方法打断。而如果是我们常用的await方法遇到interrupt方法后,会抛出异常InterruptedException。
4.1.15 方法awaitUntil()的使用
  • Condition的方法boolean awaitUntil(Date deadline)的作用是,指定一个截止时间,在这个时间之前,其他线程可以使用signal/signalAll方法唤醒【调用awaitUntil方法的线程】,而如果到了截止时间,则【调用awaitUntil方法的线程】自动唤醒。
4.1.16 使用Conditiong实现顺序打印

使用Condition对象可以对线程执行的业务进行排序规划。

代码示例:

/**
 * 使用Condition实现三种输出按顺序打印
 */
public class Test46ConditionSequentialPrint {
    volatile private static int nextPrintWho = 1;
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition conditionA = lock.newCondition();
    private static Condition conditionB = lock.newCondition();
    private static Condition conditionC = lock.newCondition();

    public static void main(String[] args) {
        Thread a = new Thread() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 1) {
                        conditionA.await();
                    }
                    System.out.println("ThreadA...");
                    nextPrintWho = 2;
                    conditionB.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };
        Thread b = new Thread() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 2) {
                        conditionB.await();
                    }
                    System.out.println("ThreadB...");
                    nextPrintWho = 3;
                    conditionC.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };
        Thread c = new Thread() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 3) {
                        conditionC.await();
                    }
                    System.out.println("ThreadC...");
                    nextPrintWho = 1;
                    conditionA.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };
        Thread[] aArr = new Thread[5];
        Thread[] bArr = new Thread[5];
        Thread[] cArr = new Thread[5];
        for (int i = 0; i < 5; i++) {
            aArr[i] = new Thread(a);
            bArr[i] = new Thread(b);
            cArr[i] = new Thread(c);
            aArr[i].start();
            bArr[i].start();
            cArr[i].start();
        }
        /*
        通过使用不同的Condition,可以使输出按照一定顺序执行
        输出结果:
        ThreadA...
        ThreadB...
        ThreadC...
        ThreadA...
        ThreadB...
        ThreadC...
        ThreadA...
        ThreadB...
        ThreadC...
        ThreadA...
        ThreadB...
        ThreadC...
        ThreadA...
        ThreadB...
        ThreadC...
         */
    }
}

4.2 使用ReentrantReadWriteLock类

类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务。这样做保证类安全性,但效率较低。

JDK提供了一种读写锁ReentrantReadWriteLock类,使它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁ReentrantReadWriteLock来提升该方法的代码运行速度。

读写锁表示有两个锁,一个是读操作相关的锁,也成为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

代码示例:

/**
 * ReentrantReadWriteLock简单测试
 */
public class Test47ReentrantReadWriteLock {
    public static void main(String[] args) {
        MyService47 myService47 = new MyService47();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                myService47.read();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                myService47.read();
//                myService47.write();
            }
        };
        t1.start();
        t2.start();
        /*
        由于读读不互斥,所以两个线程同时获得读锁
        如果将t2线程读read方法改为write方法,则两个线程变为竞争关系,只能一个执行完另一个才能执行

        运行结果:
        获得读锁 Thread-1 1572332543264
        获得读锁 Thread-0 1572332543264

        修改t2线程为write方法后,写锁需要在读锁释放后才能获取,运行结果:
        获得读锁 Thread-0 1572332603008
        获得写锁 Thread-1 1572332606010
         */
    }
}
class MyService47 {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void read() {
        try {
            lock.readLock().lock();
            System.out.println("获得读锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }
    public void write() {
        try {
            lock.writeLock().lock();
            System.out.println("获得写锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
}

4.3 ReentrantLock参考资料

读写锁是由AbstractQueuedSynchronizer实现,它内部维护了一个同步等待队列。并且用一个int类型的state来表示锁是否被占用(0或1)、锁是否被重入(1或>1)。底层实现是由compareAndSetState方法提供的,也就是使用来CAS的方式。

相关源码分析文章可参考:https://www.cnblogs.com/takumicx/p/9402021.html

最后

1、所有代码示例都在github中
https://github.com/llbqhh/LlbStudy/tree/master/StudyJava/src/main/java/org/llbqhh/study/java/book/java_multi_thread_programming
2、感谢您的阅读,如果对内容感兴趣可以关注我的公众号,搜索【猫耳山自习室】或扫描下方二维码,会有定期更新

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值