一、使用两个线程交替打印输出“1A2B3C.....26Z”
具体描述:使用两个线程,一个输出字母,一个输出数字,交替输出1A2B3C......26Z
1、LockSupport
使用JUC下的LockSupport工具类我们可以精确的阻塞或唤醒一个线程,并且LockSupport不需要配合Object Monitor使用,非常方便。这里我们主需要开启两个线程,然后各自在打印完之后唤醒对方并阻塞自己,交替进行即可。这里给出参考实现。
public class LockSupport_park_unpark {
private static Thread numThread=null, letterThread=null;
public static void main(String[] args) {
numThread = new Thread(() -> {
for (int num = 0; num++ < 26; ) {
System.out.print(num);
//唤醒letterThred,打印字母
LockSupport.unpark(letterThread);
LockSupport.park();
}
}, "numThread");
letterThread = new Thread(() -> {
for (char letter = 'A'; letter <='Z'; letter++) {
//即使letterThread先运行,它到这里也会被阻塞住,只有numThread首次打印之后唤醒它,它才可以继续执行
LockSupport.park();
System.out.print(letter);
//唤醒numThread
LockSupport.unpark(numThread);
}
}, "letterThread");
numThread.start();
letterThread.start();
}
}
2、CAS
还可以使用自旋锁的思想,设置一个标记,如果没达到预期就不修改,话不多说,直接看代码:
public class CAS_AtomicInteger {
private static AtomicInteger cas = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i++ < 26; ) {
while (cas.get() != 1) {}
System.out.print(i);
cas.compareAndSet(1,2);
}
}, "numThread").start();
new Thread(() -> {
for (char i = 'A'; i <= 'Z'; i++) {
while (cas.get() != 2) {}
System.out.print(i);
cas.compareAndSet(2,1);
}
}, "letterThread").start();
}
}
3、synchronized+wait+notify
使用wait/notify可能是本题人家面试官的考点,人家就是想看你会不会使用wait/notify这种等待通知机制实现线程之间的通信,因此这种方式必须掌握。下面给出参考实现。
public class Synchronized_wait_notify {
private final Object mutex = new Object();
private int currentNum = 0;
private char currentLetter = 'A';
//可用可不用,使用的目的是要求严格按照1A2B3C的顺序打印
//如果不使用,可能会造成A1B2C3的顺序
private CountDownLatch cdl = new CountDownLatch(1);
public static void main(String[] args) {
new Synchronized_wait_notify().print();
}
/**
* 两个线程交替打印:1A2B3C...
*/
public void print() {
new Thread(() -> {
try {
cdl.await(); //如果打印字母的线程先运行了,让他先等待
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mutex) {
for (; currentLetter <= 'Z'; currentLetter++) {
System.out.print(currentLetter);
try {
mutex.notifyAll();
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mutex.notifyAll();
}
}, "letterThread").start();
new Thread(() -> {
synchronized (mutex) {
for (; currentNum++ < 26; ) {
System.out.print(currentNum);
cdl.countDown();
try {
mutex.notifyAll();
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mutex.notifyAll();
}
}, "numThread").start();
}
}
4、ReentrantLock+Condition
在JUC包有一个Condition可以实现和wait/notify一样的功能,并且不需要配合Object Monitor使用,但是需要同步,这里直接使用ReentrantLock就好了。下面给出参考实现
public class Condition_await_signal {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private CountDownLatch cdl = new CountDownLatch(1);
private int currentNum = 0;
private char currentLetter = 'A';
public static void main(String[] args) {
new Condition_await_signal().print();
}
/**
* 两个线程交替打印:1A2B3C...
*/
public void print() {
new Thread(() -> {
try {
lock.lock();
for (; currentNum++ < 26; ) {
System.out.print(currentNum);
cdl.countDown();
condition.signalAll();
condition.await();
}
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "numThread").start();
new Thread(() -> {
try {
cdl.await(); //如果打印字母的线程先运行了,让他先等待
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();
for (; currentLetter <= 'Z'; currentLetter++) {
System.out.print(currentLetter);
condition.signalAll();
condition.await();
}
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "letterThread").start();
}
}
二、两个线程,一个线程打印奇数,一个线程打印偶数,控制先一个奇数后一个偶数这种顺序
1、synchronized+wait+notify
基本思路和上面打印1A2B3C的思路一样,两个线程使用wait/notify控制唤醒和阻塞。然后使用CountDownLatch严格控制打印奇数的线程先运行。
public class Synchronized_wait_notify {
//当前值,使用volatile保证共享变量在多个线程之间的内存可见性
private volatile int currentNum = 0;
//严格控制线程t1先运行,t2后运行
private CountDownLatch cdl = new CountDownLatch(1);
private final Object lock = new Object();
public static void main(String[] args) {
new Synchronized_wait_notify().print();
}
public void print() {
//t1 打印奇数
new Thread(() -> {
synchronized (lock) {
while (currentNum++ < 100) {
System.out.print(currentNum + " ");
cdl.countDown();
lock.notifyAll();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notifyAll();
}
}, "t1").start();
//t2 打印偶数
new Thread(() -> {
synchronized (lock) {
try {
//保证线程t2永远在t1之后打印
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (currentNum++ < 100) {
System.out.print(currentNum + " ");
lock.notifyAll();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notifyAll();
}
}, "t2").start();
}
}
三、重显死锁
public class DeadLock{
//两个资源
private final Object resource1=new Object();
private final Object resource2=new Object();
public static void main(String[] args) {
new DeadLock().displayDeadLock();
}
//两个线程都需要请求两个资源才可以工作,下面这种情况就会产生死锁
public void displayDeadLock(){
new Thread(()->{
synchronized(resources1){
System.out.println(Thread.currentThread().getName() + "获得了resources1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "等待获取resources2");
synchronized(resoures2){
System.out.println(Thread.currentThread().getName() + "获得了resources2");
}
}
},"t1").start();
new Thread(()->{
synchronized(resources2){
System.out.println(Thread.currentThread().getName() + "获得了resources2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "等待获取resources1");
synchronized(resoures1){
System.out.println(Thread.currentThread().getName() + "获得了resources1");
}
}
},"t2").start();
}
}
四、生产者消费者模式
在线程的世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,消费者处理速度很慢,那么生产者就必须等待消费者处理完才能继续生产;同样的道理,如果消费者的处理能力大图生产者,那么消费者就必须等待生产者。因此为了解决这种生产消费能力不平衡的问题,就有了生产者和消费者模式。
生产者和消费者通过一个容器来解决生产者和消费者和强耦合问题。生产者和消费者彼此之间不直接统统,而是通过阻塞队列进行通信,生产者产生数据后直接扔进崔阻塞队列,消费者就到阻塞队列中获取数据消费就好了,这样一来,阻塞队列就相当于一个缓冲区,平衡了生产消费能力的不平衡。
首先我们实现生产者和消费者,接着我会给出几种不同的Stock容器实现方式。
生产者Productor:
生产者持有商品容器,并实现了Runnable接口,在run方法中无限循环地往商品容器stock中放入商品。
public class Producer implements Runnable {
//货物,生产者就生产货物
private final Stock stock;
//统计生产的数量
private volatile AtomicInteger tootleNum = new AtomicInteger(0);
public Producer(Stock stock, int targetNum) {
this.stock = stock;
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName() + "开始生产!");
try {
while (!currentThread.isInterrupted()) {
Thread.sleep(new Random().nextInt(3000));
//UUID随机字串表示生产的货物
String good = UUID.randomUUID().toString();
//将货物丢进阻塞队列
stock.put(good);
int tootle = tootleNum.getAndIncrement();
System.out.println(currentThread.getName() + "生产数据:" + good + ",累计生产" + tootle);
}
} catch (InterruptedException e) {
e.printStackTrace();
//恢复中断标记位
currentThread.interrupt();
}
}
}
消费者Customer:
消费者持有商品容器,并实现了Runnable接口,无限循环地从商品容器stock中取出商品消费。
public class Customer implements Runnable {
//货物,消费者消费货物
private final Stock stock;
public Customer(Stock stock) {
this.stock = stock;
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName() + "开始消费!");
try {
while (!currentThread.isInterrupted()) {
String good = stock.take();
Thread.sleep(new Random().nextInt(5000));
System.out.println(currentThread.getName() + "消费数据:"+good);
}
} catch (InterruptedException e) {
e.printStackTrace();
//在sleep的时候调用interrupt()会产生异常之后会破坏中断标记位,
// 这里需要恢复中断标记位
currentThread.interrupt();
}
}
}
商品容器Stock接口:
public interface Stock {
int MAX_SIZE=100;
//获取商品
String take();
//放入商品
void put(String stack);
}
1、BlockingQueue实现
直接使用阻塞队列实现,生产者只管生产,生产好了直接丢到阻塞队列中,消费者从阻塞队列中获取数据,获取到了就消费,没有数据了就阻塞住等待生产者生产。下面是简单实现。
商品的管理队列StockBlockQueue:使用ArrayBlockingQueue同步商品信息
public class StockBlockQueue implements Stock {
//阻塞队列缓冲区
private BlockingQueue<String> queue=new ArrayBlockingQueue<>(MAX_SIZE);
@Override
public String take() {
String stock=null;
try {
//从阻塞队列中阻塞的获取数据
stock=queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return stock;
}
@Override
public void put(String stock) {
try {
queue.put(stock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
技术要点:
ArrayBlockingQueue主要有如下方法: add、offer、put都是放入元素。 remove、poll、take都是移除元素。 element、peek是获取头元素,但不移除。
它们的实现不同:
- 抛出异常:add() remove() element()
- 返回一个特殊值(null或false,具体取决于操作): offer(e) poll() peek()
- 操作成功前,无限期地阻塞:put(e) take()
- 阻塞给定的时间:offer(e,time,unit) poll(time,unit)
因此在使用的是否一定要注意使用他的阻塞方法,使用其他方法没有效果。
2、synchronized+wait/notify实现
该实现主要由synchronized、wait、notify配合使用。
synchronized的语义大家应该都知道,当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。即同一时间内要么只有消费者执行take()方法,要么只有生产者执行put()方法。
只有synchronized保证只有一个线程执行方法还不够,我们需要在容器空的时候,需要调用wait()让出锁进行等待,将执行权交给生产者生产商品,生产者生产完商品后再调用notify()方法通知消费者线程消费商品(有可能唤醒的还是生产者,如果唤醒的是还是生产者就继续生产商品直到容器满,让出锁进行等待。)。反之亦然。
public class SynchronousStock implements Stock {
//普通队列,用来保存货物
private Queue<String> products = new LinkedList<>();
/**
* 使用synchronized同步
*
* @return
*/
@Override
public synchronized String take() {
String good = null;
while (products.isEmpty()) {
try {
//如果空了,消费者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
good = products.poll();
notifyAll();
return good;
}
@Override
public synchronized void put(String stock) {
while (products.size() >= MAX_SIZE) {
//商品容器满了,生产者停止生产,等待消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products.add(stock);
notifyAll();
}
}
技术要点:
- 在放入商品或取出商品时进行while条件判断,条件满足的话,进行等待。
- 取出商品或者放入商品时通知其他线程。
3、RenentrantLock+Condition实现
该实现主要由ReentrantLock、以及customer、producer两个Condition来一起实现。Condition一样也是用来阻塞等待线程。那为什么需要两个Condition呢?可以看看刚才的例子,使用notifyAll()的时候可能会唤醒生产者和消费者。而两个Condition的话,我们可以在精准的控制唤醒,在消费者中唤醒生产者,在生产者中唤醒消费者。
public class ReentrantLockStock implements Stock {
//普通队列,用来保存货物
private Queue<String> products = new LinkedList<>();
private Lock lock = new ReentrantLock();
//控制消费者的停止与运行
private Condition customer = lock.newCondition();
//控制生产者的停止与运行
private Condition producer = lock.newCondition();
@Override
public String take() {
String good = null;
try {
lock.lock();
while (products.isEmpty()) {
//队列中没有货物,消费者休息
customer.await();
}
//从队列中获取一个数据
good = products.poll();
//唤醒生产者
producer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return good;
}
@Override
public void put(String stock) {
try {
lock.lock();
while (products.size() == MAX_SIZE) {
try {
//生产者休息
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products.offer(stock);
customer.signalAll();
} finally {
lock.unlock();
}
}
}
技术要点:
1、将lock.lock()放到try块中。许多人会将lock放在try catch块外面,这样很容易出现死锁。因为lock锁和synchronized锁不一样。synchronized锁会自动释放锁。而lock不会自动释放锁,必须手工释放锁。如果lock放在try catch块之外的话,持有锁后却发生了异常,此时并不会释放锁。其他线程就永远得不到这个锁了。
2、使用两个Condition实现精准的控制唤醒生产者和消费者。