线程问题多线程

前言
多线程一直以来都知识点比较多,让人难以理解,网上的文章大多也是长篇大论。最近有空所以将平时接触到的多线程知识由浅入深的以代码示例形式整理出来,希望为后来者解惑,快速的了解多线程相关的知识,减少采坑。

一、初识多线程
多线程最基本的两种实现方式
方式一
继承Thread类并并重写run方法

public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(“线程名:” + Thread.currentThread().getName() + " i是:" + i);
}
}
}
1
2
3
4
5
6
7
8
测试:

public class Test {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
}
}
1
2
3
4
5
6
7
8
输出:

线程名:Thread-0 i是:1
线程名:Thread-1 i是:1
线程名:Thread-0 i是:2
线程名:Thread-1 i是:2
线程名:Thread-0 i是:3
线程名:Thread-1 i是:3
线程名:Thread-1 i是:4
线程名:Thread-1 i是:5
线程名:Thread-0 i是:4
线程名:Thread-0 i是:5
1
2
3
4
5
6
7
8
9
10
方式二
实现Runnable接口,并用其初始化Thread

public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(“线程名:” + Thread.currentThread().getName() + " i是:" + i);
}
}
}
1
2
3
4
5
6
7
8
测试:

public class Test {
public static void main(String[] args) {
Thread myThread1 = new Thread(new MyThread());
Thread myThread2 = new Thread(new MyThread());
myThread1.start();
myThread2.start();
}
}
1
2
3
4
5
6
7
8
输出:

线程名:Thread-0 i是:1
线程名:Thread-1 i是:1
线程名:Thread-1 i是:2
线程名:Thread-0 i是:2
线程名:Thread-1 i是:3
线程名:Thread-1 i是:4
线程名:Thread-1 i是:5
线程名:Thread-0 i是:3
线程名:Thread-0 i是:4
线程名:Thread-0 i是:5
1
2
3
4
5
6
7
8
9
10
说明: 两种方式的测试代码里分别开了myThread1 和myThread2 两个线程,调用 start 方法便会开始执行run 方法中的循环打印1到5。

你会发现输出结果并不是先执行完Thread-0 再执行 Thread-1的,而且每次执行输出顺序可能都不一样,这就是多线程。

平时我们写的代码都是线性的,代码会按照顺序执行完这一行再执行下一行,而多线程你可以理解为多条车道,cup是驱动着你写的程序这辆车在多车道上飞驰,它一下在这条车道上开,一下在另一台车道上开,由于cup实在是太快了,让你看起来好像有多张车在多车道上开,就好像武林高手出拳速度太快产生残影让你以为他有很多手,实际上在某一时刻上只有一张车在其中的一条车道上开。

多线程优点
1.采用多线程技术的应用程序可以更好地利用系统资源。主要优势在于充分利用了CPU的空闲时间片,用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。

2.异步,当你想执行多个耗时的复杂运算,可以考虑多线程。
比如:你想做一道菜,你需要烧水、买菜、切菜、煮饭,你可以多开几个线程,一个线程烧水,一个线程去买菜、一个线程去煮饭,同时进行,等最后再把几个线程的结果合起来就把一道菜做好了。

二、匿名函数实现无返回值的多线程异步调用
public class Test {
public static void main(String[] args) {
System.out.println(“准备一个新线程”);
Thread newThread = new Thread(() -> {
printA();
});
newThread.start();//开始执行线程
System.out.println(“主线程完成”);
}

public static void printA() {
    for (int i = 0; i < 5; i++) {
        System.out.println("线程执行中:"+i);
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jdk8中上面代码可简化成如下:

public class Test {
public static void main(String[] args) {
System.out.println(“开始一个新线程”);
new Thread(() -> {
printA();
}).start();
System.out.println(“主线程完成”);
}

public static void printA() {
    for (int i = 0; i < 5; i++) {
        System.out.println("线程执行中:" + i);
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出结果:

准备一个新线程
主线程完成
线程执行中:0
线程执行中:1
线程执行中:2
线程执行中:3
线程执行中:4
1
2
3
4
5
6
7
三、CompletableFuture实现多线程异步调用
Java 8中的 java.util.concurrent.CompletableFuture 提供了CompletableFuture,可用于进行简单的新开一个线程进行异步操作。

无返回值的异步调用
CompletableFuture.runAsync(() -> {
// 要执行的异步方法逻辑
});
1
2
3
示例:
使用CompletableFuture.runAsync异步的计算1到100的和

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
System.out.println(“准备开始一个新线程”);
CompletableFuture.runAsync(() -> {
int mumber = 0;
for (int i = 0; i <= 100; i++) {
mumber += i;
}
System.out.println(“0到100的和是:” + mumber);
});
System.out.println(“主线程完成”);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
结果:

准备开始一个新线程
主线程完成
0到100的和是:5050
1
2
3
有返回值的异步调用
有时候我们主线程需要用到子线程返回结果,可以使用CompletableFuture.supplyAsync

CompletableFuture future =CompletableFuture.supplyAsync(() -> {
///要执行的异步方法逻辑
});
1
2
3
示例:

子线程计算1到100的和,计算完后子线程将结果返回到 future 中,主线程通过get()来获取子线程的值。

future.get()是阻塞的,也就是说如果主线程执行到 future.get()子线程还没返回值,主线程会一直等待不再往下执行,直到子线程执行完主线程通过 future.get() 获取到了子线程的值才会往下运行。

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
System.out.println(“准备开始一个新线程”);
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
Integer number = 0;
for (int i = 0; i <= 100; i++) {
number += i;
}
return number;
});

    try {
        System.out.println(future.get());
        System.out.println("完成");
        System.out.println("主线程完成");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } 
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
结果:

准备开始一个新线程
5050
完成
主线程完成
1
2
3
4
什么是 Callable 和 Future?
关于线程的有返回值和无返回值其实就是实现了Callable接口的call方法和Runnable接口的run方法。(本文实例基本都用了lambda简化代码,下面的定长线程池部分你可以看到是怎么简化的)

Callable接口类似于 Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而 Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future 可以拿到异步执行任务的返回值。可以认为是带有回调的 Runnable。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果。

四、四种线程池的使用
上面讲的是通过new Thread等方式创建线程,这种方式的弊端是:

a. 每次new Thread新建对象性能差。

b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

c. 缺乏更多功能,如定时执行、定期执行、线程中断。

下面将要介绍的是Jdk提供的四种线程池的好处在于:

a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。

b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

c. 提供定时执行、定期执行、单线程、并发数控制等功能。

1.newFixedThreadPool定长线程池
public class MyRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(“线程名:” + Thread.currentThread().getName() + " i是:" + i);
}
}
}
1
2
3
4
5
6
7
8
import thread.MyRunable;
import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
fixedThreadPool.execute(new MyRunable());
}
fixedThreadPool.shutdown();//关闭线程池
}
}
1
2
3
4
5
6
7
8
9
10
11
12
上面代码可用lambda表达式简化如下:
(为方便讲解,后面的几种线程池都将采用lambda简化形式)

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int j = 0; j < 3; j++) {
fixedThreadPool.execute(() -> {
for (int i = 0; i < 2; i++) {
System.out.println(“线程名:” + Thread.currentThread().getName() + " i是:" + i);
}
});
}
fixedThreadPool.shutdown();//关闭线程池
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出:

线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-2 i是:0
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-2 i是:1
1
2
3
4
5
6
说明:
Executors.newFixedThreadPool(2)里我们声明只能创建2个线程,即使我们循环了3次,也只有两个线程创建出来,超出的一个线程会在队列中等待最后用第二个线程执行,所以出现了thread-1有4次,thread-2两次的情况。

关闭线程池有两种关闭方式:

shutdown()://停止接收新任务,不会立即关闭,直到当前所有线程执行完成才会关闭
shutdownNow();//停止接收新任务,原来的任务停止执行,但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。
1
2
2.CachedThreadPool可缓存线程池
可缓存线程池为无限大,当执行第二个任务时第一个任务已经完成,会回收复用第一个任务的线程,而不用每次新建线程,可灵活回收空闲线程,若无可回收,则新建线程。

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int j = 0; j < 3; j++) {
cachedThreadPool.execute(() -> {
for (int i = 0; i < 2; i++) {
System.out.println(“线程名:” + Thread.currentThread().getName() + " i是:" + i);
}
});
}
cachedThreadPool.shutdown();//关闭线程池
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出:

线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-3 i是:0
线程名:pool-1-thread-2 i是:0
线程名:pool-1-thread-2 i是:1
线程名:pool-1-thread-3 i是:1
1
2
3
4
5
6
3.newSingleThreadExecutor单线程化线程池
newSingleThreadExecutor线程池你可以理解为特殊的newFixedThreadPool线程池,它只会创建一个线程,并且所有任务按照指定顺序。如果你创建了多个任务,因为只会有一个线程,多余的任务会被阻塞到队列里依次执行。

下面的示例循环3次,每次都是用的一个线程,这个线程会先执行第一个循环的任务,在执行第二个循环的任务,再执行第三个循环的任务,所以输出的 i 是有序的。

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
for (int j = 0; j < 3; j++) {
singleThreadPool.execute(() -> {
for (int i = 0; i < 3; i++) {
System.out.println(“线程名:” + Thread.currentThread().getName() + " i是:" + i);
}
});
}
System.out.println(“准备关闭线程池”);
singleThreadPool.shutdown();//关闭线程池
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
输出:

准备关闭线程池
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-1 i是:2
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-1 i是:2
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-1 i是:2
1
2
3
4
5
6
7
8
9
10
4. newScheduledThreadPool周期性线程池
newScheduledThreadPool周期性线程池用来处理延时任务或定时任务。

无返回值的延时线程示例
import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3);
System.out.println(“测试1”);
for (int i = 0; i < 5; i++) {
scheduleThreadPool.schedule(() -> {
System.out.println(“线程名:” + Thread.currentThread().getName() + “已经过了3秒”);
}, 3, TimeUnit.SECONDS);
}
System.out.println(“测试2”);
scheduleThreadPool.shutdown();//关闭线程池
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
说明:我们声明了3个线程,创建的时候用循环创建了5个,多出来的2个会阻塞直到前3个线程有执行完的再复用他们的线程;因为采用了延时3秒输出,所以会先输出测试1、测试2,然后等待3秒后再执行输出线程的内容。

输出:

测试1
测试2
线程名:pool-1-thread-2已经过了3秒
线程名:pool-1-thread-3已经过了3秒
线程名:pool-1-thread-1已经过了3秒
线程名:pool-1-thread-3已经过了3秒
线程名:pool-1-thread-2已经过了3秒
1
2
3
4
5
6
7
有返回值的延时线程示例
public class Test {
public static void main(String[] args) {
ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3);
System.out.println(“测试1”);
ScheduledFuture scheduledFuture = scheduleThreadPool.schedule(() -> {
return “线程名:” + Thread.currentThread().getName() + “已经过了3秒”;
}, 3, TimeUnit.SECONDS);

    System.out.println("测试2");
    try {
        //获取线程返回的值并输出
        System.out.println(scheduledFuture.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    scheduleThreadPool.shutdown();//关闭线程池
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输出:

测试1
测试2
线程名:pool-1-thread-1已经过了3秒
1
2
3
定时线程执行
定时执行可以用scheduleAtFixedRate方法进行操作,里面的参数4表示代码或启动运行后第4秒开始执行,3表示每3秒执行一次。因为我们设置了3个线程,所以运行后线程会在第4秒开始用3个线程每3秒执行一次。

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3);
System.out.println(“测试1”);
scheduleThreadPool.scheduleAtFixedRate(() -> {
System.out.println(“线程名:” + Thread.currentThread().getName() + “已经过了3秒”);
}, 4, 1, TimeUnit.SECONDS);
System.out.println(“测试2”);

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
输出:

测试1
测试2
线程名:pool-1-thread-1已经过了3秒
线程名:pool-1-thread-1已经过了3秒
线程名:pool-1-thread-2已经过了3秒
线程名:pool-1-thread-3已经过了3秒

1
2
3
4
5
6
7
五、使用guava库的线程池创建线程(推荐)
上面我们介绍了四种jdk自带的线程池,但是平常不推荐使用。在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,并且线程池不允许使用Executors去创建,要通过ThreadPoolExecutor方式创建。

这一方面是由于jdk中自带的线程池,都有其局限性,不够灵活;另外使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

因此下面介绍更为推荐的使用guava库的线程池创建线程。
使用的依赖maven如下

com.google.guava guava 29.0-jre 1 2 3 4 5 在开始前需要注意线程池的几个参数: (在下面代码的ThreadPoolExecutor里你会看到这些参数):

corePoolSize=> 线程池里的核心线程数量
maximumPoolSize=> 线程池里允许有的最大线程数量
keepAliveTime=> 空闲线程存活时间
unit=> keepAliveTime的时间单位,比如分钟,小时等
workQueue=> 缓冲队列
threadFactory=> 线程工厂用来创建新的线程放入线程池
handler=> 线程池拒绝任务的处理策略,比如抛出异常等策略

线程池按以下行为执行任务
1. 当线程数小于核心线程数时,创建线程。
2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3. 当线程数大于等于核心线程数,且任务队列已满
-1 若线程数小于最大线程数,创建线程
-2 若线程数等于最大线程数,抛出异常,拒绝任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
无返回值的线程创建
下面代码初始化了线程池并用 executorService.execute 分别创建了两个线程,一个用来输出本线程的名字,另一个用来异步调用 printA() 方法。

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
System.out.println(“开始”);
//线程池的初始化
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build();
ExecutorService executorService = new ThreadPoolExecutor(60, 100,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024), namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());

    //开启一个新线程用来输出线程的名字
    executorService.execute(() -> System.out.println("第1个线程名字" + Thread.currentThread().getName()));
    
    //再开启一个新线执行printA()
    executorService.execute(() -> {
        System.out.println("第2个线程名字" +Thread.currentThread().getName());
        printA();
    });

    System.out.println("完成");
    executorService.shutdown();
}

public static void printA() {
    for (int i = 0; i < 3; i++) {
        System.out.println("打印:aaaaaaaaaaaaa");
    }
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
输出:

开始
完成
第1个线程名字demo-pool-0
第2个线程名字demo-pool-1
打印:aaaaaaaaaaaaa
打印:aaaaaaaaaaaaa
打印:aaaaaaaaaaaaa
1
2
3
4
5
6
7
有返回值的多线程调用
使用submit

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
System.out.println(“开始”);

    //线程池的初始化
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
    ExecutorService executorService = new ThreadPoolExecutor(5, 10,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingDeque<>(1024), namedThreadFactory,
            new ThreadPoolExecutor.AbortPolicy());
    
    //异步调用对象integerCallableTask中的call()计算1-100的和
    Future<Integer> future = executorService.submit(() -> {
        int nummber = 100;
        int sum = 0;
        for (int i = 0; i <= nummber; i++) {
            sum += i;
        }
        return sum;
    });
    
    try {
        //获取计算的结果
        Integer result = future.get();
        System.out.println("和是:" + result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

    System.out.println("完成");
    //shutdown():停止接收新任务,原来的任务继续执行
    //shutdownNow():停止接收新任务,原来的任务停止执行
    executorService.shutdown();
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
线程池的submit和execute方法区别
1、接收的参数不一样
execute接收的参数是new Runnable(),重写run()方法,是没有返回值的:

    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("第1个线程名字" + Thread.currentThread().getName());
        }
    });

1
2
3
4
5
6
可用lambda表达式简化如下:

           executorService.execute(() -> System.out.println("第1个线程名字" + Thread.currentThread().getName()));

1
submit接收的参数是Callable,重写call()方法,是有返回值的:

Future future = executorService.submit(new Callable() {
@Override
public Integer call() throws Exception {
int nummber = 100;
int sum = 0;
for (int i = 0; i <= nummber; i++) {
sum += i;
}
return sum;
}
});
1
2
3
4
5
6
7
8
9
10
11
可用lambda表达式简化如下:

Future future = executorService.submit(() -> {
int nummber = 100;
int sum = 0;
for (int i = 0; i <= nummber; i++) {
sum += i;
}
return sum;
});
1
2
3
4
5
6
7
8
2、submit有返回值用于返回多线程计算后的值,而execute没有返回值

有返回值的批量多线程调用
开三个线程计算0-3000的和,第一个任务计算0-1000,第二个任务计算1001-2000,第三个任务计算2001-3000。用invokeAll方法一次性把三个任务进行提交执行,提交后每个任务开始分别计算,最后使用futu.get()分别获取三个结果相加得到总的结果。

用于每个任务的类IntegerCallableTask :

import java.util.concurrent.Callable;

public class IntegerCallableTask implements Callable {

int start;
int end;

public int getStart() {
return start;
}

public void setStart(int start) {
this.start = start;
}

public int getEnd() {
return end;
}

public void setEnd(int end) {
this.end = end;
}

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

@Override
public Integer call() throws Exception {
System.out.println(“正在执行call方法”);
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import utils.IntegerCallableTask;

import java.util.;
import java.util.concurrent.
;

public class Test {
public static void main(String[] args) {
System.out.println(“开始”);
//线程池的初始化
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build();
ExecutorService executorService = new ThreadPoolExecutor(5, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque(1024), namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());

    //三个多线程异步批量调用对象integerCallableTask中的call()计算0-3000的和
    List<IntegerCallableTask> list = new ArrayList<>();
    list.add(new IntegerCallableTask(0, 1000));
    list.add(new IntegerCallableTask(1001, 2000));
    list.add(new IntegerCallableTask(2001, 3000));


    List<Future<Integer>> future = null;
    Integer result = 0;
    try {
        System.out.println("准备提交三个任务");
        //invokeAll方法提交批量任务,提价后每个任务才会开始执行call方法
        future = executorService.invokeAll(list);
        //遍历取出三个结果相加得到0-3000的和
        for (Future<Integer> futu : future) {
            result += futu.get();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println("0-3000的和是:" + result);
    System.out.println("完成");

    executorService.shutdown();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
结果:

开始
准备提交三个任务
正在执行call方法
正在执行call方法
正在执行call方法
0-3000的和是:4501500
完成
1
2
3
4
5
6
7
多任务其中一个执行完其余都停止
有时候我们需要开多个线程执行多个一样的任务,谁先计算出结果就返回谁的结果,其余线程全部停止结束。比如,开两个线程查找一个txt文件,A线程查找电脑C盘,B线程查找D盘,哪一个线程先找到就返回谁的,并且另一个停止不再继续查找。

采用invokeAny方法。

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

public class StringCallableTask implements Callable {
private String taskName;
private int seconds;

public StringCallableTask(String taskName, int seconds) {
    this.taskName = taskName;
    this.seconds = seconds;
}

@Override
public String call() throws Exception {
    System.out.println("正在执行" + taskName);
    //模拟耗时操作
    TimeUnit.SECONDS.sleep(seconds);
    System.out.println(taskName + "执行完成!");
    return  taskName + "执行完成";
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import utils.StringCallableTask;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;

public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//线程池的初始化
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build();
ExecutorService executorService = new ThreadPoolExecutor(6, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    //分别创建两个任务
    List<StringCallableTask> taskList = new ArrayList<>();
    taskList.add(new StringCallableTask("任务1", 2));
    taskList.add(new StringCallableTask("任务2", 1));

    //使用invokeAny提交两个任务,谁先执行完返回谁,另一个没执行完的结束不再执行
    String result = executorService.invokeAny(taskList);

    executorService.shutdown();
    System.out.println("----------------");
    System.out.println("结果:" + result);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
结果:

正在执行任务2
正在执行任务1
任务2执行完成!

结果:任务2执行完成
1
2
3
4
5
invokeAny()与invokeAll()区别:
在上面的示例中我们使用了invokeAny(),而在上一标题有返回值的批量多线程调用中我们使用了invokeAll()。

invokeAny(): 取得第一个完成任务的结果值,当第一个任务执行完成后,会调用interrupt()方法将其他任务中断,也就是结束其他线程的执行。

invokeAll(): 等全部线程任务执行完毕后,取得每一个线程的结果然后返回。

六、线程间的数据同步
有时候我们需要让数据在多个线程间共享,在一个线程上数据改变后,在另一个线程上该数据也要跟着同步。

举个例子,你创建了两个线程,分别用来对课间进出教室的人进行计数,假设有学生n个,一个线程用来计数出教室的人出去一个就n-1,另一个线程用来对进教室的人计数进一个就n+1,两个线程间的n要同步一致。

多线程间的数据一致性——原子操作
下面的代码开启30个线程,每个线程执行1000次num加一操作最后输出结果。

(1). 为确保num在30个线程间的同步,使用了AtomicInteger原子类;

(2) .为确保30个线程都执行完再输出,使用了CountDownLatch来对线程计数,每执行完一个线程则countDownLatch.countDown()线程计数器减一,最后用countDownLatch.await()让程序等着,直到countDownLatch为0了才往下执行输出结果。

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
public static void main(String[] args) {
//使用AtomicInteger来保证不同线程进行加1后,num都能保持一致,num初始化为0
AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待所有30个计算线程执行完,计数器初始化为30
CountDownLatch countDownLatch = new CountDownLatch(30);

    //线程池初始化
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
    ExecutorService executorService = new ThreadPoolExecutor(60, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    //开启30个新线程进行累加操作,每个线程进行10000次对num加1
    for (int i = 0; i < 30; i++) {
        executorService.execute(() -> {
            System.out.println(Thread.currentThread().getName());
            for (int j = 0; j < 10000; j++) {
                num.incrementAndGet();//num加1,原子性的num++,通过循环CAS方式
            }
            //当此线程执行完,线程计数器减1
            countDownLatch.countDown();
        });
        //返回当前线程执行到第几个
        System.out.println("剩余线程数量:" + countDownLatch.getCount());
    }
    //等到线程计数器变为0,也就是确保30个线程都执行完
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //输出最终的num
    System.out.println("结果:" + num);
    executorService.shutdown();
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
输出:


demo-pool-27
demo-pool-25
demo-pool-20
demo-pool-28
demo-pool-29
结果:300000
1
2
3
4
5
6
7
补充:

常用的是原子类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference;并且AtomicInteger、AtomicLong还支持算数运算。其中AtomicReference用于对象。

还有原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;原子数组中的每个元素都可以实现原子更新。

线程间变量同步——synchronized关键字
使用synchronized作用在代码块上或方法上对变量进行同步操作。

用synchronized关键字修饰的方法叫同步方法,虽然 synchronized关键字加在代码块上但它锁定的是调用当前方法的对象。

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class AddNumber {
private int number = 0;
static CountDownLatch countDownLatch = new CountDownLatch(7);

//synchronized作用在方法上, 循环100次对number加1
public synchronized void addNumber() {
    IntStream.range(0, 100).forEach(i -> number += 1);
    //线程减1
    countDownLatch.countDown();
}

//或者synchronized作用在方法块上,循环100次对number加1

/* public void addNumber() {
synchronized (this) {
IntStream.range(0, 100).forEach(i -> number += 1);
}
//线程减1
countDownLatch.countDown();
}*/

public int count() {
    //线程池初始化
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
    ExecutorService executorService = new ThreadPoolExecutor(60, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    //创建7个线程,每个线程循环100次对number加1
    IntStream.range(0, 7).forEach(i -> executorService.execute(() -> addNumber()));
    try {
        //等待所有线程执行完成
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("完成");
    executorService.shutdown();
    return number;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
测试:

import utils.AddNumber;
public class Test {
public static void main(String[] args) {
AddNumber addNumber = new AddNumber();
System.out.println(“结果是:” + addNumber.count());
}
}
1
2
3
4
5
6
7
输出:

完成
结果是:700
1
2
线程间变量隔离独立——ThreadLocal
ThreadLocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。

ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

下面的代创建了两个线程,每个线程向localVar 存储一个值,第一个线程存储了“localVar1”,第二个线程存储了localVar2“”,各现场存储的值只能在各自的线程里设置、读写、删除,各不影响。

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;

public class Test {
static ThreadLocal localVar = new ThreadLocal<>();
public static void main(String[] args) {
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build();
ExecutorService executorService = new ThreadPoolExecutor(60, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    executorService.execute(()->{
        //设置线程1中本地变量的值为localVar1
        localVar.set("localVar1");
        //打印当前线程中本地内存中本地变量的值
        System.out.println("thread1" + " :" + localVar.get());
        //清除本地内存中本线程创建的的本地变量
        localVar.remove();
        //打印清除后的本地变量
        System.out.println("thread1 after remove : " + localVar.get());

    });

    executorService.execute(()->{
        //设置线程2中本地变量的值
        localVar.set("localVar2");
        //打印当前线程中本地内存中本地变量的值
        System.out.println("thread2" + " :" + localVar.get());
        //清除本地内存中本线程创建的的本地变量
        localVar.remove();
        //打印清除后的本地变量
        System.out.println("thread2 after remove : " + localVar.get());
    });
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
输出:

thread1 :localVar1
thread2 :localVar2
thread1 after remove : null
thread2 after remove : null
1
2
3
4
ConcurrentHashMap的使用
一般我们在开发中会使用HashMap,但是HashMap是线程不安全的不能在多线程中使用,一般在多线程中我们使用ConcurrentHashMap来确保线程安全。

ConcurrentHashMap使用示例:
开启三个线程,两个put数据,一个get数据
注意:ConcurrentHashMap的key和value不能为null,否则会报空指针异常。

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap();
System.out.println(“开始”);
//线程池的初始化
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(“demo-pool-%d”).build();
ExecutorService executorService = new ThreadPoolExecutor(60, 100,0L,TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    executorService.execute(() -> {
        for (int i = 0; i < 10; i++) {
            map.put(i,String.valueOf(i));
        }
    });

    executorService.execute(() -> {
        for (int i = 10; i < 20; i++) {
            map.put(i,String.valueOf(i));
        }
    });

    executorService.execute(() -> {
        for (int i = 0; i < 20; i++) {
            System.out.println(map.get(i));
        }
    });

    System.out.println("完成");
    executorService.shutdown();
    System.out.println(map.size());
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
输出:

开始
完成
0
1
2
3

1
2
3
4
5
6
7
扩展:
HashMap、HashTable、ConcurrentHashMap的区别:

HashTable: 线程安全,采用synchronized实现,效率低,key和value不能为空

HashMap: 线程不安全,key和value能为空(key只能有一个为null,value可以有多个为null),初始化容量为16,负载因子0.75

ConcurrentHashMap: 线程安全,用于多线程,采用分段锁,比HashTable效率更高,key和value不能为空,,初始化容量为16,负载因子0.75

参考:
多线程编程中如何确保子线程执行完毕后主线程再执行-CountDownLatch

Java并发编程-常用的辅助类

Java 8中的并行和异步编程

​ CompletableFuture 使用详解

ThreadPoolExecutor参数说明

Runnable和Callable的区别和联系

java Future用法

任务的批量提交invokeAll两种方法的区别

Java多线程看这一篇就足够了

Java多线程:彻底搞懂线程池

ThreadLocal

Java中的ThreadLocal详解

多线程同步的五种方法
————————————————
版权声明:本文为CSDN博主「西凉的悲伤」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33697094/article/details/115920570

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值