Loading...

Java多线程学习总结:初窥门径

最近断断续续终于把《Java并发编程的艺术》这本书给看完了,大部分内容都是在上课时看的,看完还是感觉不太扎实,且有些源码部分看不懂就快速过了一遍,有点囫囵吞枣的意思,故打算找一些多线程的题目来做做,验证一下自己最近多线程学习的质量。于是顺便把收集的题目分享出来,大家一起进步~~

题目一

题目:

我们提供了一个类:

public class Foo {
  public void first() { print("first"); }
  public void second() { print("second"); }
  public void third() { print("third"); }
}

三个不同的线程将会共用一个 Foo 实例。

线程 A 将会调用 first() 方法
线程 B 将会调用 second() 方法
线程 C 将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/print-in-order

题解:

class Foo {

    private volatile int status = 1;
    private final Object lock =  new Object();

    public Foo() {
    }

    public void first(Runnable printFirst) throws InterruptedException {
        synchronized(lock) {
            while (this.status != 1) lock.wait();
            printFirst.run();
            this.status = 2;
            lock.notifyAll();
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        synchronized(lock) {
            while (this.status != 2) lock.wait();
            printSecond.run();
            this.status = 3;
            lock.notifyAll();   
        }
    }

    public void third(Runnable printThird) throws InterruptedException {
        synchronized(lock) {
            while (this.status != 3) lock.wait();
            printThird.run();
        }
    }
}

看一圈大佬们的评论和题解,个人感觉上面题解的原语写法看起来会比较清晰...

题目二

题目:

我们提供一个类:

class FooBar {
  public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
    }
  }

  public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
  }
}

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。

请设计修改程序,以确保 "foobar" 被输出 n 次。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/print-foobar-alternately

题解:

class FooBar {
    private int n;
    private volatile int status = 0;
    private final Object lock = new Object();

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

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            synchronized(lock) {
                while (status != 0) lock.wait();
                printFoo.run();
                status = 1;
                lock.notifyAll();
            }
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
             synchronized(lock) {
                while (status != 1) lock.wait();
                printBar.run();
                status = 0;
                lock.notifyAll();
            }
        }
    }
}

感觉这题和上题没什么区别啊?...我就直接用上题的解法了....因为在上课,就没过多去看其他解法...

题目三

题目:

现在有两种线程,氧 oxygen 和氢 hydrogen,你的目标是组织这两种线程来产生水分子。

存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。

氢和氧线程会被分别给予 releaseHydrogen 和 releaseOxygen 方法来允许它们突破屏障。

这些线程应该三三成组突破屏障并能立即组合产生一个水分子。

你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。

换句话说:

  • 如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
  • 如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。
    书写满足这些限制条件的氢、氧线程同步代码。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/building-h2o

题解:

class H2O {
    private final CyclicBarrier cyclicBarrier; // 屏障
    private final Semaphore hydrogenSemaphore; // 氢原子的信号量
    private final Semaphore oxygenSemaphore; // 氧原子的信号量

    public H2O() {
        this.cyclicBarrier = new CyclicBarrier(3);
        this.hydrogenSemaphore = new Semaphore(2);
        this.oxygenSemaphore = new Semaphore(1);
    }

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        hydrogenSemaphore.acquire();
        releaseHydrogen.run();
        try {
            cyclicBarrier.await();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        hydrogenSemaphore.release();
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        oxygenSemaphore.acquire();
        releaseOxygen.run();
        try {
            cyclicBarrier.await();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        oxygenSemaphore.release(); 
    }
}

这题感觉挺有意思的,信号量+屏障组合解决问题,让我想到了设计模式之间的组合。各个“成员”发挥自己的特性,各司其职,达到1+1>2的效果(⊙ˍ⊙)

题目四

题目:

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

图1

问题描述和图片来自维基百科 wikipedia.org

哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

  • philosopher 哲学家的编号。
  • pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。
  • eat 表示吃面。
  • putLeftFork 和 putRightFork 表示放下左边或右边的叉子。
  • 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。

给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/the-dining-philosophers

题解:

题解参考自

作者:gfu

链接:https://leetcode-cn.com/problems/the-dining-philosophers/solution/1ge-semaphore-1ge-reentrantlockshu-zu-by-gfu/

class DiningPhilosophers {
    
    private final Semaphore eatLimit = new Semaphore(4);;
    private final ReentrantLock[] reentrantLocks = {
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock()
        };

    public DiningPhilosophers() {
    }

    public void wantsToEat(int philosopher,
                           Runnable pickLeftFork,
                           Runnable pickRightFork,
                           Runnable eat,
                           Runnable putLeftFork,
                           Runnable putRightFork) throws InterruptedException {
        // 初始化叉子编号
        int leftFork = (philosopher + 1) % 5;
        int rightFork = philosopher;                        
        // 状态锁定
        eatLimit.acquire();
        reentrantLocks[leftFork].lock();
        reentrantLocks[rightFork].lock();
        // 动作执行
        pickLeftFork.run();
        pickRightFork.run();
        eat.run();
        putLeftFork.run();
        putRightFork.run();
        // 状态锁定解除
        reentrantLocks[leftFork].unlock();
        reentrantLocks[rightFork].unlock();
        eatLimit.release();
    }
}

本菜鸡不禁陷入了沉思(´▽`).....看题目看了“半天”....看大佬题解看了“半天”....

题目五

CopyOnWriteArrayLis源码赏析

首先,允许我粘贴一份关于CopyOnWriteArrayLis的介绍...

CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。

集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略。

添加元素的方法

public boolean add(E e) {
    // this.lock就是final transient ReentrantLock lock = new ReentrantLock();
	final ReentrantLock lock = this.lock; 
    lock.lock();
    try {
    	Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
   	} finally {
   		lock.unlock();
    }
}

原理:首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

个人理解:

​ 1、首先拿到一个重入锁并锁上

​ 2、获取了CopyOnWriteArrayLis原数组及其长度,然后把原数组拷贝到新数组中(因为要添加元素,所以长度加一)

​ 3、把新加的元素赋值给新数组下标len的位置,把CopyOnWriteArrayLis的数组设为新数组

​ 4、返回true,最后释放锁。

获取元素方法:

public E get(int index) {
	return get(getArray(), index);
}

个人理解:很简单,特性就是不上锁的读....看代码字面就能会意,无需言传

删除元素的方法:

public E remove(int index) {
	final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
       		setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

原理:将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。

个人理解:

​ 1、先获取了一个重入锁并锁上

​ 2、获取了CopyOnWriteArrayLis原数组及其长度

​ 3、接着获取相应下标的值并计算出删除后的下标,此时

  • 如果删除后下标为0的话:就把当前数组的前len-1个元素拷贝到CopyOnWriteArrayLis的数组(因为如果删除后下标为0的话,就说明删除的是原数组最后一个元素);

  • 否则新建一个容量为len-1的数组

    • 然后把原数组从下标0开始拷贝共index个的元素拷贝到新数组中(拷贝新数组下标为0的地方开始)
    • 接着把下标index+1开始拷贝共numMoved个元素到新数组中(此时从新数组下标index开始拷贝)

    感觉我描述的或许不太清楚,各位可以对着这个方法自行理解

    public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
    参数说明:
      Object src : 原数组
       int srcPos : 从原数据的起始位置开始
      Object dest : 目标数组
      int destPos : 目标数组的开始起始位置
      int length  : 要copy的数组的长度
    

​ 4、最后把新数组设为CopyOnWriteArrayLis的数组,最后返回删除的元素并解锁

设置元素的方法:

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);
        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

个人理解:

​ 1、获取锁,锁上锁

​ 2、获取原数组,以及原数组相应index的值(即oldValue)

  • 如果oldValue不等于要更新的值,就把之前获取到的原数组赋给一个新数组对象(即newElements),接着把新数组下标为index的元素设为要更新的值,最后把新数组拷贝到CopyOnWriteArrayLis数组中
  • 如果oldValue等于要更新的值,那就直接把获取到的原数组拷贝到opyOnWriteArrayLis数组中

​ 3、最后返回被更新的值并释放锁

由于篇幅有限,CopyOnWriteArrayLis源码就说到这吧

总结

下个阶段打算开始看《Java多线程编程实战指南(设计模式篇)》,但是最近要期末了...得赶紧复习去了(╯‵□′)╯︵┻━┻

posted @ 2021-01-08 18:01  _轻舟  阅读(149)  评论(0编辑  收藏  举报