Java 给多线程编程提供了内置的支持。在多线程编程之前,我们需要先了解什么是线程。
进程和多线程简介
进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:线程与进程相似,但线程是一个比进程更小的执行单位。一条线程是进程中一个单一顺序的控制流
多线程:多线程就是多个线程同时运行或交替运行。
几个重要概念
同步和异步
:同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
并发和并行
:它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。而并行是真正意义上的“同时执行”。
高并发
:高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
阻塞和非阻塞
:非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,而阻塞与之相反。
临界区
:临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。
线程的生命周期
线程的生命周期就是指线程由创建到死亡的过程。如下图:
线程在生命周期内的几种状态除了阻塞状态,都比较好理解。那我们就重点看看阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。
三种阻塞状态:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
线程的优先级与守护线程
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
- 线程优先级具有继承特性,比如A线程启动B线程,则B线程的优先级和A是一样的。
- 线程优先级具有随机性,也就是说线程优先级高的不一定每一次都先执行完。
- Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。在默认情况下优先级都是Thread.NORM_PRIORITY(常数5)
守护线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用户线程: 运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
守护线程: 运行在后台,为其他前台线程服务。也可以说守护线程是JVM中非守护线程的 “佣人保姆”。它的特点是一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。所以会常应用到数据库连接池中的检测线程,JVM虚拟机启动后的检测线程的场景中。
那么如何设置守护线程呢?
可以通过调用Thead类的setDaemon(true)方法设置当前的线程为守护线程,最常见的守护线程:垃圾回收线程
多线程实现的四种方式
Java 多线程实现方式有四种:
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
- 通过 Callable 和 FutureTask 创建线程 重写call方法
- 通过线程池创建线程
从上面四种实现方式来看,我们可以将其分为两类:无返回值和有返回值。
前面两种重写run方法,返回值是void,所以没有办法返回结果;后面两种则有返回值。
具体来看看多线程的四种实现方式:
一、继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。 start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
class MyThread extends Thread {
// 线程执行体
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
class TestThread {
public static void main(String[] args) {
MyThread thread1 = new MyThread();// 创建一个新的线程thread1 此线程进入新建状态
MyThread thread2 = new MyThread();
thread1.start(); // 调用start()方法,使线程进入就绪状态
thread2.start();
}
}
二、实现Runnable接口
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。
class MyThread implements Runnable{
// 线程执行体
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
class TestThread {
public static void main(String[] args) {
MyThread myRunnable = new MyThread();// 创建一个Runnable实现类的对象
Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
Thread thread2 = new Thread(myRunnable);
thread1.start(); // 调用start()方法,使线程进入就绪状态
thread2.start();
}
}
创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
三、通过 Callable 和 Future 创建线程
Java 5.0 在 java.util.concurrent 提供了一个新的创建执行线程的方式: 实现 Callable 接口。
具体实现步骤:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
public class ThreadTest {
public static void main(String[] args) {
// 创建CallableThread对象
Callable<Integer> myCallable = new CallableThread();
//使用FutureTask来包装CallableThread对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
// FutureTask对象作为Thread对象的target创建新的线程
Thread thread = new Thread(ft);
thread.start();// 线程进入到就绪状态
}
}
System.out.println("主线程for循环执行完毕..");
try {
int sum = ft.get();
System.out.println("子线程的返回值:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class CallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
注意:执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
四、通过线程池创建线程
说线程池之前,应该先了解什么是线程池?
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程。池线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
为什么要使用线程池?
线程池是为了防止内存溢出,可以方便的管理线程,减少内存的消耗。
在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。所以,我们就提出了线程池的概念。
线程池的作用:
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
可以根据系统的承受能力,调整线程池中工作线程的数据,防止因为消耗过多的内存导致服务器崩溃
线程池类结构
- 最顶级的接口是Executor,不过Executor严格意义上来说并不是一个线程池而只是提供了一种任务如何运行的机制而已
- ExecutorService才可以认为是真正的线程池接口,接口提供了管理线程池的方法
- AbstractExecutorService分支就是普通的线程池分支,ScheduledExecutorService是用来创建定时任务的。
再介绍Executors类:提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。(通过Executors.newXXX方法即可创建。)
方法 | 描述 |
---|---|
newSingleThreadExecutos() | 单线程线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级列队)执行。 |
newFixedThreadPool(int nThreads) | 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待 |
newCachedThreadPool () | 创建一个可缓存线程池,这种线程池内部没有核心线程,线程的数量是有没限制的。(闲置状态)在超过了60S还不做事,就会销毁 |
newScheduledThreadPool () | 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。 |
代码示例:
public class ThreadTest {
public static void main(String[] args) {
// 创建固定大小线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
MyRunnable myRunnable = new MyRunnable();
// 执行任务并获取Future对象
threadPool.execute(myRunnable);
}
//关闭线程池
threadPool.shutdown();
System.out.println("主线程for循环执行完毕..");
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
}
}
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
Callable c = new MyCallable();
// 执行任务并获取Future对象
Future f = threadPool.submit(c);
创作不易,关注、点赞就是对作者最大的鼓励,欢迎在下方评论留言
求关注,定期分享Java知识,一起学习,共同成长。