java学习——多线程:并发框架以及数据结构


前言

首先,为啥要使用框架来进行并发编程呢?

1、分离任务的创建和执行者的创建
2、线程的重复利用(new线程代价很大)


一、Executor并发编程框架

从java JDK5之后就开始提供ExecutorFrameWork并发编程框架,在java.util.concurrent.*中。

这个框架提供共享线程池,只要把任务创建好,扔进线程池中,程序员不需要关心线程池怎么执行的,直接拿到执行结果就好了。

主要类:

  • Executors.newCachedThreadPool/newFixedThreadPool(创建线程池)
  • ExecutorService (线程池服务)
  • Callable具体的逻辑对象(线程类)
  • Future 返回结果

直接用例子来看怎么使用吧。

运行类:

package executor;
import java.util.concurrent.*;
import java.util.ArrayList;
import java.util.List;

public class SumTest {

	public static void main(String[] args) {
		//线程池
		ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);
		//返回结果
		List<Future<Integer>> resultList = new ArrayList<>();
		
		for(int i=0; i<10; i++) {
			SumTask calcultor = new SumTask(i*100+1, (i+1)*100);
			Future<Integer> result = executor.submit(calcultor);
			resultList.add(result);
		}

		//轮询查看任务结果
		do {
			System.out.printf("Main:已经完成多少任务:%d\n", executor.getCompletedTaskCount());
			for (int i=0; i<resultList.size(); i++) {
				Future<Integer> result = resultList.get(i);
				System.out.printf("Main: Task %d: %s\n",i,result.isDone());
			}
		}while(executor.getCompletedTaskCount()<resultList.size());
		
		//综合计算结果
		int total = 0;
		for( int i=0; i<resultList.size(); i++) {
			Future<Integer> result = resultList.get(i);
			Integer sum = null;
			try {
				sum = result.get();
				total = total + sum;
			}catch (InterruptedException e) {
				e.printStackTrace();
			} catch (ExecutionException e) {
				e.printStackTrace();
			}
		}
		System.out.printf("1-1000的总和:" + total);
		// 关闭线程池
		executor.shutdown();
	}
}

任务类:

package executor;
import java.util.Random;
import java.util.concurrent.Callable;

public class SumTask implements Callable<Integer>{
	//定义线程区间
	private int startNumber;
	private int endNumber;
	
	public SumTask(int startNumber, int endNumber) {
		this.startNumber = startNumber;
		this.endNumber = endNumber;
	}

	public Integer call() throws Exception {
		int sum = 0;
		for(int i= startNumber; i<=endNumber; i++) {
			sum = sum+  i;
		}
		Thread.sleep(new Random().nextInt(1000));
		
		System.out.printf("%s:%d\n", Thread.currentThread().getName(),sum);
		return sum;
	}
}

在自己跟着教程敲这段代码的时候呢,发现要注意这几点:

  • Callable 注意是这个接口,不再是runnable接口了,实现的方法也是call()方法,但是除了名字不一样,里面的写法什么的与之前的完全一致。
  • 初始化线程池可以预先定义大小,一般为CPU核数的2倍或者4倍,如果没有定义大小,线程池会一直增加线程数。
  • 不需要再对任务包装为thread类了,不需要调用start方法了,直接submit就可以了,线程池自动执行。
  • 线程的执行结果放在了Future类中,如果存在多个结果,通过Future<Integer> result = resultList.get(i);这种get可以获取到该结果,再通过result.isDone()判断该线程是否已经执行完。

线程池这玩意现在用的很多,但说实话自己还不是非常清楚咋用,先种个草。

二、Fork-join并发编程框架

java7提出来的框架,用于分治编程(分解、治理、合并),适用于整体任务量不好确定的场合(最小任务可确定)

主要类:

  • ForkJoinPool 任务池
  • RecursiveAction
  • RecursiveTask 可递归的方法,必须事先compute方法。

贴代码看看好理解


import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;

//分任务求和
public class SumTest {
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建执行线程池
    	ForkJoinPool pool = new ForkJoinPool();
    	//ForkJoinPool pool = new ForkJoinPool(4);
    	
    	//创建任务
        SumTask task = new SumTask(1, 10000000);
        
        //提交任务
        ForkJoinTask<Long> result = pool.submit(task);
        
        //等待结果
        do {
			System.out.printf("Main: Thread Count: %d\n",pool.getActiveThreadCount());
			System.out.printf("Main: Paralelism: %d\n",pool.getParallelism());
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} while (!task.isDone());
        
        //输出结果
        System.out.println(result.get().toString());
    }
}

任务类,写的有点递归分解的意思。

import java.math.BigInteger;
import java.util.concurrent.RecursiveTask;

//分任务求和
public class SumTask extends RecursiveTask<Long> {
	
	private int start;
	private int end;

	public SumTask(int start, int end) {
		this.start = start;
		this.end = end;
	}

	public static final int threadhold = 5;

	@Override
	protected Long compute() {
		Long sum = 0L;
		
		// 如果任务足够小, 就直接执行
		boolean canCompute = (end - start) <= threadhold;
		if (canCompute) {
			for (int i = start; i <= end; i++) {
				sum = sum + i;				
			}
		} else {
			// 任务大于阈值, 分裂为2个任务
			int middle = (start + end) / 2;
			SumTask subTask1 = new SumTask(start, middle);
			SumTask subTask2 = new SumTask(middle + 1, end);

			invokeAll(subTask1, subTask2);

			Long sum1 = subTask1.join();
			Long sum2 = subTask2.join();

			// 结果合并
			sum = sum1 + sum2;
		}
		return sum;
	}
}

需要注意的几点

  • 实现的接口又变了,实现RecursiveTask接口,并且需要重写compute方法。
  • invoke方法,提交线程。.join()方法,等待调用这个方法的线程完成才能够取结果。
  • 注意这种递归分解的思路。

三、java并发数据结构

常用的数据结构是线程不安全的。ArrayList,HashMap,HashSet是非同步的,多个线程对其进行读或者写的时候,就会出现数据错乱。

java5之后提供了一些并发的数据结构。
可以分为:

  • 阻塞式:集合为空或者满时,等待。
  • 非阻塞式:集合为空或者满时,不等待,返回null或者异常。

直接看代码吧:
List

//线程不安全
List<String> unsafeList = new ArrayList<String>();
//线程安全:基于synchronized,效率差
List<String> safeList1 = Collections.synchronizedList(new ArrayList<String>());
//线程安全:基于复制机制,适用于读多写少,非阻塞
CopyOnWriteArrayList<String> safeList2 = new CopyOnWriteArrayList<String>();

map

//线程不安全
Map<Integer,String> unsafeMap = new HashMap<Integer,String>();
//线程安全:效率差
Map<Integer,String> safeMap1 = Collections.synchronizedMap(new HashMap<Integer,String>());
//线程安全:读多写少,非阻塞
ConcurrentHashMap<Integer,String> safeMap2 = new ConcurrentHashMap<Integer,String>();

queue

//线程不安全
Deque<String> unsafeQueue = new ArrayDeque<String>();
//线程安全:非阻塞
ConcurrentLinkedDeque<String> safeQueue1 = new ConcurrentLinkedDeque<String>();

set

//线程不安全
Set<String> unsafeSet = new HashSet<String>();
//线程安全:基于synchronized,效率差
Set<String> safeSet1 = Collections.synchronizedSet(new HashSet<String>());
//线程安全:读多写少,非阻塞
CopyOnWriteArraySet<String> safeSet2 = new CopyOnWriteArraySet<String>();

四、java并发协作控制

现总结一下之前写了啥?

  • Thread/Executor/Fork-Join:这三个东西呢,都可以实现多线程。但是呢,相当于创建任务一次执行到底,中间没啥交流,线程之间缺少协作。但实际编程中呢,比如说访问同一个文件,有人写有人读,各个线程需要约定好一些资源占用的规则,只采用上面的玩意就显然达不到这种功能了。
  • 不对呀,synchronized不是能够控制线程只能访问一个关键区吗?volatile不是能够只让一个线程对资源进行修改吗?还有sleep/wait/join啥的不是能够对细粒度的控制吗?确实,但是synchronize太过于简单粗暴,性能损失大,不能够完全实现用户需求哈。本节只针对synchronized这个讲讲有啥改进的。

4.1 Lock

功能:能够实现更复杂的临界区结构,功能类似于synchronized但是实现的功能更加复杂。

主要类:

  • ReentrantLock :可重入的互斥锁
  • ReentrantReadWriteLock:可重入的读写锁

我的理解是,synchronized相当于一把互斥锁,但是lock能够实现的更多,性能还更好。



import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockExample {

	private static final ReentrantLock queueLock = new ReentrantLock(); //可重入锁
	private static final ReentrantReadWriteLock orderLock = new ReentrantReadWriteLock(); //可重入读写锁
	
	/**
	 * 有家奶茶店,点单有时需要排队 
	 * 假设想买奶茶的人如果看到需要排队,就决定不买
	 * 又假设奶茶店有老板和多名员工,记单方式比较原始,只有一个订单本
	 * 老板负责写新订单,员工不断地查看订单本得到信息来制作奶茶,在老板写新订单时员工不能看订单本
	 * 多个员工可同时看订单本,在员工看时老板不能写新订单
	 * @param args
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException {
		//buyMilkTea();
		handleOrder(); //需手动关闭
	}
	
	public void tryToBuyMilkTea() throws InterruptedException {
		boolean flag = true;
		while(flag)
		{
			if (queueLock.tryLock()) {
				//queueLock.lock();
				long thinkingTime = (long) (Math.random() * 500);
				Thread.sleep(thinkingTime);
				System.out.println(Thread.currentThread().getName() + ": 来一杯珍珠奶茶,不要珍珠");
				flag = false;
				queueLock.unlock();
			} else {
				//System.out.println(Thread.currentThread().getName() + ":" + queueLock.getQueueLength() + "人在排队");
				System.out.println(Thread.currentThread().getName() + ": 再等等");
			}
			if(flag)
			{
				Thread.sleep(1000);
			}
		}
	}
	
	public void addOrder() throws InterruptedException {
		orderLock.writeLock().lock();
		long writingTime = (long) (Math.random() * 1000);
		Thread.sleep(writingTime);
		System.out.println("老板新加一笔订单");
		orderLock.writeLock().unlock();
	}
	
	public void viewOrder() throws InterruptedException {
		orderLock.readLock().lock();
		long readingTime = (long) (Math.random() * 500);
		Thread.sleep(readingTime);
		System.out.println(Thread.currentThread().getName() + ": 查看订单本");
		orderLock.readLock().unlock();			
	}
	
	public static void buyMilkTea() throws InterruptedException {
		LockExample lockExample = new LockExample();
		int STUDENTS_CNT = 10;
		
		Thread[] students = new Thread[STUDENTS_CNT];
		for (int i = 0; i < STUDENTS_CNT; i++) {
			students[i] = new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						long walkingTime = (long) (Math.random() * 1000);
						Thread.sleep(walkingTime);
						lockExample.tryToBuyMilkTea();
					} catch(InterruptedException e) {
						System.out.println(e.getMessage());
					}
				}
			}
			);
			students[i].start();
		}
		for (int i = 0; i < STUDENTS_CNT; i++)
			students[i].join();
	}
	
	public static void handleOrder() throws InterruptedException {
		LockExample lockExample = new LockExample();
		
		Thread boss = new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
					try {
						lockExample.addOrder();
						long waitingTime = (long) (Math.random() * 1000);
						Thread.sleep(waitingTime);
					} catch (InterruptedException e) {
						System.out.println(e.getMessage());
					}
				}
			}
		});
		boss.start();

		int workerCnt = 3;
		Thread[] workers = new Thread[workerCnt];
		for (int i = 0; i < workerCnt; i++)
		{
			workers[i] = new Thread(new Runnable() {

				@Override
				public void run() {
					while (true) {
						try {
								lockExample.viewOrder();
								long workingTime = (long) (Math.random() * 5000);
								Thread.sleep(workingTime);
							} catch (InterruptedException e) {
								System.out.println(e.getMessage());
							}
						}
				}
			});
			workers[i].start();
		}
	}
}

需要注意的是:readLock表示读锁,可以多个线程共享,writeLock表示写锁,排他,只能有一个线程拥有。

4.2 Semaphore

信号量:本质是一个计数器,计数器大于0就可以使用,等于零就不能使用,可以设置多个并发量,可以限制为n个访问。

相当于是Lock更近一步,可以控制多个同时访问关键区。

  • acquire,获取,信号量减1
  • release,释放,信号量加1

其实与lock超像。

import java.util.concurrent.Semaphore;

public class SemaphoreExample {

	private final Semaphore placeSemaphore = new Semaphore(5);
	
	public boolean parking() throws InterruptedException {
		if (placeSemaphore.tryAcquire()) {
			System.out.println(Thread.currentThread().getName() + ": 停车成功");
			return true;
		} else {
			System.out.println(Thread.currentThread().getName() + ": 没有空位");
			return false;
		}

	}
	
	public void leaving() throws InterruptedException {
		placeSemaphore.release();
		System.out.println(Thread.currentThread().getName() + ": 开走");
	}
	
	/**
	 * 现有一地下车库,共有车位5个,由10辆车需要停放,每次停放时,去申请信号量
	 * @param args
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException {
		int tryToParkCnt = 10;
		
		SemaphoreExample semaphoreExample = new SemaphoreExample();
		
		Thread[] parkers = new Thread[tryToParkCnt];
		
		for (int i = 0; i < tryToParkCnt; i++) {
			parkers[i] = new Thread(new Runnable() {

				@Override
				public void run() {
					try {
						long randomTime = (long) (Math.random() * 1000);
						Thread.sleep(randomTime);
						if (semaphoreExample.parking()) {
							long parkingTime = (long) (Math.random() * 1200);
							Thread.sleep(parkingTime);
							semaphoreExample.leaving();
						}
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
			
			parkers[i].start();
		}

		for (int i = 0; i < tryToParkCnt; i++) {
			parkers[i].join();
		}	
	}
}

4.3 latch

同步辅助类,用来同步执行任务的一个或者多个线程,协调各个线程到了某个阶段的时候大家都等等,大家都到了,才一起接着往下走。
主要类和方法
CountDownLatch类

  • countDown()计数器减1
  • await() 等待latch变为0,如果没有变为0,继续等待。latch变为0之后,将环形所有在此latch上await的线程,解锁他们的await等待。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {

	/**
	 * 设想百米赛跑比赛 发令枪发出信号后选手开始跑,全部选手跑到终点后比赛结束
	 * 
	 * @param args
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		int runnerCnt = 10;
		CountDownLatch startSignal = new CountDownLatch(1);
		CountDownLatch doneSignal = new CountDownLatch(runnerCnt);

		for (int i = 0; i < runnerCnt; ++i) // create and start threads
			new Thread(new Worker(startSignal, doneSignal)).start();

		System.out.println("准备工作...");
		System.out.println("准备工作就绪");
		startSignal.countDown(); // let all threads proceed
		System.out.println("比赛开始");
		doneSignal.await(); // wait for all to finish
		System.out.println("比赛结束");
	}

	static class Worker implements Runnable {
		private final CountDownLatch startSignal;
		private final CountDownLatch doneSignal;

		Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
			this.startSignal = startSignal;
			this.doneSignal = doneSignal;
		}

		public void run() {
			try {
				startSignal.await();
				doWork();
				doneSignal.countDown();
			} catch (InterruptedException ex) {
			} // return;
		}

		void doWork() {
			System.out.println(Thread.currentThread().getName() + ": 跑完全程");
		}
	}
}

其实这里代码也超好理解,我觉得重要的地方在于怎么构建这个latch任务,注意它是把CountDownLatch对象传到任务中,如果共用的是同一个CountDownLatch对象,他们就会一起等待到该对象的latch变为0.

4.4 barrier

集合点,功能与latch一样,允许多个线程在某一点上进行同步。
他的原理是:通过构造函数设置同步线程的数量,await等待其他线程,到达数量后就放行。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
	
	/**
	 * 假定有三行数,用三个线程分别计算每一行的和,最终计算总和
	 * @param args
	 */
	public static void main(String[] args) {
		final int[][] numbers = new int[3][5];
		final int[] results = new int[3];
		int[] row1 = new int[]{1, 2, 3, 4, 5};
		int[] row2 = new int[]{6, 7, 8, 9, 10};
		int[] row3 = new int[]{11, 12, 13, 14, 15};
		numbers[0] = row1;
		numbers[1] = row2;
		numbers[2] = row3;
		
		CalculateFinalResult finalResultCalculator = new CalculateFinalResult(results);
		CyclicBarrier barrier = new CyclicBarrier(3, finalResultCalculator);
		//当有3个线程在barrier上await,就执行finalResultCalculator
		
		for(int i = 0; i < 3; i++) {
			CalculateEachRow rowCalculator = new CalculateEachRow(barrier, numbers, i, results);
			new Thread(rowCalculator).start();
		}		
	}
}

class CalculateEachRow implements Runnable {

	final int[][] numbers;
	final int rowNumber;
	final int[] res;
	final CyclicBarrier barrier;
	
	CalculateEachRow(CyclicBarrier barrier, int[][] numbers, int rowNumber, int[] res) {
		this.barrier = barrier;
		this.numbers = numbers;
		this.rowNumber = rowNumber;
		this.res = res;
	}
	
	@Override
	public void run() {
		int[] row = numbers[rowNumber];
		int sum = 0;
		for (int data : row) {
			sum += data;
			res[rowNumber] = sum;
		}
		try {
			System.out.println(Thread.currentThread().getName() + ": 计算第" + (rowNumber + 1) + "行结束,结果为: " + sum);
			barrier.await(); //等待!只要超过3个(Barrier的构造参数),就放行。
		} catch (InterruptedException | BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

class CalculateFinalResult implements Runnable {
	final int[] eachRowRes;
	int finalRes;
	public int getFinalResult() {
		return finalRes;
	}
	
	CalculateFinalResult(int[] eachRowRes) {
		this.eachRowRes = eachRowRes;
	}
	
	@Override
	public void run() {
		int sum = 0;
		for(int data : eachRowRes) {
			sum += data;
		}
		finalRes = sum;
		System.out.println("最终结果为: " + finalRes);
	}
}

创建方法还是一样的,把CyclicBarrier对象传到任务里,达到数量后会自动进行回调,注意这里的回调也是采用多线程run方法???这里还不是很清楚。

4.5 phaser

在每一阶段结束的位置对线程进行同步,当所有的线程都到达了这一步,再进行下一步,与上面的barrier不同的是可以多次来进行使用。

Phaser

  • arrive()
  • arriveAndAwaitAdvance() 所有的线程进行等待
import java.util.concurrent.Phaser;

public class PhaserExample {

   /**
    * 假设举行考试,总共三道大题,每次下发一道题目,等所有学生完成后再进行下一道
    * 
    * @param args
    */
   public static void main(String[] args) {

   	int studentsCnt = 5;
   	Phaser phaser = new Phaser(studentsCnt);

   	for (int i = 0; i < studentsCnt; i++) {
   		new Thread(new Student(phaser)).start();
   	}
   }
}

class Student implements Runnable {

   private final Phaser phaser;

   public Student(Phaser phaser) {
   	this.phaser = phaser;
   }

   @Override
   public void run() {
   	try {
   		doTesting(1);
   		phaser.arriveAndAwaitAdvance(); //等到5个线程都到了,才放行
   		doTesting(2);
   		phaser.arriveAndAwaitAdvance();
   		doTesting(3);
   		phaser.arriveAndAwaitAdvance();
   	} catch (InterruptedException e) {
   		e.printStackTrace();
   	}
   }

   private void doTesting(int i) throws InterruptedException {
   	String name = Thread.currentThread().getName();
   	System.out.println(name + "开始答第" + i + "题");
   	long thinkingTime = (long) (Math.random() * 1000);
   	Thread.sleep(thinkingTime);
   	System.out.println(name + "第" + i + "道题答题结束");
   }
}

4.6 exchanger

允许在并发线程中相互交换消息,允许在2个线程中定义同步点,当两个线程都达到同步点,他们交换数据结构

Exchanger

  • exchange(),线程双方互相交互数据
  • 交互数据是双向的

两个线程都执行到同一个Exchanger的exchange()方法时,就交换一下数据。



import java.util.Scanner;
import java.util.concurrent.Exchanger;

public class ExchangerExample {
	
	/**
	 * 本例通过Exchanger实现学生成绩查询,简单线程间数据的交换
	 * @param args
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		Exchanger<String> exchanger = new Exchanger<String>();
		BackgroundWorker worker = new BackgroundWorker(exchanger);
		new Thread(worker).start();
		
		Scanner scanner = new Scanner(System.in);
		while(true) {
			System.out.println("输入要查询的属性学生姓名:");
			String input = scanner.nextLine().trim();
			exchanger.exchange(input); //把用户输入传递给线程
			String value = exchanger.exchange(null); //拿到线程反馈结果
			if ("exit".equals(value)) {
				break;
			}
			System.out.println("查询结果:" + value);
		}
		scanner.close();
	} 
}

class BackgroundWorker implements Runnable {

	final Exchanger<String> exchanger;
	BackgroundWorker(Exchanger<String> exchanger) {
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		while (true) {
			try {
				String item = exchanger.exchange(null);
				switch (item) {
				case "zhangsan": 
					exchanger.exchange("90");
					break;
				case "lisi":
					exchanger.exchange("80");
					break;
				case "wangwu":
					exchanger.exchange("70");
					break;
				case "exit":
					exchanger.exchange("exit");
					return;
				default:
					exchanger.exchange("查无此人");
				}					
			} catch (InterruptedException e) {
				e.printStackTrace();
			}				
		}
	}		
}

总结

面对敌人的言行逼供,我一句话都没说,只是一五一十地把它写下来。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值