LeetCode上专门的多线程题目版块,本文通过解决几道leetCode上的多线程协作交替打印的编程题,来加深对并发编程的理解,并训练一下并发编程思想。
一、交替打印
示例
输入: n = 2
输出: “foobarfoobar”
解释: “foobar” 将被输出两次。
方法1:synchronized锁
背景知识
1、wait()使当前线程阻塞,前提是必须先获得锁,一般配synchronized 关键字使用,即只能在synchronized同步代码块里使用wait()、notify/notifyAll()方法。
2、 由于wait()、notify/notifyAll()在synchronized 代码块执行,说明当前线程一定是获取了锁的。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。只有当notify/notifyAll()被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait() ,才释放锁。
3、notify/notifyAll()的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区,以唤醒其他线程让其获得锁。
4、notify和wait的顺序不能错,如果A线程先执行notify方法,B线程再执行wait方法,那么B线程是无法被唤醒的。
5、notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll方法。比如在生产者——消费者场景下使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
public class FooBarSync {
private int n;
private volatile boolean fooExec = true;
public FooBarSync(int n) {
this.n = n;
}
public void foo() throws InterruptedException {
int i = 0;
synchronized (this) {
while (i < n) {
if (fooExec) {
System.out.print("foo");
i++;
fooExec = false;
this.notifyAll();
} else {
//foo线程等待
this.wait();
}
}
}
}
public void bar() throws InterruptedException {
int i = 0;
synchronized (this) {
while (i < n) {
if (!fooExec) {
System.out.print("bar");
i++;
fooExec = true;
this.notifyAll();
} else {
//bar线程等待
this.wait();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
FooBarSync fooBar = new FooBarSync(5);
new Thread(()-> {
try {
fooBar.foo();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()-> {
try {
fooBar.bar();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
方法2:ReentrantLock
背景知识
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。CAS是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。
public class FooBarLock {
private int n;
private ReentrantLock lock = new ReentrantLock();
private volatile boolean fooExec = true;
public FooBarLock(int n) {
this.n = n;
}
public void foo() throws InterruptedException {
for (int i = 0; i < n; ) {
lock.lock();
try {
if (fooExec) {
System.out.print("foo");
fooExec = false;
i++;
}
} finally {
lock.unlock();
}
}
}
public void bar() throws InterruptedException {
for (int i = 0; i < n; ) {
lock.lock();
try {
if (!fooExec) {
System.out.print("bar");
fooExec = true;
i++;
}
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
FooBarLock fooBar = new FooBarLock(5);
new Thread(()-> {
try {
fooBar.foo();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()-> {
try {
fooBar.bar();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
二、按条件打印字符串
交替打印字符串
编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:
如果这个数字可以被 3 整除,输出 “fizz”。
如果这个数字可以被 5 整除,输出 “buzz”。
如果这个数字可以同时被 3 和 5 整除,输出 “fizzbuzz”。
例如,当 n = 15,输出: 1, 2, fizz, 4, buzz, fizz, 7, 8, fizz, buzz, 11, fizz, 13, 14, fizzbuzz。
方法1:synchronized锁
public class FizzBuzzSync {
private int n;
/**
* 定义当前数字
*/
private int i = 1;
public FizzBuzzSync(int n) {
this.n = n;
}
//被3整除不能被5整除,输出fizz
public void fizz() throws InterruptedException {
synchronized (this) {
while (i <= n) {
if (i % 3 == 0 && i % 5 != 0) {
System.out.print("fizz");
i++;
this.notifyAll();
} else {
//Fizz线程等待
this.wait();
}
}
}
}
//被5整除不能被3整除,输出buzz
public void buzz() throws InterruptedException {
synchronized (this) {
while (i <= n) {
if (i % 3 != 0 && i % 5 == 0) {
System.out.print("buzz");
i++;
this.notifyAll();
} else {
//Buzz线程等待
this.wait();
}
}
}
}
//能被3整除能被3整除,输出fizzbuzz
public void fizzbuzz() throws InterruptedException {
synchronized (this) {
while (i <= n) {
if (i % 3 == 0 && i % 5 == 0) {
System.out.print("fizzbuzz");
i++;
this.notifyAll();
} else {
//FizzBuzz线程等待
this.wait();
}
}
}
}
//不能被3整和5整除,输出fizzbuzz
public void number() throws InterruptedException {
synchronized (this) {
while (i <= n) {
if (i % 3 != 0 && i % 5 != 0) {
System.out.print(i);
i++;
this.notifyAll();
} else {
//Number线程等待
this.wait();
}
}
}
}
public static void main(String[] args) {
System.out.println("Hello World!");
FizzBuzzLock fizzBuzz = new FizzBuzzLock(15);
new Thread(()-> {
try {
fizzBuzz.fizz();
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
fizzBuzz.buzz();
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
fizzBuzz.fizzbuzz();
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
fizzBuzz.number();
} catch (InterruptedException e) {
}
}).start();
}
}
方法2:ReentrantLock + Condition
Condition强大的地方在于它能够精确的控制多线程的休眠与唤醒(注意是唤醒,唤醒只意味着进入了同步队列,不意味着一定能获得资源),相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式能够更加安全和高效地实现线程间协作。
public class FizzBuzzLock {
private int n;
private ReentrantLock lock = new ReentrantLock();
/**
* 定义Condition
*/
private Condition condition = lock.newCondition();
/**
* 定义当前数字
*/
private int i = 1;
public FizzBuzzLock(int n) {
this.n = n;
}
//被3整除不能被5整除,输出fizz
public void fizz() throws InterruptedException {
lock.lock();
try {
while (i <= n) {
if (i % 3 == 0 && i % 5 != 0) {
System.out.print("fizz");
i++;
condition.signalAll();
} else {
//Fizz线程等待
condition.await();
}
}
} finally {
lock.unlock();
}
}
//被5整除不能被3整除,输出buzz
public void buzz() throws InterruptedException {
lock.lock();
try {
while (i <= n) {
if (i % 3 != 0 && i % 5 == 0) {
System.out.print("buzz");
i++;
condition.signalAll();
} else {
//Buzz线程等待
condition.await();
}
}
} finally {
lock.unlock();
}
}
//能被3整除也能被5整除,输出fizzbuzz
public void fizzbuzz() throws InterruptedException {
lock.lock();
try {
while (i <= n) {
if (i % 3 == 0 && i % 5 == 0) {
System.out.print("fizzbuzz");
i++;
condition.signalAll();
} else {
//FizzBuzz线程等待
condition.await();
}
}
} finally {
lock.unlock();
}
}
//不能被3整和5整除,输出fizzbuzz
public void number() throws InterruptedException {
lock.lock();
try {
while (i <= n) {
if (i % 3 != 0 && i % 5 != 0) {
System.out.print(i);
i++;
condition.signalAll();
} else {
//Number线程等待
condition.await();
}
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
System.out.println("Hello World!");
FizzBuzzLock fizzBuzz = new FizzBuzzLock(15);
new Thread(()-> {
try {
fizzBuzz.fizz();
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
fizzBuzz.buzz();
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
fizzBuzz.fizzbuzz();
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
fizzBuzz.number();
} catch (InterruptedException e) {
}
}).start();
}
}
三、交替打印零与奇偶数
打印零与奇偶数
示例
输入:n = 2
输出:“0102”
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 “0102”。
输入:n = 5
输出:“0102030405”
方法1:ReentrantLock + Condition
Condition强大的地方在于它能够精确的控制多线程的休眠与唤醒(注意是唤醒,唤醒只意味着进入了同步队列,不意味着一定能获得资源),相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式能够更加安全和高效地实现线程间协作。
public class ZeroEvenOdd {
/**
* 最后一个打印的数字
*/
private int n;
/**
* 控制打印的标志变量
* flag = 1表示要打印奇数了,flag = 2表示要打印偶数了,flag = 0表示打印0
*/
private int flag = 0;
private Lock lock = new ReentrantLock();
private Condition zeroCondition = lock.newCondition();
private Condition evenCondition = lock.newCondition();
private Condition oddCondition = lock.newCondition();
public ZeroEvenOdd(int n) {
this.n = n;
}
// 仅打印出 0
public void zero(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
for (int i = 1; i <= n; i++) {
if (flag != 0) {
// zero线程等待
zeroCondition.await();
}
printNumber.accept(0);
if (i % 2 == 1) {
flag = 1;
// 指定唤醒odd线程
oddCondition.signal();
} else {
flag = 2;
// 指定唤醒even线程
evenCondition.signal();
}
}
} finally {
lock.unlock();
}
}
// 仅打印出 偶数
public void even(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
for (int i = 2; i <= n; i += 2) {
if (flag != 2) {
// even线程等待
evenCondition.await();
}
printNumber.accept(i);
flag = 0;
// 唤醒zero线程
zeroCondition.signal();
}
} finally {
lock.unlock();
}
}
// 仅打印出 奇数
public void odd(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
for (int i = 1; i <= n; i += 2) {
if (flag != 1) {
// odd线程等待
oddCondition.await();
}
printNumber.accept(i);
flag = 0;
// 唤醒zero线程
zeroCondition.signal();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ZeroEvenOdd zeroEvenOdd = new ZeroEvenOdd(5);
new Thread(()-> {
try {
zeroEvenOdd.odd(e -> System.out.print(e));
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
zeroEvenOdd.zero(e -> System.out.print(e));
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
zeroEvenOdd.even(e -> System.out.print(e));
} catch (InterruptedException e) {
}
}).start();
}
}
方法2:CountDownLatch
背景知识
CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在锁上等待的线程就可以恢复工作了
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException;
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException;
//将count值减1
public void countDown();
public class ZeroEvenOdd {
/**
* 最后一个打印的数字
*/
private int n;
private CountDownLatch zeroLatch = new CountDownLatch(1);
private CountDownLatch evenLatch = new CountDownLatch(1);
private CountDownLatch oddLatch = new CountDownLatch(1);
public ZeroEvenOdd(int n) {
this.n = n;
}
// 仅打印出 0
public void zero(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
zeroLatch = new CountDownLatch(1);
printNumber.accept(0);
// 如果已经打印了奇数个0,那么接下来要打印奇数
if ((i&1) == 1) {
oddLatch.countDown();
} else {
evenLatch.countDown();
}
// 当前线程挂起,直到值减为0
zeroLatch.await();
}
}
// 仅打印出 偶数
public void even(IntConsumer printNumber) throws InterruptedException {
for (int i = 2; i <= n; i += 2) {
evenLatch.await();
printNumber.accept(i);
// 接下来要打印0
evenLatch = new CountDownLatch(1);
zeroLatch.countDown();
}
}
// 仅打印出 奇数
public void odd(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i += 2) {
oddLatch.await();
printNumber.accept(i);
// 接下来要打印0
oddLatch = new CountDownLatch(1);
zeroLatch.countDown();
}
}
public static void main(String[] args) {
ZeroEvenOdd zeroEvenOdd = new ZeroEvenOdd(5);
new Thread(()-> {
try {
zeroEvenOdd.odd(e -> System.out.print(e));
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
zeroEvenOdd.zero(e -> System.out.print(e));
} catch (InterruptedException e) {
}
}).start();
new Thread(()-> {
try {
zeroEvenOdd.even(e -> System.out.print(e));
} catch (InterruptedException e) {
}
}).start();
}
}
四、手写阻塞队列
实现一个有界阻塞队列,拥有put方法与take方法,当队列满时put方法阻塞,当队列空时take方法阻塞。
public class MyBlockingQueue {
private Deque<Integer> queue;
private int capacity;
private Lock lock = new ReentrantLock();
private Condition isFull = lock.newCondition();
private Condition isEmpty = lock.newCondition();
public MyBlockingQueue(int capacity) {
this.queue = new LinkedList<>();
this.capacity = capacity;
}
public void put(String item) throws InterruptedException {
lock.lock();
try {
// 队列已满
while (queue.size() >= capacity) {
// put线程等待
isFull.await();
}
queue.addLast(item);
// 唤醒take线程
isEmpty.signalAll();
} finally {
lock.unlock();
}
}
public String take() throws InterruptedException {
lock.lock();
String res = null;
try {
// 队列已满
while (container.size() == 0) {
// take线程等待
isEmpty.await();
}
res = queue.pollFirst();
// 唤醒put线程
isFull.signalAll();
return res;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
MyBlockingQueue blockingQueue = new MyBlockingQueue(64);
new Thread(()-> {
for (int i = 0; i < 100; i++) {
try {
blockingQueue.put(String.valueOf(i));
System.out.println("放入" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()-> {
for (;;) {
try {
System.out.println("取出" + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
五、按顺序输出字符串
问题描述:有4个线程和1个公共的字符串数组。线程1的功能就是向数组输出A,线程2的功能就是向数组输出B,线程3的功能就是向数组输出C,线程4的功能就是向数组输出D。要求按顺序向数组赋值ABCDABCDABCD,ABCD的个数由输入的参数指定。请编写一个类,类中封装公共字符串数组及输出逻辑,4个不同的线程将会共用一个该类的实例。
方法1:ReentrantLock + Condition
Condition强大的地方在于它能够精确的控制多线程的休眠与唤醒(注意是唤醒,唤醒只意味着进入了同步队列,不意味着一定能获得资源),相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式能够更加安全和高效地实现线程间协作。
public class AbcdLock {
private List<String> strList;
private int n;
private Lock lock = new ReentrantLock();
private Condition aCondition = lock.newCondition();
private Condition bCondition = lock.newCondition();
private Condition cCondition = lock.newCondition();
private Condition dCondition = lock.newCondition();
public AbcdLock(List<String> strList, int n) {
this.strList = strList;
this.n = n;
}
public void addA() {
for (int i = 0; i < n; ) {
lock.lock();
try {
if (strList.size() % 4 != 0) {
// 添加A的线程等待
aCondition.await();
}
strList.add("A");
i++;
// 指定唤醒添加B的线程
bCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public void addB() {
for (int i = 0; i < n; ) {
lock.lock();
try {
if (strList.size() % 4 != 1) {
// 添加B的线程等待
bCondition.await();
}
strList.add("B");
i++;
// 指定唤醒添加C的线程
cCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public void addC() {
for (int i = 0; i < n; ) {
lock.lock();
try {
if (strList.size() % 4 != 2) {
// 添加C的线程等待
cCondition.await();
}
strList.add("C");
i++;
// 指定唤醒添加D的线程
dCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public void addD() {
for (int i = 0; i < n; ) {
lock.lock();
try {
if (strList.size() % 4 != 3) {
// 添加A的线程等待
dCondition.await();
}
strList.add("D");
i++;
// 指定唤醒添加A的线程
aCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
List<String> strList = new ArrayList<>(128);
AbcdLock abcdLock = new AbcdLock(strList, 4);
new Thread(() -> {
abcdLock.addA();
}).start();
new Thread(() -> {
abcdLock.addB();
}).start();
new Thread(() -> {
abcdLock.addC();
}).start();
new Thread(() -> {
abcdLock.addD();
}).start();
}
方法2:synchronized关键字
只用一个Object锁,4个线程同时竞争一个锁,然后同时唤醒再竞争,再附加一个条件变量就行了。
将要输入的字符串以及字符串的丢失顺序,作为变量传入。只有当数组当前索引与本线程要输入字符串的位置匹配时,才做输入动作,否则线程等待。
public class AbcdSync extends Thread {
private List<String> strList;
private String element;
private int cycle;
// 用一个变量来规定要输入字符串的顺序,例如0、1、2、3
private int index;
public AbcdSync(List<String> strList, String element, int cycle, int index) {
this.strList = strList;
this.element = element;
this.cycle = cycle;
this.index = index;
}
@Override
public void run() {
try {
for (int i = 0;i < cycle;) {
synchronized (strList) {
// 这里的判断是关键
if (strList.size() % 4 == index) {
strList.add(element);
i++;
// 唤醒其他所有线程
strList.notifyAll();
} else {
// 当前线程阻塞
strList.wait();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
List<String> strList = new ArrayList<>(256);
new AbcdSync(strList, "A", 4, 0).start();
new AbcdSync(strList, "B", 4, 1).start();
new AbcdSync(strList, "C", 4, 2).start();
new AbcdSync(strList, "D", 4, 3).start();
}
}
六、模拟实现拍卖系统
问题描述:有3个人(线程)针对同一个商品展开竞价,商品起拍价格为100元,每个元都有自己的预算,出价不能超过预算,3个人轮流争抢式出价,后出的价格统一比前出的价格大100元,模拟实现这个过程。
分析:显然,谁的预算最大,谁将最终竞得商品,预算少的人依次退出竞价,最后仅剩一人
public class AuctionTest {
// 存储a、b、c三人的预算
private int aBudget;
private int bBudget;
private int cBudget;
// 实时出价价格
private int temp;
private Lock lock = new ReentrantLock();
private Condition aCondition = lock.newCondition();
private Condition bCondition = lock.newCondition();
private Condition cCondition = lock.newCondition();
public AuctionTest(int a, int b, int c, int temp) {
this.aBudget = a;
this.bBudget = b;
this.cBudget = c;
this.temp = temp;
}
public void aQuote() throws InterruptedException {
while (temp < aBudget) {
lock.lock();
try {
if (temp + 100 > aBudget) {
aCondition.await();
}
temp = temp + 100;
System.out.println("a出价:" + temp);
bCondition.signal();
cCondition.signal();
} finally {
System.out.println("a出价完成");
lock.unlock();
}
}
}
public void bQuote() throws InterruptedException {
while (temp < bBudget) {
lock.lock();
try {
if (temp + 100 > bBudget) {
bCondition.await();
}
temp = temp + 100;
System.out.println("b出价:" + temp);
cCondition.signal();
aCondition.signal();
} finally {
System.out.println("b出价完成");
lock.unlock();
}
}
}
public void cQuote() throws InterruptedException {
while (temp < cBudget) {
lock.lock();
try {
if (temp + 100 > cBudget) {
cCondition.await();
}
temp = temp + 100;
System.out.println("c出价:" + temp);
aCondition.signal();
bCondition.signal();
} finally {
System.out.println("c出价完成");
lock.unlock();
}
}
}
public static void main(String[] args) {
System.out.println("hello world");
AuctionTest auctionTest = new AuctionTest(2000, 2400, 2500, 1000);
new Thread(() -> {
try {
auctionTest.aQuote();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
auctionTest.bQuote();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
auctionTest.cQuote();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
模拟拍卖竞价的运行结果
hello world
a出价:1100
a出价完成
a出价:1200
a出价完成
a出价:1300
a出价完成
a出价:1400
a出价完成
a出价:1500
a出价完成
a出价:1600
a出价完成
a出价:1700
a出价完成
a出价:1800
a出价完成
a出价:1900
a出价完成
a出价:2000
a出价完成
b出价:2100
b出价完成
b出价:2200
b出价完成
b出价:2300
b出价完成
b出价:2400
b出价完成
c出价:2500
c出价完成
Process finished with exit code 0