使用条件锁控制多个线程同步(java实现)

2021/4/17 20:28:09

本文主要是介绍使用条件锁控制多个线程同步(java实现),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

题目:

解答:

讲解:


最近做了一道多线程同步的题目,我使用了条件锁的方式解答。通过做这道题,我们能对锁的应用有一个基本的了解,这篇文章就来简单的讲解一下。

Ps:做完了后发现这是力扣上的原题,题目链接:https://leetcode-cn.com/problems/print-zero-even-odd/,这是我的提交记录:

可以在力扣网上看到这道题多种多样的解法,请读者自行探索,本文只针对条件锁实现方式讲解。

 

题目:

假设有这么一个类:

class ZeroEvenOdd {
  public ZeroEvenOdd(int n) { ... }      // 构造函数
  public void zero(printNumber) { ... }  // 仅打印出 0
  public void even(printNumber) { ... }  // 仅打印出 偶数
  public void odd(printNumber) { ... }   // 仅打印出 奇数
}
相同的一个 ZeroEvenOdd 类实例将会传递给三个不同的线程:

线程 A 将调用 zero(),它只输出 0 。
线程 B 将调用 even(),它只输出偶数。
线程 C 将调用 odd(),它只输出奇数。
每个线程都有一个 printNumber 方法来输出一个整数。请修改给出的代码以输出整数序列 010203040506... ,其中序列的长度必须为 2n。

 

示例 1:

输入:n = 2
输出:"0102"
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 "0102"。
示例 2:

输入:n = 5
输出:"0102030405"

解答:

package com.demo;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntConsumer;

public class ZeroEvenOddImpl1 {

    private int n;

    private int curr = 0;

    private ReentrantLock lock;

    private Condition cA;

    private Condition cB;

    private Condition cC;

    private volatile  int control = 0;

    public ZeroEvenOddImpl1(int n) {
        lock = new ReentrantLock();
        cA = lock.newCondition();
        cB = lock.newCondition();
        cC = lock.newCondition();
        this.n = n;
    }

    public static void main(String[] args) {
        ZeroEvenOddImpl1 impl = new ZeroEvenOddImpl1(10);
        Thread tA = new Thread( ()-> {
            try {
                impl.zero(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tB = new Thread( ()-> {
            try {
                impl.even(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tC = new Thread( ()-> {
            try {
                impl.odd(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        tC.start();
        tB.start();
        tA.start();
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber)  throws InterruptedException {
        while(control<2);
        lock.lock();
        try {
            while(true) {
                if(curr >=n) {
                    cB.signal();
                    cC.signal();
                    return;
                }
                printNumber.accept(0);
                cC.signal();
                cA.await();
                if(curr >=n) {
                    cB.signal();
                    cC.signal();
                    return;
                }
                printNumber.accept(0);
                cB.signal();

                cA.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        } finally {
            lock.unlock();
        }

    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        lock.lock();
        control++;
        try {
            while (true) {
                cC.await();
                if(curr >=n) {
                    cA.signal();
                    return;
                }
                printNumber.accept(++curr);
                cA.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        } finally {
            lock.unlock();
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        lock.lock();
        control++;
        try {
            while(true) {
                cB.await();
                if(curr >=n) {
                    cA.signal();
                    return;
                }
                printNumber.accept(++curr);
                cA.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        } finally {
            lock.unlock();
        }
    }
}

讲解:

        这里先讲解代码中使用的api,以下是条件锁创建的方式,一个ReentrantLock可以创建多个条件锁。

            ReentrantLock lock = new ReentrantLock();

            Condition cA = lock.newCondition();

        本次代码使用了Condition的两个方法:await、signal。await的作用是阻塞当前线程,并释放掉锁资源。signal的作用是唤醒调用了await的线程,被唤醒线程从调用await处继续执行。这两个函数都需要在获取锁后调用,否则抛出IllegalMonitorStateException异常,即需要先执行lock.lock()(养成良好的编码习惯,请在finally 中释放锁资源:    lock.unlock())。

        接下来讲解解题思路,这道题本质上就是在控制多个线程的执行顺序(即同步)。我们把题目中有几个线程,线程之间又是什么执行顺序的细节忽略掉,只关心控制的方法。这里的思路就是为每一个线程分配一个单独的条件锁。假设为B线程分配的条件锁为cB,那么控制B线程的同步需要先执行cB.await()来阻塞自己,并等待其他线程通知自己执行任务。如果A线程执行完毕后轮到B线程执行,那么A线程在执行完任务后去调用cB.signal()将B线程唤醒。同理,如果B线程执行完毕后轮到C线程执行,那么也在自己执行完任务后去调用cC.signal()将C线程唤醒,通知C线程执行任务。解题思路有了,再解题很容易了。不管题目中有几个线程,它们之间又是什么执行顺序,只要把这个思路往上套都能解决问题。

        最后再来讲解一下本题代码中的两个小细节。一:细心的读者已经在思考control变量的作用了,其实control变量是为了保证A线程要在B、C线程获取锁之后再获取锁。为什么要这样做呢,假设一种情况:A线程最先获取到锁,那么A线程会执行cC.signal(),而此时C线程还没有获取到锁,cC.signal()将起不到任何作用,这就会导致C锁永远得不到通知,从而导致线程间产生死锁问题,任务全部卡住不动。而A线程获取锁的顺序是第二的话也会有类似的问题。有的人可能会想通过延时启动或者通过使用Thread类的isAlive方法来达到让A最后获取锁的效果,但是这样都不能百分百保证达到目的,是不安全的代码。二:如果n为一个偶数,然后将even方法finally代码块中的释放锁代码注释掉,那么在执行后会发现程序不能退出了,这是因为虽然B线程执行完操作已经通知A线程了,但是通知后自己的线程就通知了,这时如果不释放锁是不能真的通知到的。所以我们一定要养成好的释放资源习惯。

        最后再提供一种该题的简单是实现方式,但是该实现方式应该是多线程不安全的,在牛客网提交也提示超时,欢迎有想法的读者评论讨论。

package com.demo;

import java.util.Currency;
import java.util.function.IntConsumer;

public class ZeroEvenOddImpl2 {

    private int n;

    private volatile int exec = 0;

    private volatile int curr = 0;

    public ZeroEvenOddImpl2(int n) {
        this.n = n;
    }

    public static void main(String[] args) {
        ZeroEvenOddImpl2 impl = new ZeroEvenOddImpl2(51);
        Thread tA = new Thread( ()-> {
            try {
                impl.zero(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tB = new Thread( ()-> {
            try {
                impl.even(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tC = new Thread( ()-> {
            try {
                impl.odd(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        tC.start();
        tB.start();
        tA.start();
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        while(curr < n) {
            while(exec!=0 && curr < n);
            if(curr < n) {
                printNumber.accept(0);
                exec = 1;
            }

            while(exec!=0 && curr < n);
            if(curr < n) {
                printNumber.accept(0);
                exec = 2;
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        while(curr<n) {
            while(exec!=2 && curr<n);
            if(curr<n) {
                printNumber.accept(++curr);
                exec = 0;
            }
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        while(curr<n) {
            while(exec!=1 && curr<n);
            if(curr<n) {
                printNumber.accept(++curr);
                exec = 0;
            }
        }
    }
}

 

 

 

 

 

 

 



这篇关于使用条件锁控制多个线程同步(java实现)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程