Java多线程:交替打印FooBar

Java多线程:交替打印FooBar,第1张

Java多线程:交替打印FooBar Java多线程:交替打印FooBar

文章目录
  • Java多线程:交替打印FooBar
      • 问题
      • 方法
        • 1.Semaphore
          • 代码:
        • 2.CyclicBarrier
          • 代码
        • 3.Thread.yield()
          • 代码
        • 4.ReentrantLock
          • 代码:
        • 5.BlockingQueue
          • 代码:
        • 6.Synchronized + wait()/notify() 等待/通知机制
          • 代码:
        • 7.LockSupport
          • 代码

问题


方法 1.Semaphore

  Semaphore常用于控制同时访问特定资源的线程数。
  通过构造方法Semaphore(int permits)构造Semaphore,permits表示可用的许可证数量,例如Semaphore(10)即用于控制最多10个线程同时访问特定资源。
  如果线程要访问一个资源就必须先获得对应信号量Semaphore的许可证。线程使用Semaphore的acquire()方法来获取一个许可证,如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0。当信号量使用完时,必须释放,线程调用Semaphore的release()方法归还许可证。
  Java多线程中的Semaphore信号量和当初我们学习 *** 作系统时接触的信号量原理是类似的,我们也可以使用它完成线程间的同步

  当我们准备打印Foo时,我们须保证Bar已经打印,因此我们需要一个表示Bar已经打印完成的信号量,记为barFinish,在打印Foo之前,我们去获取barFinish对应的许可证,由于我们最开始需要先打印Foo,且只有一个打印Foo的线程需要进行许可证获取,因此barFinish的初始值设为1。
  同理,当我们准备打印Bar时,必须保证Foo已经打印,因此需要一个表示Foo已经打印完成的信号量,记为fooFinish,在打印Bar之前,我们去获取fooFinish对应的许可证,由于我们最开始需要先打印Foo,即在第一个Foo打印出来之前先禁止打印Bar,且只有一个打印Bar的线程需要进行许可证获取,因此fooFinish的初始值设为0。
  在Foo打印完成或Bar打印完成之后,需要通知另一个线程,即对应信号量的release *** 作,在foo打印完后,我们释放fooFinish信号量的许可证,通知打印bar,同理,在bar打印完后,我们释放barFinish信号量的许可证,通知打印foo。

代码:
class FooBar {
        private int n;

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

        private Semaphore fooFinish = new Semaphore(0);
        private Semaphore barFinish = new Semaphore(1);

        public void foo(Runnable printFoo) throws InterruptedException {

            for (int i = 0; i < n; i++) {
                barFinish.acquire();
                // printFoo.run() outputs "foo". Do not change or remove this line.
                printFoo.run();
                fooFinish.release();
            }
        }

        public void bar(Runnable printBar) throws InterruptedException {

            for (int i = 0; i < n; i++) {
                fooFinish.acquire();
                // printBar.run() outputs "bar". Do not change or remove this line.
                printBar.run();
                barFinish.release();
            }
        }
    }
2.CyclicBarrier

  CyclicBarrier从字面意思上理解是可循环的Barrier,它的作用是:让一组线程到达一个Barrier(屏障,即同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截(阻塞)的线程才会继续运行。从作用上来看,CyclicBarrier与CountDownLatch很相似,主要的区别是CyclicBarrier可重复使用,而CountDownLatch是一次性的。
  CyclicBarrier默认的构造方法是CyclicBarrier(int parties),参数表示屏障拦截的线程数。线程调用CyclicBarrier的await()方法告知CyclicBarrier其已经到达屏障,然后线程被阻塞。
  在CyclicBarrier的实现中,若计数器减为0,将调用nextGeneration方法将屏障转到下一代,在该方法中会将所有线程唤醒,将计数器的值重新设为parties,因此CyclicBarrier是可以重复使用的,当所有线程都到达同步点,计数器为0时,线程全被唤醒,之后计数器的值又会复原。

  因为共有2个线程,CyclicBarrier的计数值设为2。我们需要使用CyclicBarrier实现线程间的相互等待,打印foo之前需要等待bar打印完成,打印bar之前需要等待foo打印完成,第一次需要先打印foo,因此我们可以在打印foo之后调用CyclicBarrier的await方法等待打印bar的线程,在打印bar之前调用await方法等待foo打印完成。但这还不够,因为线程都达到同步点后,有可能打印foo的线程比打印bar的线程快,在打印bar之前又打印了一次foo,因此连续打印了两个foo。因此我们还需要创建一个volatile变量作为条件变量,以保证在都到达同步点后,bar打印一定在foo打印之前,将该条件变量设为barExecuting,或许会想到在打印bar之前的await()方法后,加上barExecuting = true,表示bar线程要执行打印bar了,foo线程先等等,同时在打印foo之前加上while判断,若barExecuting = true,则执行空循环等待。但这不行,有可能还没执行改变barExecuting状态语句,foo就打印完了,因此在打印完foo之后,将barExecuting = true,用foo线程自己来控制自己,这样就能保证一定不会出现连续的两个foo了。打印完bar之后,将barExecuting = false,通知foo线程bar已经打印完了,foo线程退出空循环。

代码
class FooBar {
        private int n;

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

        private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
        private volatile boolean barExecuting = false;

        public void foo(Runnable printFoo) throws InterruptedException {

            for (int i = 0; i < n; i++) {
                while (barExecuting) {

                }
                // printFoo.run() outputs "foo". Do not change or remove this line.
                printFoo.run();
                barExecuting = true;
                try {
                    cyclicBarrier.await();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

            }
        }

        public void bar(Runnable printBar) throws InterruptedException {

            for (int i = 0; i < n; i++) {
                try {
                    cyclicBarrier.await();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                // printBar.run() outputs "bar". Do not change or remove this line.
                printBar.run();
                barExecuting = false;
            }
        }
    }
3.Thread.yield()

  Thread.yield()表示线程礼让,线程礼让是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,运行状态/就绪状态——>阻塞状态,将CPU资源让给其他线程来使用
  假如线程甲和线程乙在交替执行,某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了CPU资源,执行业务逻辑,但不代表线程甲一直暂停执行。线程甲只是在特定的时间节点礼让,过了时间节点,线程甲再次进入就绪状态,和线程乙争夺CPU资源

  正确的思路是,当现在不应该是foo线程运行时,foo线程进行礼让,不应该是bar线程运行时,bar线程进行礼让。因此我们还需要创建一个volatile变量作为条件变量,以判断现在到底是foo线程该执行还是bar线程该执行,设该条件变量为fooCanExecute,表示foo线程是否该执行,初值为true,在打印foo之前判断fooCanExecute是否为true,若为true则打印foo,同时将fooCanExecute = false,若打印前判断fooCanExecute为false,则进行线程礼让。同理,在打印bar之前判断fooCanExecute是否为false,若为false则打印bar,同时fooCanExecute = true,若打印前判断fooCanExecute为true,进行线程礼让。需注意循环变量i的恢复 *** 作。

代码
class FooBar {
    private int n;

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

            private volatile boolean fooCanExecute = true;

        public void foo(Runnable printFoo) throws InterruptedException {

            for (int i = 0; i < n; i++) {
                if (fooCanExecute) {
                    // printFoo.run() outputs "foo". Do not change or remove this line.
                    printFoo.run();
                    fooCanExecute = false;
                }
                else {
                    Thread.yield();
                    i--;
                }
            }
        }

        public void bar(Runnable printBar) throws InterruptedException {

            for (int i = 0; i < n; i++) {
                if (!fooCanExecute) {
                    // printBar.run() outputs "bar". Do not change or remove this line.
                    printBar.run();
                    fooCanExecute = true;
                } else {
                    Thread.yield();
                    i--;
                }
            }
        }
}
4.ReentrantLock

  打印foo或bar的代码区域作为不能两个线程同时执行的区域,将其用ReentrantLock上锁,但这还不够,因为若foo线程速度更快,可能会连续打印foo。因此还需要volatile变量作为条件变量,设为fooCanExecute,初始为true,打印foo前判断其是否为true,打印完后将其变为false,bar线程的处理与其相反

  由于ReentrantLock的可重复上锁和解锁的性质,下述代码执行过程中,某线程可能会不断上锁解锁,导致开销很大,因此该代码提交后会超时,这样使用ReentrantLock不是一种较好的实现方式

代码:
    class FooBar {
        private int n;
        public FooBar(int n) {
            this.n = n;
        }
        private volatile boolean fooCanExecute = true;
        private ReentrantLock lock = new ReentrantLock(true);
        public void foo(Runnable printFoo) throws InterruptedException {

            for (int i = 0; i < n;) {
                lock.lock();
                try {
                    if (fooCanExecute) {
                        // printFoo.run() outputs "foo". Do not change or remove this line.
                        printFoo.run();
                        fooCanExecute = false;
                        i++;
                    }
                } finally {
                    lock.unlock();
                }
            }
        }

        public void bar(Runnable printBar) throws InterruptedException {

            for (int i = 0; i < n;) {
                lock.lock();
                try {
                    if (!fooCanExecute) {
                        // printBar.run() outputs "bar". Do not change or remove this line.
                        printBar.run();
                        fooCanExecute = true;
                        i++;
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }
5.BlockingQueue

  阻塞队列BlockingQueue是一个支持阻塞的插入和移出方法的队列,阻塞的插入指当队列满时,队列会阻塞插入元素的线程,直到队列不满。阻塞的移出是指当队列为空时,获取元素的线程会被阻塞,等待队列变为非空。
  阻塞队列的put(e)和take()方法即对应阻塞的插入和移出
  根据上述性质,我们可以知道阻塞队列很适合用于生产者消费者的场景。

  我们也可以用阻塞队列完成本题,我们将打印foo和打印bar看作两个任务,创建两个阻塞队列表示任务队列,由于第一次需先打印foo,因此表示打印foo任务的阻塞队列先置入一个元素,表示bar任务的阻塞队列初始为空,在打印foo和bar之前,我们调用阻塞的移出方法,从任务队列中取元素,若能取到,则执行打印,否则阻塞。在打印完成后,向对方的任务队列中插入元素

  JDK提供了7种阻塞队列,这里使用用链表实现的linkedBlockingQueue

代码:
    class FooBar {
        private int n;
        public FooBar(int n) {
            this.n = n;
        }

        private linkedBlockingQueue fooQ = new linkedBlockingQueue() {{add(0);}};
        private linkedBlockingQueue barQ = new linkedBlockingQueue<>();

        public void foo(Runnable printFoo) throws InterruptedException {

            for (int i = 0; i < n;i++) {
                fooQ.take();
                        // printFoo.run() outputs "foo". Do not change or remove this line.
                        printFoo.run();
                barQ.put(0);
            }
        }

        public void bar(Runnable printBar) throws InterruptedException {

            for (int i = 0; i < n;i++) {
                barQ.take();
                        // printBar.run() outputs "bar". Do not change or remove this line.
                        printBar.run();
                fooQ.put(0);
            }
        }
    }
6.Synchronized + wait()/notify() 等待/通知机制

  等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续 *** 作。
  等待/通知机制是依托于同步机制的,一般先使用synchronized获取对象O的锁,再调用wait()或notify()

  单用wait()/notify()达不到效果,在打印前调用wait(),打印后调用notify(),两个线程都会在wait()处阻塞。因此我们还需要创建volatile变量表示条件变量,设为canBar,表示bar线程是否能执行,初值为false,在打印foo前先判断canBar是否为false(用if或while判断均可),若为false,则打印foo,将canBar设为true,之后调用notifyAll()唤醒等待的bar线程。若canBar为true,则调用wait()等待。Bar线程中的处理相反即可

代码:
    class FooBar {
        private int n;
        public FooBar(int n) {
            this.n = n;
        }

        private Object lock = new Object();
        private volatile boolean canBar = false;

        public void foo(Runnable printFoo) throws InterruptedException {

            for (int i = 0; i < n;i++) {
                synchronized (lock) {
                    while (canBar) {
                        lock.wait();
                    }
                        // printFoo.run() outputs "foo". Do not change or remove this line.
                        printFoo.run();
                    canBar = true;
                    lock.notifyAll();
                }
            }
        }

        public void bar(Runnable printBar) throws InterruptedException {

            for (int i = 0; i < n;i++) {
                synchronized (lock) {
                    while (!canBar) {
                        lock.wait();
                    }
                        // printBar.run() outputs "bar". Do not change or remove this line.
                        printBar.run();
                    canBar = false;
                    lock.notifyAll();
                }
            }
        }
    }
7.LockSupport

  LockSupport工具类定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能。
  LockSupport定义了一组以park开头的方法用来阻塞当前线程,如LockSupport.park()表示阻塞当前线程,以及LockSupport.unpark(Thread thread)唤醒一个被阻塞的线程。

  由于LockSupport的唤醒方法需要线程作为参数,因此创建一个ConcurrentHashMap保存线程,key为String,value为Thread,调用唤醒方法时参数从ConcurrentHashMap中取。
  同样也需借助volatile变量作为条件变量以判断当前打印是否能执行,设为canBar,初值为false。在打印foo之前,判断canBar是否为false,若为false,则打印foo,canBar设为true,并调用unpark方法唤醒bar线程。若打印前判断canBar为true,则调用park方法阻塞自己。Bar线程的处理同理,相反即可。

代码
    class FooBar {
        private int n;
        public FooBar(int n) {
            this.n = n;
        }

        private volatile boolean canBar = false;
        private Map map = new ConcurrentHashMap<>();


        public void foo(Runnable printFoo) throws InterruptedException {
            map.put("foo", Thread.currentThread());
            for (int i = 0; i < n;i++) {
                    while (canBar) {
                        LockSupport.park();
                    }
                        // printFoo.run() outputs "foo". Do not change or remove this line.
                        printFoo.run();
                    canBar = true;
                    LockSupport.unpark(map.get("bar"));
            }
        }

        public void bar(Runnable printBar) throws InterruptedException {
            map.put("bar", Thread.currentThread());
            for (int i = 0; i < n;i++) {
                    while (!canBar) {
                        LockSupport.park();
                    }
                        // printBar.run() outputs "bar". Do not change or remove this line.
                        printBar.run();
                    canBar = false;
                    LockSupport.unpark(map.get("foo"));
            }
        }
    }

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5120523.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-17
下一篇 2022-11-17

发表评论

登录后才能评论

评论列表(0条)

保存