Java线程详解
注:这里详解的意思是完全让小白明白,当然一家之言根本不可能的,所以希望看到的又对您有点帮助的,或者大牛们看到了觉得有补充的地方,欢迎留言批评指正,后期陆续都会慢慢完善,谢谢……
一、概念介绍
进程与线程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。进程不依赖于线程而独立存在,一个进程中可以启动多个线程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
“同时”执行是人的感觉,在线程之间实际上轮换执行。
进程在执行过程中拥有独立的内存单元,进程有独立的地址空间,而多个线程共享内存,从而极大地提高了程序的运行效率。
区别:
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容:
- 一个指向当前被执行指令的指令指针;
- 一个栈;
- 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
- 一个私有的数据区。
相关面试题:
1.线程、进程,二者的区别?
线程是CPU独立运行和独立调度的基本单位;
进程是资源分配的基本单位;是执行着的应用程序;
进程包含线程;
两者的联系:进程和线程都是操作系统所运行的程序运行的基本单元。
区别:进程具有独立的空间地址,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程只是一个进程的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。
2.线程和进程各自有什么区别和优劣呢?
进程是资源分配的最小单位,线程是程序执行的最小单位。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
二、Java中的线程
用户线程与守护线程:
二者其实基本上是一样的。唯一的区别在于JVM何时离开。
用户线程:当存在任何一个用户线程未离开,JVM是不会离开的。
守护线程:如果只剩下守护线程未离开,JVM是可以离开的。
1.创建线程的方法
1.1 继承Thread类,重写run方法
public class ThreadTestThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread类");
}
public static void main(String[] args) {
ThreadTestThread t = new ThreadTestThread();
t.start();
}
}
1.2 实现Runnable 接口,重写run方法
public class ThreadRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable 接口创建线程");
}
public static void main(String[] args) {
ThreadRunnable runnable = new ThreadRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
1.3 实现Callable ()接口,重写run方法
java.util.concurrent.Callable包下
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadCallable implements Callable {
@Override
public Object call() throws Exception {
return "实现Callable线程";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadCallable callable = new ThreadCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
futureTask.run();
System.out.println(futureTask.get());
}
}
总结:
继承Thread类-在实际开发中,很少使用;
实现Runnable接口-实际开发中用的还是很多的;
实现Callable接口-实际开发中用的很多,大多是线程有返回值的情况下,若没有返回值,建议用Runnable接口,另外callable接口要结合Future 与 FutureTask 一起使用。
2. 线程池
2.1 为什么要用线程池
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
2.2 线程池的原理
流程:
1>线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2>线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3>线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){
}
流程:
1>如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2>如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3>如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4>如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。根据不同的拒绝策略去处理。
参数:
1>corePoolSize(线程池的基本大小):当提交一个任务到线程池时,如果当前poolSize<corePoolSize时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2>maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
3>keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
4>TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
5>runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
-
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
-
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
-
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
-
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
6>ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线
程设置有意义的名字,代码如下。
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
7>RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
-
AbortPolicy:直接抛出异常。
-
CallerRunsPolicy:只用调用者所在线程来运行任务。
-
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
-
DiscardPolicy:不处理,丢弃掉。
注:上面的参数在实际运用中,不一定都需要,还有不同的构造方法,选则适合自己业务的进行构造。
2.3 提交
有俩个方法:execute()和submit()方法。区别:execute()用于提交不需要返回值的任务,submit()方法用于提交需要返回值的任务。
execut():
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
submit():
线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方
法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Future<Object> future = executor.submit(haveReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}
2.4 关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。
三、多线程的实现
1. ThreadPoolExecutor + Callable + Future
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class ThreadController {
// 定义线程池
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
10,
20,
1000,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Test
public void test1() throws ExecutionException, InterruptedException {
List<User> list = Arrays.asList(
new User(1,"小渺",18),
new User(2,"小明",14),
new User(3,"小化",19),
new User(4,"小小",21),
new User(5,"小没",20)
);
Callable<List<User>> callable = () -> filterUser(list);
Future<List<User>> submit = THREAD_POOL_EXECUTOR.submit(callable);
System.out.println(JSON.toJSONString(submit));
if (submit.isDone()) {
System.out.println(JSON.toJSONString(submit.get()));
}
submit.get().stream().forEach(e -> System.out.println(e));
}
private List<User> filterUser(List<User> list) {
return list.stream().filter(user -> user.getAge() >= 15).collect(Collectors.toList());
}
}
2. ThreadPoolExecutor + Callable + FutureTask
public class ThreadTest {
/**
* 线程池
*/
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
10, //corePollSize
20, //maximumPoolSize
1000,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
/**
*
*/
@Test
public void test1_ThreadTest() throws ExecutionException, InterruptedException {
List<User> list = Arrays.asList(
new User(1,"小渺",18),
new User(2,"小明",14),
new User(3,"小化",19),
new User(4,"小小",21),
new User(5,"小没",20)
);
Callable<List<User>> callable = () -> filterUser(list);
FutureTask<List<User>> futureTask = new FutureTask<>(callable);
THREAD_POOL_EXECUTOR.submit(futureTask);
if (futureTask != null) {
System.out.println(JSON.toJSONString(futureTask.get()));
futureTask.get().stream().forEach(e -> {
System.out.println(JSON.toJSONString(e));
});
}
}
private List<User> filterUser(List<User> list) {
return list.stream().filter(user -> user.getAge() >= 20).collect(Collectors.toList());
}
}
注:以上大部分来源与网络,望各位道友见谅,共同进步,谢谢……
补充:
1.现在在工作中遇到线程池+Future/FutureTask 循环中开启线程,然后线程中处理逻辑,切记共享值的传入,可能导致线程返回数据混乱,处理方法:可以在线程的方法中加上 synchronized 关键字,但是不建议这么做,这样使得内存消耗很大,另一种处理方法就是共享值作为参数传入时,在线程方法中重新定义 也就是new,这样可以防止多线程数据错乱问题;
2.就是工作中git代码 findbugs 校验,此时不宜使用 isDone() 函数,同样也会导致多线程返回数据错乱;
3.线程安全的集合List,使用Collections.synchronizedList(List<T> list);
4.StringBuffer(多线程) 与 StringBuilder (单线程);
5.使用线程池+FutureTask+Callable 入参处理不当的话,会造成数据混乱,如下所示:(如要验证结果,请自己尝试)
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.ObjectUtils;
import org.junit.Test;
import java.util.*;
import java.util.concurrent.*;
/**
* 多线程callable 使用要注意的问题
*/
public class CallableProblem {
/**
* 创建线程池
*/
private static final ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(
10,
20,
1000,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
@Test
public void test1_CallableProblem() throws ExecutionException, InterruptedException {
List<User> users = Arrays.asList(
new User(1,"小渺",18),
new User(2,"小明",14),
new User(3,"小化",19),
new User(4,"小小",21),
new User(5,"小没",20)
);
JSONObject body = new JSONObject();
List<FutureTask<List<String>>> futureTasks = Collections.synchronizedList(new ArrayList<>());
for (int i=0; i<4; i++) {
body.put("key", i);
FutureTask<List<String>> futureTask = getFutureTask(users, body);
if (ObjectUtils.isNotEmpty(futureTask)) {
futureTasks.add(futureTask);
}
}
if (!futureTasks.isEmpty()) {
for (FutureTask<List<String>> futureTask : futureTasks) {
List<String> list = futureTask.get();
System.out.println(list);
}
}
}
/**
*
* @param users
* @param body
* @return
*/
private FutureTask<List<String>> getFutureTask(List<User> users, JSONObject body) {
JSONObject newBody = new JSONObject();
newBody.putAll(body);
/**
* 不加上面俩句,以及下面入参变量 变成新定义的变量,则输出结果会错乱。
*/
Callable<List<String>> callable = () -> getCallable(users, newBody);
FutureTask<List<String>> futureTask = new FutureTask<>(callable);
POOL_EXECUTOR.submit(futureTask);
return futureTask;
}
private List<String> getCallable(List<User> users, JSONObject body) {
List<String> lis = Collections.synchronizedList(new ArrayList<>());
for (User user : users) {
lis.add(user.getName());
}
if (!body.isEmpty()) {
lis.add(body.get("key").toString());
}
return lis;
}
}