美文网首页学习JUCAndroid知识
Java多线程开发(二)| 多线程的竞争与协作

Java多线程开发(二)| 多线程的竞争与协作

作者: WolfXu | 来源:发表于2017-04-23 08:15 被阅读249次

    0. 前言

    使用多线程的过程中,主要要解决的是两类问题:

    1. 多个线程共享资源
    2. 多个线程的协作

    线程就像独立的个体,每个线程都有各自的任务。为了完成各自的任务,会去获取自己需要的资源,可能会和其他线程产生竞争。但每个线程的任务,最终都是为了实现共同的目标,线程与线程之间需要相互配合。而我们要做的,就是建立一种机制,让多个线程能合理地竞争,有效地合作。这么一想,管理多线程就像管理团队一样。团队的任务拆解到个人,每个人有各自的任务和目标,在执行过程中会用到相同的资源,成员之间也需要沟通和协作。管理团队不容易,管理多线程也要小心谨慎。
      对 Java 的多线程机制不了解的同学,可以先看我的上一篇文章:Java多线程开发(一)| 基本的线程机制

    1. 多个线程共享资源

    1.1 不正确地访问资源

    多个线程经常有需要共享的资源。有些是因为资源本身有限,比如打印机;有一些是出于协作的需要,比如共享变量。当两个以上的线程同时访问相同的资源时,很容易出现问题。
      举个例子演示一下。想了很久找不出很好的例子,就用《Thinking in Java》书上的例子吧。

    public abstract class IntGenerator {
        private volatile boolean mCanceled = false;
        public abstract int next();
      
        public void cancel() {
            mCanceled = true;
        }
      
        public boolean isCanceled() {
            return mCanceled;
        }
    }
    
    public class EvenChecker implements Runnable {
        private IntGenerator mGenerator;
        private final int mId;
    
        public EvenChecker(IntGenerator generator, int ident) {
            mGenerator = generator;
            mId = ident;
        }
      
        public void run() {
            while (!mGenerator.isCanceled()) {
                int val = mGenerator.next();
                if (val % 2 != 0) {
                    System.out.println(val + " not even");
                    mGenerator.cancel();
                }
            }
        }
      
        public static void test(IntGenerator generator, int count) {
            System.out.println("Press Control-C to exit");
            ExecutorService executor = Executors.newCachedThreadPool();
            for (int i = 0; i < count; ++i) {
                executor.execute(new EvenChecker(generator, i));
            }
            executor.shutdown();
        }
      
        public static void test(IntGenerator generator) {
            test(generator, 10);
        }
    }
    

    IntGenerator 是产生数字的抽象类,EvenChecker 创建多个线程去调用 IntGenerator 的 next() 方法生成数字,并检测数字是否是偶数。

    public class EvenGenerator extends IntGenerator {
        private int mCurrentEvenValue = 0;
      
        @Override
        public int next() {
            ++mCurrentEvenValue;
            Thread.yield();
            ++mCurrentEvenValue;
            return mCurrentEvenValue;
        }
      
        public static void main(String[] args) {
            EvenChecker.test(new EvenGenerator());
        }
    }
    // 输出:
    11 not even
    15 not even
    17 not even
    19 not even
    21 not even
    

    EvenGenerator 用于生成偶数。在两个自增操作之间,增加了一个 Thread.yield() ,是为了更快地观察到现象。EvenGenerator 的 next() 被多个线程调用,mCurrentEvenValue 在这里就是多个线程共享的变量。从输出结果可以看到,由于线程在第一次自增操作之后切换,next() 返回的值会出现奇数。程序处于不正常的状态。

    1.2 解决共享资源竞争问题

    为了解决线程竞争共享资源导致的问题,通常让线程以序列化的方式访问资源,即同一时刻只能有一个线程访问资源。当一个线程在访问资源时,就不允许另一个线程访问,线程间的这种制约关系叫互斥。而同一时间只能被一个线程访问的资源叫临界资源,访问临界资源的代码块叫临界区。一般通过对临界区加锁,实现线程的互斥。

    在 Java 中,对临界区加锁常用的方式有两种:

    • synchronized 关键字
    • Lock 对象

    1.2.1 synchronized 关键字

    Java 中的 synchronized 关键字,为防止资源冲突提供了内置支持。当执行到 synchronized 关键字保护的代码块时,首先要获取锁,然后才能执行代码,执行完成后释放锁。synchronized 关键字的使用有如下几种形式:

    • 修饰方法,synchronized void fun() {...}
    • 修饰静态方法,synchronized static fun() {...}
    • 包裹代码块,synchronized (obj) {...}

    虽然 synchronized 关键字的使用有不同的形式,但本质上是一样的,都是对对象加锁。在 Java 中,所有对象都含有一个锁(源码注释中叫 monitor),synchronized 就是获取对象的锁。再回过头看一下 synchronized 的几种使用形式:

    • 修饰方法,是对调用该方法的对象加锁
    • 修饰 static 方法,是对 Class 对象加锁
    • 修饰语句块,是对指定对象加锁

    一个线程可以多次获得同一个对象的锁,比如在 synchronized 方法中调用另一个 synchronized 方法。JVM 会记录对象加锁的次数,已经获得锁的线程再次获得锁,计数加1。计数为0时,锁才被完全释放,其他线程才能获得这个锁。
      使用 synchronized 关键字修改一下前面的例子,把 next() 方法用 synchronized 保护起来。这次不管运行多久,都不会出现 next() 返回奇数的情况了。

    public class SynchronizedEvenGenerator extends IntGenerator {
        private int mCurrentEvenValue = 0;
    
        @Override
        public synchronized int next() {
            ++mCurrentEvenValue;
            Thread.yield();
            ++mCurrentEvenValue;
            return mCurrentEvenValue;
        }
    
        public static void main(String[] args) {
            EvenChecker.test(new SynchronizedEvenGenerator());
        }
    }
    

    1.2.2 Lock 对象

    除了内置的 synchronized 关键字外。还可以使用 java.util.concurrent.locks 类库中 Lock 的对象来实现互斥。Lock 是一个接口。需要显示地创建 Lock 对象,然后调用 lock() 方法去获取锁,调用 unlock() 方法去释放锁。

    public interface Lock {
        void lock();
        void lockInterruptibly() throws InterruptedException;
        boolean tryLock();
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        void unlock();
        Condition newCondition();
    }
    

    下面是使用 Lock 重写的 EventGenerator ,简单展示一下 Lock 的使用方法。注意,使用 Lock 时,尽量用 try-finally , 把 unlock() 方法的调用放到 finally 中。避免中途出现异常,导致锁无法被释放。

    public class MutexEvenGenerator extends IntGenerator {
    
        private int mCurrentEvenValue = 0;
        private Lock mLock = new ReentrantLock();
    
        @Override
        public int next() {
            try {
                mLock.lock();
                ++mCurrentEvenValue;
                Thread.yield();
                ++mCurrentEvenValue;
                return mCurrentEvenValue;
            } finally {
                // try-finally 的惯用法,保证锁能释放
                mLock.unlock();
            }
        }
    
        public static void main(String[] args) {
            EvenChecker.test(new MutexEvenGenerator());
        }
    }
    

    和 synchronized 相比,Lock 的使用更麻烦,但同时也更灵活。因为锁的创建、加锁和释放都由我们控制,可以实现 synchronized 做不到的需求。下面列举一些 Lock 能做到,而 synchronized 无法实现的功能:

    • 尝试获取锁,但如果锁已经被占用会直接返回而不阻塞;也可以设置尝试获取锁的等待时间,超时之后就返回。使用 Lock 的 tryLock() 方法即可。
    • 等待锁导致阻塞时,能够被 interrupt() 方法打断。
    • 更细粒度的控制,可以实现代码块之间交叉的加锁。比如,遍历链表时,要在释放当前节点的锁之前获取下一个节点的锁。

    关于 synchronized 关键字和 Lock 对象的选择,尽量用 synchronized 。当 synchronized 满足不了需求的时候,才考虑用 Lock 。synchronized 使用更简单,而且相对来说更加安全。
      java.util.concurrent.lock 包中,有三种锁。

    • Lock ,普通锁接口,有一个实现:ReentrantLock。
    • ReadWriteLock,包含读锁和写锁两个锁,这里的锁是 Lock 对象。也有一个实现:ReentrantReadWriteLock。
    • StampedLock,包含乐观读锁、悲观读锁、写锁三种模式锁,是一个具体的实现类。1.8才引入的,与 Lock 和 ReadWriteLock 完全无关。

    对于 ReentrantReadWriteLock,有一点需要注意。

    Thread1: A.readlock().lock() -> … 已经拿到读锁

    Thread2: A.writelock().lock() ->.. .请求写锁

    Thread3: A.readlock().lock() ->… 等待,一直到 Thread2 获取到然后又释放写锁。

    第一个线程获取了读锁,第二个线程在等待获取写锁。这时其他线程想要获取读锁的话,得等待第二个线程获取到写锁,做完事情,释放写锁。

    1.3 原子性、可见性

    原子性:一个操作是不可中断的,开始执行就一定会完全执行完。这种操作被称为原子操作。
      除了 long 和 dobule 之外的基本类型变量的读取和写入操作,都是原子性的。JVM 可以将 long 和 dobue 的读取和写入当成两个 32 位操作来执行,在中间可以发生切换。JVM 规范里面说明对 long 和 dobule 的读写操作不要求原子性,但是加了 volatile 关键字的 long 和 dobule 变量,必须是原子性的。
      java.util.concurrent.atomic 类库提供了一系列原子操作的类,比如 AtomicInteger、AtomicLong、AtomicReference 等。它们能提供自增、赋值并读取等扩展的原子操作。

    可见性:也可以叫作可视性、一致性。
      在多处理器或者多核处理器系统中,一个任务修改了某个共享变量,但这个修改暂时只存在处理器的缓存中,还没有更新到主存中。对于运行在其他处理器上的任务来说,这个变量的修改时不可见的,它们看到的还是修改前的值。在 Java 的内存模型中,每个线程都有单独的工作内存,线程内变量的修改会先缓存在工作内存中。

    线程、主存、工作内存之间的交互关系
      volatile 关键字可以保证可见性,使变量的修改立即写入到主存中,读取时也从主存中读取变量的最新值。synchronized 可以保证加锁的方法或者代码块中的修改的可见性。
      volatile 同时还能提供一定的有序性。对 volatile 变量的读写在一些情况下不会被重排序。具体可以参考这篇文章:深入理解Java内存模型(四)——volatileJava内存访问重排序的研究

    1.4 线程本地存储

    有一些变量不希望被其他线程共享,那可以使用 ThreadLocal。顾名思义,通过 ThreadLocal ,每个线程可以存储只有自身能访问的变量。ThreadLocal 对象本身只有一个,但是它里面存的值是每个线程一个,完全独立的。Thread中持有 ThreadLocalMap的对象threadLocals,这个map完全由ThreadLocal创建和维护。ThreadLocal对象,获取到当前线程的ThreadLocalMap,往里面存值,所以每个线程的值都是独立的。ThreadLocalMap的key是ThreadLocal对象,value就是ThreadLocal要保存的值。
      来看下面的例子。

    private static ThreadLocal<Integer> sVal = ThreadLocal.withInitial(() -> (1));
    
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                sVal.set(sVal.get() + 1);
                System.out.println(Thread.currentThread().getName() + " " + sVal.get());
            });
        }
        executorService.shutdown();
    }
    // 输出:
    pool-1-thread-1 2
    pool-1-thread-2 2
    pool-1-thread-3 2
    pool-1-thread-2 3
    pool-1-thread-1 3
    pool-1-thread-3 3
    pool-1-thread-2 4
    pool-1-thread-2 5
    pool-1-thread-2 6
    pool-1-thread-4 2
    

    因为 ThreadLocal 的对象 sVal 中存储的值应该是每个线程独立的,理论上输出都是 2 才对,但是最终的输出结果甚至有 6。这是因为,我们使用了线程池,而这里的任务执行时间太短,线程被复用了。这意味着线程中保存的值还是之前那个,所以会一直累加上来了。所以在使用线程池时,如果任务依赖 ThreadLocal 储存的值,那就需要小心了。

    2 线程之间的协作

    线程之间共享资源的问题解决了,接下来学习如何让线程之间彼此协作了。线程之间的协作,说白了,就是某些部分任务需要等待其他部分任务完成后,才能继续进行。我们可以通过 Object 的 wait() 和 notify() 方法来实现,也可以通过 Condition 对象的 await() 和 signal() 方法来实现。

    2.1 wait()、notifyAll() 和 notify()

    某些任务在执行时,会依赖某个条件,只有条件满足了才能继续执行,而这个条件是有其他任务改变的。如果我们只是不断地循环去检测这个条件,将会导致线程无意义地占用 CPU 资源,这被称为忙等待。而 wait() 可以在等待条件变化时,将任务挂起,等到 notify() 或者 notifyAll() 方法被调用时才会唤醒去检测条件是否满足。
      wait() 、notify() 和 notifyAll() 都是 Object 类的方法。基于对象锁(monitor)实现。所以,调用这几个方法前,必须要先获得相应对象的锁,不然会抛出 IllegalMonitorStateExecption 异常。获取对象锁的方式就是使用 synchronized 关键字。
      调用 wait() 方法挂起任务时,线程会释放获取到的对象锁,以让其他线程能够获得这个对象的锁。流程是这样的:在调用 wait() 之前,已经获得了该对象的锁;调用 wait() 的时候,会释放对象锁;而在从 wait() 唤醒之前,线程会重新获得对象锁。
      下面是一个给汽车打蜡的例子,两个线程,一个负责打蜡,一个负责抛光。抛光任务要等待打蜡任务完成,而下一层打蜡任务要等待上一层蜡被抛光。使用 wait() 和 notify() 来进行任务同步。

    public class WaxOMatic {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            Car car = new Car();
            executorService.execute(new WaxOn(car));
            executorService.execute(new WaxOff(car));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.shutdownNow();
        }
    }
    
    class Car {
        private boolean waxOn = false;
        synchronized void waxed() {
            waxOn = true;
            notifyAll();
        }
        synchronized void buffed() {
            waxOn = false;
            notifyAll();
        }
        synchronized void waitForWaxing() throws InterruptedException {
            while (!waxOn) {
                wait();
            }
        }
        synchronized void waitForBuffing() throws InterruptedException {
            while (waxOn) {
                wait();
            }
        }
    }
    
    class WaxOn implements Runnable {
        private Car car;
        WaxOn(Car car) {
            this.car = car;
        }
        @Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    System.out.println("Wax On! " + System.currentTimeMillis());
                    Thread.sleep(200);
                    car.waxed();
                    car.waitForBuffing();
                }
            } catch (InterruptedException e) {
                System.out.println("Exiting via interrupt");
            }
            System.out.println("Ending Wax On task");
        }
    }
    
    class WaxOff implements Runnable {
        private Car car;
        WaxOff(Car car) {
            this.car = car;
        }
        @Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    car.waitForWaxing();
                    System.out.println("Wax Off! " + System.currentTimeMillis());
                    Thread.sleep(200);
                    car.buffed();
                }
            } catch (InterruptedException e) {
                System.out.println("Exiting via interrupt");
            }
            System.out.println("Ending Wax On task");
        }
    }
    

    一般使用 wait() 方法,应该用一个 while 循环去检查条件。因为无法保证唤醒的时候,条件是否满足,在每次唤醒后去检查才能保证安全。有很多可能性导致唤醒的时候条件并不满足:

    1. 有多个任务因为相同的原因在等待。第一个被唤醒的任务先进行了处理,条件又变成不满足了。

    2. 被唤醒时,其他任务做了一些操作,影响到了当前条件。

    3. 多个任务因为不同的原因在等待。因为要唤醒其他任务而被连带唤醒了。

      synchronized (monitor)
      while (someCondition) {
          monitor.wait();
      }
      

    错失的信号

    当两个线程使用 wait() / notify() / notifyAll() 进行协作时,有可能出现错过某个信号的情况。

    T1:
    synchronized(sharedMonitor) {
        <setup condition for T2>
        someCondition = false;
        sharedMonitor.notify();
    }
    
    T2:
    while (someCondition) {
        // Point1. 在这个时间点被切换
        synchronized(sharedMonitor) {
            sharedMonitor.wait();  
        }
    }
    

    T2 线程先执行,判断条件 someCondition 为 true。在 Point1 这个时间点被切换了。T1 执行,someCondition 变为 false,并且调用 notify() 。T2 继续执行,这时已经晚了,T2 调用 wait() 进行等待。而 notify() 已经错过了,T2 将一直在 wait() ,等不到唤醒的信号。这个也算是共享资源的竞争导致的问题。为了避免这种情况,我们应该把对条件的判断和 wait() 放在同一个 synchronized 代码块中。

    T2:
    synchronized(sharedMonitor) {
        while (someCondition) {
            sharedMonitor.wait();  
        }
    }
    

    notify() 还是 notifyAll()

    notify() 和 notifyAll() 。如果有多个线程在同一个对象上等待,notify() 会选择一个唤醒。因此,使用 notify() 的时候得确保唤醒的是你想要的线程。不然的话,还是只能用 notifyAll() 。

    2.2 使用 Lock 和 Condition 对象

    跟互斥一样,除了 Java 内建的 wait() 和 notify() 上,java.util.concurrent 类库中还提供了显示的工具来进行线程的同步。就是 Condition,也是一个接口。使用 Condition 对象,可以调用 await() 来挂起一个任务,通过调用 signal() 或者 signalAll() 来唤醒任务。
      Condition 是一个接口,但是 Condition 类的文档注释中对 Condition 的实现做了严格的要求。Condition 对象应该关联到 Lock, 应该通过 Lock 对象的 newCondition() 方法生成。Lock 和 Condition 的使用,基本上就和 synchronized 和 wait()/notify() 的用法差不多。调用 await()、singnal() 和 singnalAll() 等方法前,应该先对关联的 Lock 对象上锁,不然要抛出 IllegalMonitorStateException 异常。调用 await() 挂起任务时,会释放关联的锁,被唤醒前,重新获得锁。
      使用 Lock 和 Condition 好处是:Lock 和 Condition 都是自己创建的,使用上更灵活。比如,每个等待的条件可以专门弄一个 Condition 对象,这样可以更精确地控制唤醒的线程,优化程序的执行效率。
      下面是用 Lock 和 Condition 重写的给汽车打蜡的例子。

    public class WaxOMaticWithCondition {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            Car car = new Car();
            executorService.execute(new WaxOn(car));
            executorService.execute(new WaxOff(car));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.shutdownNow();
        }
        static class Car {
            private boolean waxOn = false;
            private Lock lock = new ReentrantLock();
            private Condition condition = lock.newCondition();
    
            void waxed() {
                lock.lock();
                try {
                    waxOn = true;
                    condition.signalAll();
                } finally {
                    // 养成好习惯,把 unlock 放到 finally 中,避免异常导致锁无法释放
                    lock.unlock();
                }
            }
            void buffed() {
                lock.lock();
                try {
                    waxOn = false;
                    condition.signalAll();
                } finally {
                    lock.unlock();
                }
            }
            void waitForWaxing() throws InterruptedException {
                lock.lock();
                try {
                    while (!waxOn) {
                        condition.await();
                    }
                } finally {
                    lock.unlock();
                }
            }
            void waitForBuffing() throws InterruptedException {
                lock.lock();
                try {
                    while (waxOn) {
                        condition.await();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
        static class WaxOn implements Runnable {
            private Car car;
            WaxOn(Car car) {
                this.car = car;
            }
            @Override
            public void run() {
                try {
                    while (!Thread.interrupted()) {
                        System.out.println("Wax On! " + System.currentTimeMillis());
                        Thread.sleep(200);
                        car.waxed();
                        car.waitForBuffing();
                    }
                } catch (InterruptedException e) {
                    System.out.println("Exiting via interrupt");
                }
                System.out.println("Ending Wax On task");
            }
        }
        static class WaxOff implements Runnable {
            private Car car;
            WaxOff(Car car) {
                this.car = car;
            }
            @Override
            public void run() {
                try {
                    while (!Thread.interrupted()) {
                        car.waitForWaxing();
                        System.out.println("Wax Off! " + System.currentTimeMillis());
                        Thread.sleep(200);
                        car.buffed();
                    }
                } catch (InterruptedException e) {
                    System.out.println("Exiting via interrupt");
                }
                System.out.println("Ending Wax Off task");
            }
        }
    }
    

    2.3 阻塞队列

    阻塞队列(BlockingQueue),解决“生产者-消费者”问题的神器,因为它内部为我们实现了线程之间的同步,可以极大地简化代码。当某个线程试图从阻塞队列中取一个对象是,如果队列为空,线程会被挂起等待。往阻塞队列添加对象也是一样,当队列已满时,线程会被挂起等待。

    主要关注以下几个方法:

    添加元素的方法:

    • put(E)。队列已满时,挂起线程,一直到队列中有空位。
    • offer(E): boolean。队列已满时,返回 false,表示添加失败。
    • offer(E, long, TimeUnit): boolean。队列已满时,等待一段时间。超时后,返回 false,表示添加失败。
    • add(E): boolean。添加成功,返回 true 。队列已满时,抛出异常。AbstractQueue 的实现是直接调用 offer(E) ,失败就抛出异常。

    取出元素的方法:

    • take(): E。队列为空时,挂起线程,一直到有新的元素插入。
    • pull(long, TimeUnit): 队列为空时,挂起线程,等待一段时间。超时后,返回 null 。

    线程池(ThreadPoolExecutor )就是依赖于阻塞队列实现的,任务被添加到阻塞队列中,线程从阻塞队列中获取任务。

    3. 结束线程

    前面提到的 synchronized 和 Lock 等,都会导致线程阻塞。那么,怎么结束阻塞状态的线程呢?先来看看 Java 中的线程有哪几种状态。

    1. 新建(new)。线程刚被创建时,短暂地处于这种状态
    2. 就绪状态(RUNNABLE)。线程正在 JVM 中执行。但是线程可能正在等待操作系统的处理器资源。
    3. 阻塞状态(BLOCKED、WAITING、TIMED_WAITING)。根据阻塞的原因区分了三种,这里我们详细区分。
    4. 终止状态(Terminated)。

    为什么没有RUNNING状态,因为线程获得处理器资源去实际执行,这是操作系统负责的,JVM 不管这事儿。对JVM 来说,它只知道线程当前是就绪状态,至于是在等待处理器还是实际在执行并不关心。具体可以看这篇文章。Java 线程状态之 RUNNABLE

    进入阻塞状态的原因:

    1. sleep
    2. wait
    3. 等待输入输出
    4. synchronized 或者 Lock

    对阻塞状态的线程调用 interrupt() 方法,将抛出 InterruptedException。可以用来中断线程。如果不想或者不方便直接对线程对象操作。也可以通过 executor 的 submit 方法执行 Runable,拿到 Future 对象,调用 Future 对象的 cancel 方法。cancel 方法实际上也是调用 interrupt。
      sleep wait 等阻塞可以被 interrupt() 中断,但 io 和 synchronized阻塞 不能被中断。Lock 有 lockInterruptibly() 方法,可以被中断。对于像 io 引起的阻塞,可以通过关闭底层资源来结束线程。线程池的 shutdownNow() 方法也是调用 interrupt() 方法的,也无法结束被 io 和 synchronized 阻塞的线程。

    4. 死锁

    对于临界资源,我们使用互斥锁来保证同一时刻只能有一个线程在访问。但是,使用互斥锁也容易出现死锁的问题。死锁,指的是两个或两个以上的线程(进程),每个线程持有部分资源,而又等待其他线程的资源,形成环路地等待,导致所有线程都永远无法拿到所有所需的资源。

    产生死锁的四个必要条件:

    1. 互斥条件。
    2. 不可抢占条件。
    3. 占有且申请条件。
    4. 环路等待条件。

    预防死锁的办法。破坏一个条件即可。

    1. 打破互斥条件。让资源允许共享。
    2. 打破不可抢占条件。线程请求不到资源时,释放已获取的资源。
    3. 打破占有且申请条件。线程一次性申请所有资源。
    4. 打破环路等待条件。把资源排序,所有线程按顺序请求资源,不会出现环路等待的情况。

    5. 结语

    多线程开发主要要解决的问题是竞争与协作。对于竞争,有 synchronized 关键字和 Lock 对象两种方式来实现互斥。对于协作,也有 synchronized+wait()/notify() 和 Lock + Condition 两种方式实现任务的等待。至此,我们已经对 Java 多线程开发有了较为全面地理解。下一篇文章将研究一下 Java 并发包下给我们提供的一些好用的工具。

    相关文章

      网友评论

        本文标题:Java多线程开发(二)| 多线程的竞争与协作

        本文链接:https://www.haomeiwen.com/subject/nxbezttx.html