首先在Java里面我们为什么要使用多线程呢?虽然在编程问题中有相当大一部分都可以通过顺序编程来解决,然而,对于某些问题,如果能够并发执行程序中的多个部分,则会变得非常方便甚至非常必要.比如在android应用编程里,所有耗时的操作,比如网络请求,都会在一个独立的线程里执行,以免阻塞UI线程,导致APP卡顿造成糟糕的用户体验.其次,使用多线程可以更好的利用cpu的资源.
有两个重要的概念我们要搞清楚,并行与并发:
- 并行:多个cpu同时执行不同任务,是真正的同时
- 并发:通过cpu调度算法,让多个任务看上去像是同时执行,实际上从cpu操作层面看不同任务是间隔执行的
如图所示:
虽然使用多线程有这些好处,但是当并行执行的任务开始产生互相干涉时,实际的并发问题就会接迥而至.故障可能还是偶尔出现的,不确定性的,这对于编程来说非常致命.因此我也会讨论如何实现线程安全.
JDK1.0:使用Thread和Runnable实现多线程
实现Runnable接口:
public class Task1 implements Runnable {
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void run() {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
logger.error("interrupted", e);
}
System.out.println(getClass().getSimpleName() + "执行完成");
}
}
或者继承Thread类:
public class Task2 extends Thread {
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void run() {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
logger.error("interrupted", e);
}
System.out.println(getClass().getSimpleName() + "执行完成");
}
}
测试:
public class MultiThreadTest {
@Test
public void test() throws Exception {
Thread thread1 = new Thread(new Task1());
thread1.start();
Task2 thread2 = new Task2();
thread2.start();
TimeUnit.SECONDS.sleep(5);
}
}
JDK1.5:使用ExecutorService实现多线程
ExecutorService在JDK1.5开始提供,旨在让我们避免直接管理Thread类对象,同时ExecutorService实现了线程复用,提高了效率.
创建一个什么样的ExecutorService的实例(即线程池)需要根据具体应用场景而定,不过Java给我们提供了一个Executors的辅助类,它可以帮助我们很方便的创建各种类型ExecutorService线程池,Executors一共可以创建下面这四类线程池:
- newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待.
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行.
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行.
还提供了新的Callable<T>类,用于得到线程的执行结果.如下:
public class Task3 implements Callable<String> {
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public String call() {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
logger.error("interrupted", e);
}
System.out.println(getClass().getSimpleName() + "执行完成");
return getClass().getSimpleName() + " success";
}
}
测试:
public class MultiThreadTest {
@Test
public void test1() throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Task1());
Future<String> future4 = executorService.submit(new Task3());
System.out.println(future4.get());
executorService.awaitTermination(5, TimeUnit.SECONDS);
}
}
JDK1.7:使用ForkJoinPool实现并行执行
ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来。
例如,我们想要计算从1到1百万的总和,使用ForkJoinPool的实现是这样的:
public class SumRecursiveTask extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public SumRecursiveTask(long[] numbers) {
this(numbers, 0, numbers.length);
}
// 私有构造函数用于以递归方式为主任务创建子任务
private SumRecursiveTask(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
return computeSequentially();
}
// 对任务分片,创建子任务
SumRecursiveTask leftTask = new SumRecursiveTask(numbers, start, start + length / 2);
leftTask.fork();
SumRecursiveTask rightTask = new SumRecursiveTask(numbers, start + length / 2, end);
Long rightResult = rightTask.compute();
Long leftResult = leftTask.join();
// 归并任务结果
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
}
测试:
public class MultiThreadTest {
@Test
public void test2() throws Exception {
long[] numbers = new long[1_000_000];
for (int i = 0; i < 1_000_000; i++) {
numbers[i] = i + 1;
}
RecursiveTask<Long> task = new SumRecursiveTask(numbers);
// 默认创建和当前电脑CPU核心数相同的线程数
Future<Long> future = ForkJoinPool.commonPool().submit(task);
System.out.println(future.get());
}
}
JDK1.8:使用Stream(流)
流是Java API的新成员,它允许你以声明性方式处理数据集合.此外,流还可以透明地并行处理,这样就无需写任何多线程代码了!上面计算从1到1百万的总和使用流来实现是这样的:
public class MultiThreadTest {
@Test
public void test3() {
long[] numbers = new long[1_000_000];
for (int i = 0; i < 1_000_000; i++) {
numbers[i] = i + 1;
}
System.out.println(LongStream.of(numbers).parallel().sum());
}
}
在这里,并行流内部也是使用了默认的ForkJoinPool,只不过所有对任务进行分片及归并的逻辑都由JDK在幕后替我们完成.
接下来,我们来研究下线程安全的问题,如下是一个偶数生成器工厂EvenGeneratorFactory:
public class EvenGeneratorFactory {
private static EvenGeneratorFactory evenGeneratorFactory = new EvenGeneratorFactory();
/**
* 损坏标志
*/
private volatile boolean broken;
private int currentValue = 0;
private EvenGeneratorFactory() {
}
public static EvenGeneratorFactory newInstance() {
return evenGeneratorFactory;
}
public int next() {
currentValue++;
currentValue++;
return currentValue;
}
public boolean isBroken() {
return broken;
}
public void setBroken(boolean broken) {
this.broken = broken;
}
}
这里EvenGeneratorFactory是一个单例对象,接着编写一个偶数检查器,在run方法里不断的尝试从EvenGeneratorFactory里取值并检查:
public class EvenChecker implements Runnable {
@Override
public void run() {
EvenGeneratorFactory evenGeneratorFactory = EvenGeneratorFactory.newInstance();
while (!evenGeneratorFactory.isBroken()) {
int number = evenGeneratorFactory.next();
if (number % 2 != 0) {
System.err.println(number + "不是一个偶数!");
evenGeneratorFactory.setBroken(true);
}
}
}
}
测试:
public class MultiThreadTest {
@Test
public void test4() throws Exception {
EvenGeneratorFactory evenGeneratorFactory = EvenGeneratorFactory.newInstance();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 2; i++) {
executorService.submit(new EvenChecker(evenGeneratorFactory));
}
executorService.awaitTermination(5, TimeUnit.SECONDS);
}
}
不断运行这个测试类,可以看到基本每次打印的数字都不一样.这里的问题就在于EvenGeneratorFactory类的next方法里的变量递增操作不是原子性的,当有多个线程同时操纵同一个共享的EvenGeneratorFactory对象时就导致了意外的结果.那么要纠正这个问题,有两种方式,第一种,使用synchronized 关键字:
public class EvenGeneratorFactory {
......
public synchronized int next() {
currentValue++;
currentValue++;
return currentValue;
}
......
}
第二种,使用ReentrantLock (可重入锁):
public class EvenGeneratorFactory {
......
private ReentrantLock reentrantLock = new ReentrantLock();
public int next() {
reentrantLock.lock();
try {
currentValue++;
currentValue++;
return currentValue;
}finally {
reentrantLock.unlock();
}
}
......
}
那么synchronized 和 ReentrantLock有什么区别呢(大部分面试都会提到这个)?
- 首先synchronized是java内置关键字,在jvm层面.ReentrantLock是个java类;
- synchronized无法判断是否获取锁的状态,ReentrantLock可以判断是否获取到锁以及获取各种锁的信息;
- synchronized无法设置获取锁超时,ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 ;
- synchronized的锁仅有单一的条件,而ReentrantLock可绑定多个条件实现多路通知;
- synchronized会自动释放锁,ReentrantLock需在finally中手工释放锁,否则容易造成线程死锁;
- synchronized的锁可重入、不可中断、非公平,而ReentrantLock锁可重入、可中断、可公平(两者皆可);
- 二者的锁机制也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头中mark word.
另外实现线程安全还可以使用ThreadLocal,例如我们很常用的SimpleDateFormat类不是线程安全的,与其在方法里每次使用的时候都new一个,不如使用ThreadLocal,让每个线程都拥有一个SimpleDateFormat对象,这样就不存在竞争问题了.看下面例子,共享同一个SimpleDateFormat对象:
public class Task4 implements Callable<String> {
private SimpleDateFormat simpleDateFormat;
public Task4(SimpleDateFormat simpleDateFormat) {
this.simpleDateFormat = simpleDateFormat;
}
@Override
public String call() throws ParseException {
for (int i = 0; i < 10000; i++) {
Date date = simpleDateFormat.parse("2019-12-12 12:12:12");
if (date.getTime() != 1576123932000L) {
System.err.println("解析日期错误:" + date);
}
}
return getClass().getSimpleName() + " success";
}
}
测试:
public class MultiThreadTest {
@Test
public void test5() throws Exception {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.submit(new Task4(simpleDateFormat));
}
executorService.awaitTermination(5, TimeUnit.SECONDS);
}
}
运行测试类,就可以发现解析出了许多错误的日期,这也验证了SimpleDateFormat类不是线程安全的.改进版本,使用ThreadLocal:
public class Task4 implements Callable<String> {
private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
@Override
public String call() throws ParseException {
// 某个线程首次调用get方法时,会先调用initialValue方法
SimpleDateFormat simpleDateFormat = threadLocal.get();
for (int i = 0; i < 10000; i++) {
Date date = simpleDateFormat.parse("2019-12-12 12:12:12");
if (date.getTime() != 1576123932000L) {
System.err.println("解析日期错误" + date);
}
}
return getClass().getSimpleName() + " success";
}
}
测试:
public class MultiThreadTest {
@Test
public void test5() throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.submit(new Task4());
}
executorService.awaitTermination(5, TimeUnit.SECONDS);
}
}
无论运行多少次,都不会出现解析错误的情况.
源码github地址:
jufeng98/java-mastergithub.com