java实现多线程的四种方式及速度比较
Java多线程的实现方式主要有以下几种:
-
继承Thread类:可以通过继承Thread类并重写run()方法来创建线程。需要注意的是,Java中不能直接继承多个类,因此这种方式不能同时继承其他类。
-
实现Runnable接口:可以通过实现Runnable接口并实现run()方法来创建线程。这种方式可以避免由于Java的单继承限制而带来的问题,并且能够更好地支持多个线程共享同一个资源。
-
实现Callable接口:与Runnable接口类似,但是Callable接口的call()方法可以返回执行结果或抛出异常,且必须通过FutureTask类或ExecutorService.submit()方法等方式来执行。
-
使用线程池:Java提供了Executor框架,它封装了线程池的创建和管理,使得开发者可以更方便地管理多个线程,避免线程过多或线程资源浪费等问题,它可以管理多个线程并且可以复用已经创建的线程,避免了频繁地创建和销毁线程的开销。
Executor(执行器)类:Executor有许多静态工厂方法用来构建线程池,其定义了线程池的基本行为,它的作用主要是为我们提供任务与执行机制(包括线程使用和调度细节)之间的解耦;
ExecutorService
继承了
Executor,其提供了更多操作多线程的方法,比如submit、invokeAll、invokeAny等,可以提交任务,还可以等待任务执行完成。AbstractExecutorService则是ExecutorService的
实现类
,它提供了ExecutorService接口的默认实现。ThreadPoolExecutor和ScheduledThreadPoolExecutor
继承
自AbstractExecutorService,这样就可以减少实现的复杂度。(PS:默认实现是指在接口中定义的方法的实现。在Java 8之前,接口中不能有方法的实现,但是Java 8引入了默认方法,它允许在接口中定义方法的实现。这些方法可以被实现接口的类继承或重写。)
Executors(执行器工具类)Executors工厂类一共可以创建四种类型的线程池,分别是newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool和newScheduledThreadPool。
- ExecutorService newFixedThreadPool() : 创建固定大小的线程池
- ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
- ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程
- ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
常用方法:
在Executor框架中,可以使用ExecutorService接口来管理线程池。ExecutorService接口中有两个常用的方法
submit
和invokeAll
。submit方法用于向线程池提交一个Callable或Runnable任务,并返回一个Future对象,可以通过该对象获取任务的执行结果或取消任务的执行。
invokeAll方法用于向线程池提交一组Callable任务,并返回一个包含所有任务执行结果的Future列表。当所有任务都执行完成后,该方法才会返回。
应用场景选择:
submit()
和invokeAll()
都是ExecutorService
接口中定义的方法,用于向线程池提交任务。它们的主要区别在于返回值和异常处理:submit()
方法用于提交一个Callable
或Runnable
任务,并返回一个表示该任务待处理结果的Future
对象。通过Future
对象可以判断任务是否完成、取消任务、获取任务的执行结果等。invokeAll()
方法用于同时提交多个Callable
任务,并返回一个Future
对象列表。这个方法会阻塞当前线程,直到所有任务都执行完成或者超时,然后返回所有任务的执行结果列表。在性能方面,
submit()
和invokeAll()
的差别不大,因为它们都是基于线程池实现的。但是,它们的使用场景不同,如果需要同时提交多个任务,并等待所有任务完成后再进行处理,使用invokeAll()
会更加方便和高效;如果只需要提交单个任务,并处理任务的执行结果,那么使用submit()
更为适合。
在使用Java进行多线程编程时,需要注意以下几点:
- 线程安全:在多线程环境下,如果多个线程同时修改同一个数据或资源,可能会导致数据不一致或资源竞争的问题。因此,需要确保多个线程能够正确地访问和修改共享的数据或资源。可以使用同步机制(如synchronized关键字)或使用线程安全的数据结构(如ConcurrentHashMap)来保证线程安全。
- 线程间通信:在多线程环境下,多个线程之间可能需要进行数据交换和协作。可以使用wait()、notify()和notifyAll()等方法来实现线程间的通信。
- 死锁:死锁是指多个线程在互相等待对方释放资源时,导致程序无法继续执行的情况。为避免死锁,可以使用避免策略(如避免嵌套锁,按照固定顺序获取锁)或使用死锁检测机制。
- 性能:多线程编程可能会影响程序的性能,因为线程间的切换需要时间和开销。为了提高程序的性能,可以考虑使用线程池和使用异步编程模型。
- 异常处理:多线程环境下,可能会出现一些难以调试的异常情况,如线程中断、InterruptedException和ConcurrentModificationException等。需要编写健壮的代码,并考虑如何处理异常情况。
例子
下面将以计算1到100000区间的素数作为例子,演示如何使用多线程编程:
1、继承Thread类:-耗时81毫秒
package com.mjf.base1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//继承Thread类:
// 可以通过继承Thread类并重写run()方法来创建线程。
// 需要注意的是,Java中不能直接继承多个类,因此这种方式不能同时继承其他类。
//下面第一种方式(118毫秒)无法做到线程同步,因为创建了两个ThreadDeam实例,应该创建一个ThreadDeam实例,再创建两个thread实例来保证线程安全,就像第二种(81毫秒)
public class TestThread_extends_Thread {
public static void main(String[] args){
long startTime = System.currentTimeMillis(); // 程序开始执行的时间戳
// 第一种方式
// ThreadDeam dd = new ThreadDeam("线程一","嚯嚯嚯");
// dd.start();
// ThreadDeam dd1 = new ThreadDeam("线程二","哈哈哈");
// dd1.start();
// 第二种方式
ThreadDeam dd2 = new ThreadDeam("线程一","嚯嚯嚯");
Thread t1 = new Thread(dd2, "Thread1");
Thread t2 = new Thread(dd2, "Thread2");
t1.start();
t2.start();
//调用Thread.join()方法等待其他线程执行完毕
try {
// dd.join();
// dd1.join();
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis(); // 程序结束执行的时间戳
System.out.println("程序执行时间为:" + (endTime - startTime) + "毫秒");
}
}
class ThreadDeam extends Thread{
private String arg;
private int count = 100000;
private int num = 1;
private Lock lock = new ReentrantLock();
public ThreadDeam(String name){
super(name);
}
public ThreadDeam(String name,String arg){
super(name);
this.arg = arg;
}
@Override
public void run(){
/*求3到100之间的素数,素数:素数是指除了1和它本身以外没有其他正整数能够整除它的大于1的整数。换句话说,素数是只能被1和它本身整除的正整数。*/
boolean flag =false;
System.out.println(arg);
while(num<count){
flag = false;
for(int j = 2;j<=Math.sqrt(num);j++){
if(num%j == 0){
flag = true;
break;
}
}
if(flag == false){
System.out.println(Thread.currentThread().getName()+",素数:"+num+" ");
}
lock.lock();
try{
num++;
} finally {
lock.unlock();
}
}
}
}
2、实现Runnable接口-耗时53毫秒
/*多个线程同时计算1到100000之间的素数,使用了synchronized实现方法级同步,用时53毫秒*/
public class TestThread_impl_runnable {
public static void main(String[] args){
long startTime = System.currentTimeMillis(); // 程序开始执行的时间戳
Runnabledemo dd = new Runnabledemo(1,100000); //多个线程调用同一个实例,则会共享该实例的变量num、endnum
Thread t1 = new Thread(dd,"线程一:");
Thread t2 = new Thread(dd,"线程二二:");
Thread t3 = new Thread(dd,"线程三三三:");
t1.start();
t2.start();
t3.start();
//调用Thread.join()方法等待其他线程执行完毕
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis(); // 程序结束执行的时间戳
System.out.println("程序执行时间为:" + (endTime - startTime) + "毫秒");
}
}
class Runnabledemo implements Runnable{
private int num;
private int endnum;
public Runnabledemo(int startnum,int endnum){
this.num = startnum;
this.endnum = endnum;
}
@Override
public void run(){
boolean flag = false;
System.out.println(Thread.currentThread().getName()+" 素数: 2");
synchronized (this) {
while (num <= endnum) {
if (isPrime(num)) {
System.out.println(Thread.currentThread().getName() + " 素数:" + num);
}
num++;
}
}
}
private boolean isPrime(int n) {
if (n < 2) {
return false;
}
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
}
3、实现Callable接口-耗时51毫秒
package com.mjf.base1;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestThread_impl_callable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis(); // 程序开始执行的时间戳
CallableDemo callableDemo = new CallableDemo();
// FutureTask是一个实现了Future接口和Runnable接口的类,它可以被用来包装一个Callable或Runnable对象,被Executor执行后,会产生一个结果。
// FutureTask可以通过get()方法来获取执行结果,也可以通过cancel()方法来取消执行任务。
// FutureTask可以用于异步执行任务,将耗时的任务提交给线程池后,主线程可以继续执行其他操作,等到结果需要被使用时,可以调用FutureTask的get()方法获取结果,
// 如果结果还未计算出来,则get()方法会阻塞当前线程,直到结果计算出来。
// FutureTask还提供了一些方法,如isCancelled()方法用来判断任务是否被取消,isDone()方法用来判断任务是否已经完成,等等。
// Future 和 FutureTask 都是用于异步执行任务并获取其执行结果的工具,但它们的用途和特点有所不同。
// Future 接口是一个异步任务的抽象,代表了一个异步计算的结果。通过 Future 接口,可以异步获取任务执行结果,或者通过 isDone() 方法判断异步操作是否已经完成。
// Future 接口定义了一些方法,比如 get() 方法可以阻塞当前线程等待任务完成并返回结果,还有一些方法可以取消任务等等。Future 的特点是轻量级,它只是一个接口,
// 不需要额外的线程或任务队列,可以方便地与多种异步机制配合使用,如 Executor 和 CompletionService 等。
// FutureTask 类则是 Future 接口的一种实现,它实现了 Future 和 Runnable 接口,因此可以被提交到线程池中执行。
// 与 Future 接口相比,FutureTask 的主要特点是它封装了一个计算任务,可以在 FutureTask 对象创建时传入一个 Callable 接口实例,表示需要异步执行的计算任务。
// FutureTask 会将这个计算任务提交给线程池执行,并将任务的计算结果保存在 FutureTask 中,可以通过 get() 方法获取。
// 此外,FutureTask 还支持任务的取消和状态查询等操作,具有更丰富的功能和更强的扩展性。
// 因此,可以简单地理解为 Future 是一种轻量级的异步计算结果的封装,而 FutureTask 则是一种更加完整的异步计算任务的封装,支持任务的提交、取消、状态查询等操作。
FutureTask futureTask = new FutureTask<>(callableDemo);
FutureTask futureTask2 = new FutureTask<>(callableDemo);
Thread dd = new Thread(futureTask);
dd.setName("线程一:");
dd.start();
Thread ddd = new Thread(futureTask);
ddd.setName("线程二:");
ddd.start();
// Thread dd2 = new Thread(futureTask2);
// dd2.setName("线程二:");
// dd2.start();
List<Integer> lists = (List<Integer>)futureTask.get();
for (Integer integer : lists){
System.out.println("素数:"+integer + " ");
}
// List<Integer> lists2 = (List<Integer>)futureTask2.get();
// for (Integer integer : lists2){
// System.out.println(dd.getName()+"素数:"+integer + " ");
// }
long endTime = System.currentTimeMillis(); // 程序结束执行的时间戳
System.out.println("程序执行时间为:" + (endTime - startTime) + "毫秒");
}
}
class CallableDemo implements Callable<List<Integer>>{
@Override
public List<Integer> call() throws Exception{
boolean flag = false;
List<Integer> lists = new ArrayList<>();
for(int i = 1 ; i < 100000;i ++){
flag = false;
for (int j = 2;j <= Math.sqrt(i); j ++){
if (i%j == 0){
flag = true;
break;
}
}
if (flag == false) {
lists.add(i);
}
}
return lists;
}
}
4、使用线程池-耗时17毫秒
/*多个线程去计算一段数据区间的素数-1到100000耗时17毫秒-全场最快*/
public class TestThread_ThreadPool {
public static void main(String[] args) throws InterruptedException, ExecutionException{
int intstart = 1;
int intend = 100000;
int n = intend - intstart;
int threadNum = 3;
int subRange = n / threadNum;
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
List<Future<List<Integer>>> results = new ArrayList<>();
long startTime = System.currentTimeMillis(); // 程序开始执行的时间戳
//循环提交任务到线程池
for (int i = 0; i < threadNum; i++) {
int start = intstart + i * subRange;
int end = (i == threadNum - 1) ? (intstart + n - 1) : (intstart + (i + 1) * subRange - 1);
//Future 接口,Future<List<Integer>> 是一个Java泛型类型,表示一个可能会在未来完成并返回结果的异步操作。
//在这个代码示例中,使用线程池提交任务时,线程池返回一个 Future 对象,其中包含了可能在某个未来时刻计算出的 List<Integer> 类型的结果。
//使用 Future 对象可以异步获取任务执行结果,或者等待任务执行完成。
//具体来说,可以通过调用 Future 对象的 get() 方法来获取异步操作返回的结果,或者通过 isDone() 方法判断异步操作是否已经完成。
Future<List<Integer>> future = executorService.submit(new Callable<List<Integer>>(){
@Override
public List<Integer> call() throws Exception {
System.out.println(Thread.currentThread().getName() + " ");
List<Integer> lists = new ArrayList<>();
for(int i = start; i <= end; i++){
if (i == 1) {
continue;
}
boolean flag = true;
for (int j = 2;j <= Math.sqrt(i);j ++){
if (i%j == 0){
flag = false;
break;
}
}
if (flag == true){
lists.add(i);
}
}
return lists;
}
});
results.add(future);
}
//等待所有任务执行完成,并将结果汇总到一个List中
List<Integer> finalResult = new ArrayList<>();
for (Future<List<Integer>> future : results) {
finalResult.addAll(future.get());
}
System.out.println(finalResult);
long endTime = System.currentTimeMillis(); // 程序结束执行的时间戳
System.out.println("程序执行时间为:" + (endTime - startTime) + "毫秒");
executorService.shutdown();
}
}