java并发编程
为什么需要并发
并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开。这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同工作)。比如可以发挥多处理器的强大能力、建模更加简单、简化异步事件的处理、使用户界面的相应更加灵敏。做过Java Web开发的人都知道,Java Web中的Servlet程序在Servlet容器的支持下采用单实例多线程的工作模式,Servlet容器为你处理了并发问题。
package com.wise.tiger; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; public class SocketDemo { public static void main(String[] args) throws Exception{ var server = new ServerSocket(8888); for(int i = 0; i < 20; i++){ var client = server.accept();//等待客户端的连接 System.out.println("ip:" + client.getInetAddress()); new Thread(new Talk(client)).start(); } System.out.println("服务结束"); } static class Talk implements Runnable{ private Socket client; public Talk(Socket socket){ this.client = socket; } @Override public void run() { try { var reader = new BufferedReader(new InputStreamReader(client.getInputStream())); var writer = client.getOutputStream(); while(true){ String content = reader.readLine(); writer.write(content.getBytes()); writer.write("\r\n".getBytes()); if("0".equals(content)) break; } reader.close(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } } }
线程同步
线程同步用于协调相互依赖的线程的执行。如果一个共享资源被多个线程同时访问,可能会遭到破坏。假设创建并启动100个线程,每个线程都往同一个账户中添加一元钱。
public class Account { private int balance; public int getBalance() { return balance; } public void deposit(int amount){ //为了故意放大数据破坏的可能性,采用下列语句,其实可用 balance += amount;代替 int newBalance = balance + amount; try { Thread.sleep(10); } catch(InterruptedException e){} balance = newBalance; } }
public class AccountSync { private static Account account = new Account(); public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); for(int i = 0; i < 100; i++) { service.execute(() -> account.deposit(1)); } service.shutdown(); while (!service.isTerminated()); System.out.println(account.getBalance()); } }
显示锁
基于synchronized关键字的锁机制有以下问题:
- 锁只有一种类型,而且对所有同步操作都是一样的作用
- 锁只能在代码块或方法开始的地方获得,在结束的地方释放
- 线程要么得到锁,要么阻塞,没有其他的可能性
Java 5对锁机制进行了重构,提供了显示的锁,这样可以在以下几个方面提升锁机制:
- 可以添加不同类型的锁,例如读取锁和写入锁
- 可以在一个方法中加锁,在另一个方法中解锁
- 可以使用tryLock方式尝试获得锁,如果得不到锁可以等待、回退或者干点别的事情,当然也可以在超时之后放弃操作
显示的锁都实现了java.util.concurrent.Lock接口,主要有两个实现类:
- ReentrantLock - 比synchronized稍微灵活一些的重入锁
- ReentrantReadWriteLock - 在读操作很多写操作很少时性能更好的一种重入锁
注意:解锁的方法unlock的调用最好能够在finally块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。
生产者/消费者
假设使用缓冲区存储工作任务Task。缓冲区的大小是受限的,缓冲区提供write(Task task)方法将一个Task实例添加到缓冲区,还提供read()方法从缓冲区中读取和删除一个task。为了同步这个操作,使用具有两个条件的锁:notEmpty(缓冲区非空)和notFull(缓冲区未满)。当任务向缓冲区添加一个工作任务Task时,如果缓冲区是满的,那么任务将会等待notFull条件。当任务从缓冲区读取一个工作任务Task时,如果缓冲区是空的,那么任务将会等待notEmpty条件。
缓冲区实际是一个先进先出的队列。条件notEmpty和notFull和锁捆绑在一起。在应用一个条件之前必须获取一个锁。如果使用wait()和notify()来重写这个例子,必须指派两个对象作为监视器。
1 package com.wise.tiger; 2 import java.util.LinkedList; 3 import java.util.UUID; 4 import java.util.concurrent.Executors; 5 import java.util.concurrent.locks.Condition; 6 import java.util.concurrent.locks.Lock; 7 import java.util.concurrent.locks.ReentrantLock; 8 public class QueueTask { 9 private static Buffer buffer = new Buffer(); 10 public static void main(String[] args) { 11 var pool = Executors.newFixedThreadPool(2); 12 pool.execute(new Producer()); 13 pool.execute(new Consumer()); 14 pool.shutdown(); 15 } 16 /** 17 * 工作任务 18 */ 19 private static class Task{ 20 private String id; 21 public Task(){ 22 this.id = UUID.randomUUID().toString(); 23 } 24 @Override 25 public String toString(){ 26 return "Task["+id+"]"; 27 } 28 } 29 30 /** 31 * 生产者: 32 * 33 */ 34 private static class Producer implements Runnable { 35 @Override 36 public void run() { 37 try { 38 while (true) { 39 Thread.sleep((int) (Math.random() * 10000)); 40 var task = new Task(); 41 buffer.write(task); 42 System.out.println("Producer[" + Thread.currentThread().getName() + "] put " + task); 43 } 44 } catch(InterruptedException e){ 45 e.printStackTrace(); 46 } 47 } 48 49 } 50 /** 51 * 消费者 52 */ 53 static class Consumer implements Runnable { 54 @Override 55 public void run() { 56 try { 57 while (true) { 58 Thread.sleep((int) (Math.random() * 10000)); 59 System.out.println("Consumer[" + Thread.currentThread().getName() + "] got " + buffer.read()); 60 } 61 } catch(InterruptedException e){ 62 e.printStackTrace(); 63 } 64 } 65 } 66 67 68 /** 69 * 缓冲区 70 */ 71 private static class Buffer{ 72 private static final int CAPACITY = 20; 73 private LinkedList<Task> queue = new LinkedList<>(); 74 75 private static Lock lock = new ReentrantLock(); 76 77 private static Condition notEmpty = lock.newCondition(); 78 private static Condition notFull = lock.newCondition(); 79 80 /** 81 * 往队列中添加数据【Task】 82 * @param value Task 83 */ 84 public void write(Task value){ 85 lock.lock(); 86 try { 87 while(queue.size() >= CAPACITY){ 88 System.out.println("wait for notFull condition"); 89 notFull.await(); 90 } 91 queue.offer(value); 92 notEmpty.signal();//signal notEmpty condition 93 } catch (InterruptedException e) { 94 e.printStackTrace(); 95 } finally { 96 lock.unlock(); 97 } 98 } 99 public Task read(){ 100 Task value = null; 101 lock.lock(); 102 try { 103 while(queue.isEmpty()){ 104 System.out.println("wait for notEmpty condition"); 105 notEmpty.await(); 106 } 107 value = queue.remove(); 108 notFull.signal();//signal notFull condition 109 } catch (InterruptedException e) { 110 e.printStackTrace(); 111 } finally { 112 lock.unlock(); 113 return value; 114 } 115 } 116 } 117 } 119
阻塞队列
基于BlockingQueue的实现
package edu.uestc.avatar; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * java集合框架提供了ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue来支持阻塞队列 * 阻塞队列:试图向满队列添加元素或者从空队列删除元素会导致线程阻塞 */ public class ProducerConsumerUsingBlockingQueue { private static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(5); public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Producer()); executor.execute(new Consumer()); executor.shutdown(); } private static class Producer implements Runnable{ @Override public void run() { try { int value = 1; while(true) { System.out.println("生产者生产数据:" + value); queue.put(value++); Thread.sleep((long)(Math.random() * 5000)); } } catch (InterruptedException e) { e.printStackTrace(); } } } private static class Consumer implements Runnable{ @Override public void run() { try { while(true) { System.out.println("消费者消费数据:" + queue.take()); Thread.sleep((long)(Math.random() * 5000)); } } catch (InterruptedException e) { e.printStackTrace(); } } } }
CountDownLatch
CountDownLatch是一种简单的同步模式,它让一个线程可以等待一个或多个线程完成它们的工作从而避免对临界资源并发访问所引发的各种问题。下面借用别人的一段代码(我对它做了一些重构)来演示CountDownLatch是如何工作的。
1 package com.wise.tiger; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.CyclicBarrier; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 public class CountdownLatchTest { 8 public static void main(String[] args) { 9 ExecutorService service = Executors.newCachedThreadPool(); 10 final CountDownLatch cdOrder = new CountDownLatch(1); 11 final CountDownLatch cdAnswer = new CountDownLatch(3); 12 for(int i = 0;i < 3; i++){ 13 Runnable runnable = new Runnable(){ 14 public void run(){ 15 try { 16 System.out.println("线程" + Thread.currentThread().getName() + 17 "正准备接受命令"); 18 cdOrder.await(); 19 System.out.println("线程" + Thread.currentThread().getName() + 20 "已接受命令"); 21 Thread.sleep((long)(Math.random()*10000)); 22 System.out.println("线程" + Thread.currentThread().getName() + 23 "回应命令处理结果"); 24 cdAnswer.countDown(); 25 } catch (Exception e) { 26 e.printStackTrace(); 27 } 28 } 29 }; 30 service.execute(runnable); 31 } 32 try { 33 Thread.sleep((long)(Math.random()*10000)); 34 35 System.out.println("线程" + Thread.currentThread().getName() + 36 "即将发布命令"); 37 cdOrder.countDown(); 38 System.out.println("线程" + Thread.currentThread().getName() + 39 "已发送命令,正在等待结果"); 40 cdAnswer.await(); 41 System.out.println("线程" + Thread.currentThread().getName() + 42 "已收到所有响应结果"); 43 } catch (Exception e) { 44 e.printStackTrace(); 45 } 46 service.shutdown(); 47 } 48 }
1 package com.wise.tiger; 2 3 import java.util.concurrent.CountDownLatch; 4 5 /** 6 * 工人类 7 * 8 */ 9 class Worker { 10 private String name; // 名字 11 private long workDuration; // 工作持续时间 12 13 /** 14 * 构造器 15 */ 16 public Worker(String name, long workDuration) { 17 this.name = name; 18 this.workDuration = workDuration; 19 } 20 21 /** 22 * 完成工作 23 */ 24 public void doWork() { 25 System.out.println(name + " begins to work..."); 26 try { 27 Thread.sleep(workDuration); // 用休眠模拟工作执行的时间 28 } catch(InterruptedException ex) { 29 ex.printStackTrace(); 30 } 31 System.out.println(name + " has finished the job..."); 32 } 33 } 34 35 /** 36 * 测试线程 37 * 38 */ 39 class WorkerTestThread implements Runnable { 40 private Worker worker; 41 private CountDownLatch cdLatch; 42 43 public WorkerTestThread(Worker worker, CountDownLatch cdLatch) { 44 this.worker = worker; 45 this.cdLatch = cdLatch; 46 } 47 48 @Override 49 public void run() { 50 worker.doWork(); // 让工人开始工作 51 cdLatch.countDown(); // 工作完成后倒计时次数减1 52 } 53 } 54 55 class CountDownLatchTest { 56 57 private static final int MAX_WORK_DURATION = 5000; // 最大工作时间 58 private static final int MIN_WORK_DURATION = 1000; // 最小工作时间 59 60 // 产生随机的工作时间 61 private static long getRandomWorkDuration(long min, long max) { 62 return (long) (Math.random() * (max - min) + min); 63 } 64 65 public static void main(String[] args) { 66 CountDownLatch latch = new CountDownLatch(2); // 创建倒计时闩并指定倒计时次数为2 67 Worker w1 = new Worker("Peppa", getRandomWorkDuration(MIN_WORK_DURATION, MAX_WORK_DURATION)); 68 Worker w2 = new Worker("Emily", getRandomWorkDuration(MIN_WORK_DURATION, MAX_WORK_DURATION)); 69 70 new Thread(new WorkerTestThread(w1, latch)).start(); 71 new Thread(new WorkerTestThread(w2, latch)).start(); 72 73 try { 74 latch.await(); // 等待倒计时闩减到0 75 System.out.println("All jobs have been finished!"); 76 } catch (InterruptedException e) { 77 e.printStackTrace(); 78 } 79 } 80 }
信号量Semaphore
可以使用信号量来限制访问一个共享资源的线程数。在访问资源之前,线程必须从信号量获取许可。在访问完资源之后,这个线程必须将许可返回给信号量。
package com.wise.tiger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; public class SemaphoreTest { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); final Semaphore sp = new Semaphore(3); for(int i=0;i<10;i++){ Runnable runnable = new Runnable(){ public void run(){ try { sp.acquire(); } catch (InterruptedException e1) { e1.printStackTrace(); } System.out.println("线程" + Thread.currentThread().getName() + "进入,当前已有" + (3-sp.availablePermits()) + "个并发"); try { Thread.sleep((long)(Math.random()*10000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程" + Thread.currentThread().getName() + "即将离开"); sp.release(); //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元 System.out.println("线程" + Thread.currentThread().getName() + "已离开,当前已有" + (3-sp.availablePermits()) + "个并发"); } }; service.execute(runnable); } } }
场景介绍:有一个停车场只有5个车位,现在有100辆车要去抢这个5个车位,理想情况下最多只有五辆车同时可以抢到车位,那么没有抢到车位的车只能等到,其他的车让出车位,才有机会去使用该车位。
public class CarPark { public static void main(String[] args) { //阻塞队列 BlockingQueue<String> parks = new LinkedBlockingQueue<>(5); parks.offer("车位一"); parks.offer("车位二"); parks.offer("车位三"); parks.offer("车位四"); parks.offer("车位五"); ExecutorService executorService = Executors.newCachedThreadPool(); //如博文中所说的初始值为5, 专业的说法就是5个许可证 Semaphore semaphore = new Semaphore(5); for (int i = 0; i < 100; i++) { final int no = i; Thread t1 = new Thread(() -> { try { /** * 获取许可,首先判断semaphore内部的数字是否大于0,如果大于0, * 才能获得许可,然后将初始值5减去1,线程才会接着去执行;如果没有 * 获得许可(原因是因为已经有5个线程获得到许可,semaphore内部的数字为0), * 线程会阻塞直到已经获得到许可的线程,调用release()方法,释放掉许可, * 也就是将semaphore内部的数字加1,该线程才有可能获得许可。 */ semaphore.acquire(); /** * 对应的线程会到阻塞对,对应车辆去获取到车位,如果没有拿到一致阻塞, * 直到其他车辆归还车位。 */ String park = parks.take(); System.out.println("车辆【" + no + "】获取到: " + park); Thread.sleep((long) Math.random() * 6000); semaphore.release(); //线程释放掉许可,通俗来将就是将semaphore内部的数字加1 parks.offer(park); //归还车位 System.out.println("车辆【" + no + "】离开 " + park); } catch (InterruptedException e) { e.printStackTrace(); } }); executorService.execute(t1); } } }
1965年,荷兰计算机科学家图灵奖得主Edsger Wybe Dijkstra提出并解决了一个他称之为哲学家进餐的同步问题。这个问题可以简单地描述如下:五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘通心粉。由于通心粉很滑,所以需要两把叉子才能夹住。相邻两个盘子之间放有一把叉子如下图所示。哲学家的生活中有两种交替活动时段:即吃饭和思考。当一个哲学家觉得饿了时,他就试图分两次去取其左边和右边的叉子,每次拿一把,但不分次序。如果成功地得到了两把叉子,就开始吃饭,吃完后放下叉子继续思考。
把上面问题中的哲学家换成线程,把叉子换成竞争的临界资源,上面的问题就是线程竞争资源的问题。如果没有经过精心的设计,系统就会出现死锁、活锁、吞吐量下降等问题。

下面是用信号量原语来解决哲学家进餐问题的代码,使用了Java 5并发工具包中的Semaphore类(代码不够漂亮但是已经足以说明问题了)。
package com.wise.tiger; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * 存放线程共享信号量上下文 */ public class SemaphoreContext { public static final int NUM_OF_FORKS = 5;//叉子数量(资源) public static final int NUM_OF_PHILO = 5;//哲学家数量(线程) public static Semaphore[] forks; public static Semaphore counter; static{ forks = new Semaphore[NUM_OF_FORKS]; for(int i = 0 ; i < forks.length; i++){ forks[i] = new Semaphore(1); } counter = new Semaphore(NUM_OF_PHILO - 1); } public static void putFork(int index,boolean leftFirst) throws Exception{ if(leftFirst) { forks[index].acquire(); forks[(index + 1) % NUM_OF_PHILO].acquire(); } else { forks[(index + 1) % NUM_OF_PHILO].acquire(); forks[index].acquire(); } } public static void takeFork(int index,boolean leftFirst) throws Exception{ if(leftFirst) { forks[index].release(); forks[(index + 1) % NUM_OF_PHILO].release(); } else { forks[(index + 1) % NUM_OF_PHILO].release(); forks[index].release(); } } static class Philo implements Runnable{ private int index; private String name; public Philo(int index, String name) { this.index = index; this.name = name; } @Override public void run() { while (true) { try { counter.acquire(); boolean isLeftFirst = index % 2 == 0; putFork(index, isLeftFirst); System.out.println(name + "正在吃通心粉。。。。。"); takeFork(index, isLeftFirst); System.out.println(name + "吃完了,正在思考"); counter.release(); } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) { Philo[] philos = {new Philo(0,"peppa"), new Philo(1,"pedro"), new Philo(2,"emily"), new Philo(3,"suzy"), new Philo(4,"danny"),}; var pool = Executors.newCachedThreadPool(); for(int i= 0; i < philos.length;i++){ pool.execute(philos[i]); } pool.shutdown(); } }
Exchanger
Exchanger是自jdk1.5起开始提供的工具套件,一般用于两个工作线程之间交换数据。在本文中我将采取由浅入深的方式来介绍分析这个工具类。首先我们来看看官方的api文档中的叙述:
在以上的描述中,有几个要点:
- 此类提供对外的操作是同步的;
- 用于成对出现的线程之间交换数据;
- 可以视作双向的同步队列;
- 可应用于基因算法、流水线设计等场景。
接着看api文档,这个类提供对外的接口非常简洁,一个无参构造函数,两个重载的范型exchange方法:
public V exchange(V x) throws InterruptedException
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
从官方的javadoc可以知道,当一个线程到达exchange调用点时,如果它的伙伴线程此前已经调用了此方法,那么它的伙伴会被调度唤醒并与之进行对象交换,然后各自返回。如果它的伙伴还没到达交换点,那么当前线程将会被挂起,直至伙伴线程到达——完成交换正常返回;或者当前线程被中断——抛出中断异常;又或者是等候超时——抛出超时异常。
package com.wise.tiger; import java.util.concurrent.Exchanger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExchangerTest { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); final Exchanger exchanger = new Exchanger(); service.execute(new Runnable(){ public void run() { try { String data1 = "白粉"; System.out.println("线程" + Thread.currentThread().getName() + "正在把数据" + data1 +"换出去"); Thread.sleep((long)(Math.random()*10000)); String data2 = (String)exchanger.exchange(data1); System.out.println("线程" + Thread.currentThread().getName() + "换回的数据为" + data2); }catch(Exception e){ } } }); service.execute(new Runnable(){ public void run() { try { String data1 = "美金"; System.out.println("线程" + Thread.currentThread().getName() + "正在把数据" + data1 +"换出去"); Thread.sleep((long)(Math.random()*10000)); String data2 = (String)exchanger.exchange(data1); System.out.println("线程" + Thread.currentThread().getName() + "换回的数据为" + data2); }catch(Exception e){ } } }); } }
package com.wise.tiger; import java.util.concurrent.Exchanger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.*; public class ExchangerDemo { public static void main(String[] args) { var executor = Executors.newCachedThreadPool(); final Exchanger exchanger = new Exchanger(); executor.execute(new Runnable() { String data1 = "克拉克森,小拉里南斯"; @Override public void run() { nbaTrade(data1, exchanger); } }); executor.execute(new Runnable() { String data1 = "格里芬"; @Override public void run() { nbaTrade(data1, exchanger); } }); executor.execute(new Runnable() { String data1 = "史蒂芬.裤裆里"; @Override public void run() { nbaTrade(data1, exchanger); } }); executor.shutdown(); } private static void nbaTrade(String data1, Exchanger exchanger) { try { System.out.println(Thread.currentThread().getName() + "在交易截止之前把 " + data1 + " 交易出去"); Thread.sleep((long) (Math.random() * 1000)); String data2 = (String) exchanger.exchange(data1); System.out.println(Thread.currentThread().getName() + "交易得到" + data2); } catch (InterruptedException e) { e.printStackTrace(); } } }

浙公网安备 33010602011771号