Java并发编程实践

一、使用两个线程交替打印输出“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();
    }
}

技术要点

  1. 在放入商品或取出商品时进行while条件判断,条件满足的话,进行等待。
  2. 取出商品或者放入商品时通知其他线程。
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实现精准的控制唤醒生产者和消费者

参考资料

留言区

还能输入500个字符