线程的创建
方式一:继承Thread类
1.1 步骤:
- 创建一个子类继承Thread类;
- 重写Thread类的run()方法,在该方法中编写该线程要执行的操作;
- 创建当前子类的对象;
- 通过创建的子类对象调用start()方法,启动线程。
例如:
创建一个线程用于打印1到100间的偶数。
java
//1.创建一个子类继承Thread类;
class PrintNumber extends Thread {
//2. 重写Thread类的run()方法,在该方法中编写该线程要执行的操作;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
主类调用:
java
public class EvenNumberTest extends Thread {
public static void main(String[] args) {
//3. 在主类的main方法中,创建当前子类的对象;
PrintNumber printNumber = new PrintNumber();
//4. 通过创建的子类对象调用start()方法,启动线程。
printNumber.start();
/*
* 能否使用printNumber.run()替换printNumber.start()的调用,实现分线程的创建和调用? 不能!
* start() 方法的作用是启动一个新的线程,并在该线程中执行 run() 方法。
* 这是真正意义上的多线程执行。
* 如果调用的是 printNumber.run(),则 run() 方法会在当前线程(即主线程)中执行,并不会创建新的线程,也就无法实现并发执行的效果。
* */
// printNumber.run();
/*
* 拓展:再提供一个分线程,用于100以内偶数的遍历。
*
* 注意:不能让已经start()的线程,再次执行start(),否则报异常IllegalThreadStateException
*
* */
//此时需要新建一个对象,而不能沿用之前创建的对象
PrintNumber printNumber1 = new PrintNumber();
printNumber1.start();
}
}
练习: 创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。
解决思路: 创建两个类均继承Thread类,分别打印偶数和奇数。
偶数线程类:
java
class EvenNumber extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
奇数线程类:
java
class OddNumber extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
主线程调用:
写法一:
java
public class Test {
public static void main(String[] args) {
EvenNumber evenNumber = new EvenNumber();
OddNumber oddNumber = new OddNumber();
evenNumber.setName("偶数线程");
oddNumber.setName("奇数线程");
evenNumber.start();
oddNumber.start();
}
}
写法二:
java
public class Test {
public static void main(String[] args) {
Thread tEven= new Thread(){
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
};
Thread tOdd = new Thread(){
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
};
tEven.setName("偶数线程");
tEven.start();
tOdd.setName("奇数线程");
tOdd.start();
}
写法三:
java
public class Test {
public static void main(String[] args) {
//使用匿名对象创建线程
new Thread(){
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
}
}
写法四:
java
public class Test {
public static void main(String[] args) {
// 使用Lambda表达式创建线程
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}, "偶数线程").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}, "奇数线程").start();
}
}
方式二:实现Runnable接口
2.1步骤:
- 创建一个实现Runnable接口的类;
- 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
- 创建当前实现类的对象;
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例;
- Thread类的实例调用start():1.启动线程;2.调用当前线程的run()方法。
例如:
创建一个线程用于打印1到100间的偶数。
java
// 1. 创建一个实现Runnable接口的类;
public class MyRunnable implements Runnable{
// 2. 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public static void main(String[] args) {
//3. 创建当前实现类的对象;
MyRunnable mr = new MyRunnable();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例;
Thread t1 = new Thread(mr);
//Thread类的实例调用start()。
t1.start();
/*
* 拓展:再创建一个线程,用于遍历100以内的偶数
*
* */
//此时可以发现,我们无需再创建新的对象,而是使用之前创建的对象即可
Thread t2 = new Thread(p);
t2.start();
}
}
练习: 创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。
使用匿名内部类对象来实现线程的创建和启动
方式一:
java
//其实还是上面的写法三的创建形式,只不过在创建时设置其name属性
public class Test {
public static void main(String[] args) {
new Thread("偶数线程") {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
new Thread("奇数线程") {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
}
}
方式二:
java
public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}, "偶数线程").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}, "奇数线程").start();
}
}
两种方式的区别与联系
区别
- 继承Thread:线程代码存放Thread子类run方法中。
- 实现Runnable:线程代码存在接口的子类的run方法。
实现Runnable接口比继承Thread类所具有的优势
- 避免了单继承的局限性;
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源;
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
联系
- 方式一的Thread类本身就实现了Runnable接口,即:
java
public class Thread extends Object implements Runnable
- 方式二的Runnable接口的实现类可以作为Thread类的构造器参数;
- 方式二的Runnable接口的实现类可以被多个Thread类的实例共享。
值得注意的是,第二种方式在执行start()方法时,执行逻辑与过程是这样的:
首先我们实现runnable接口时重写了run()方法;
start()方法调用的是Thread类中的start()方法,该方法调用了Thread类自己的run()方法,而Thread中的run()方法是这样的:
java
@Override
public void run() {
if (target != null) {
target.run();
}
}
它先对target是否为空进行了判断,那么这个target是什么呢,它是
java
private Runnable target;
Runnable接口类型的一个成员变量,而我们new的Thread类的构造器是这个:
java
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
而这个构造器就把target属性赋值了,该init函数调用的是:
java
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
继续查看调用的init函数:
java
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
可以看到在从下往上数第十行的位置,对target进行了赋值操作。
方式三:实现callable接口
3.1 步骤:
- 创建一个实现Callable的实现类;
- 实现call方法,将此线程需要执行的操作声明在call()中;
- 创建Callable接口实现类的对象;
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象;
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();
- 获取Callable中call方法的返回值。
示例: 打印1到100的偶数,并对其求和;
java
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
// Thread.sleep(1000);
}
return sum;
}
}
主测试类中:
java
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread t1 = new Thread(futureTask);
t1.start();
// 接收返回值
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
对比Runnable接口
- 与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值(需要借助FutureTask类,获取返回结果)
- Future接口(简单了解)
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
- 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
方式四:使用线程池创建线程
构建一个新的线程是有代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命周期很短的线程,就应该使用线程池 。一个线程池中包含多个准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run()方法,当run()方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数量。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数"固定的"线程池来限制并发线程的总数。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- ...
从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:ExecutorService
和 Executors
。
ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutor。void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般用来执行Callablevoid shutdown()
:关闭连接池
Executors
:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(int nThreads)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(int corePoolSize)
:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
可以采用实现 Runnable 接口 或 Callable接口的形式来使用线程池创建线程。
4.1 步骤:
- 创建一个实现 Runnable 接口 或 Callable接口的实现类;
- 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
- 提供指定线程数量的线程池;
- 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象;
- 关闭连接池。
示例: 使用两个线程分别打印1到100内的奇数和偶数,并对其求和。
java
//1). 创建一个实现 Runnable 接口的实现类;
class NumberThread implements Runnable{
//2). 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
//1). 创建一个实现 Runnable 接口的实现类;
class NumberThread1 implements Runnable{
//2). 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
//1). 创建一个实现 Callable 接口的实现类;
class NumberThread2 implements Callable {
//2). 重写call()方法,在该方法中编写该线程要执行的操作;
@Override
public Object call() throws Exception {
int evenSum = 0;//记录偶数的和
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
evenSum += i;
}
}
return evenSum;
}
}
主类中:
java
public class ThreadPoolTest {
public static void main(String[] args) {
//3).提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// //设置线程池的属性
// System.out.println(service.getClass());//ThreadPoolExecutor
service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
//4).执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
try {
Future future = service.submit(new NumberThread2());//适合使用于Callable
System.out.println("总和为:" + future.get());
} catch (Exception e) {
e.printStackTrace();
}
//5).关闭连接池
service.shutdown();
}
}
与此同时,Java 提供了 java.util.concurrent.Executors 工具类来快速创建常见的线程池类型:
1. 固定大小线程池:newFixedThreadPool
java
ExecutorService executor = Executors.newFixedThreadPool(10);
- 核心线程数 = 最大线程数 = 10
- 使用无界队列 LinkedBlockingQueue,所有线程一直存活,适合负载较重、任务量稳定的场景
2. 缓存线程池:newCachedThreadPool
java
ExecutorService executor = Executors.newCachedThreadPool();
- 核心线程数 = 0
- 最大线程数 = Integer.MAX_VALUE
- 空闲线程存活时间为 60 秒
- 使用 SynchronousQueue,适用于大量短生命周期的任务
3. 单线程池:newSingleThreadExecutor
java
ExecutorService executor = Executors.newSingleThreadExecutor();
- 只有一个线程,确保任务串行执行
- 使用无界队列
- 保证顺序执行且不会有并发问题
4. 调度线程池:newScheduledThreadPool
java
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
- 支持定时任务和周期性任务执行
- 例如延迟执行或固定频率重复执行
手动创建线程池
虽然 Executors 提供了快捷方式,但在生产环境中更推荐使用 ThreadPoolExecutor 构造函数手动创建线程池,以便更灵活地配置参数。
ThreadPoolExecutor构造函数签名:
java
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
参数名 | 含义 |
---|---|
corePoolSize | 核心线程数(常驻线程数量) |
maximumPoolSize | 最大线程数(允许的最大线程数量) |
keepAliveTime | 非核心线程空闲超时时间 |
unit | 超时时间单位 |
workQueue | 存放任务的阻塞队列 |
threadFactory | 线程工厂,用于创建线程(可自定义命名等) |
handler | 拒绝策略,当任务无法提交时的处理方式 |
示例代码:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
线程池的工作流程
- 提交任务后:
- 若当前运行线程 < corePoolSize,则创建新线程处理任务。
- 否则,尝试将任务放入工作队列。
- 如果队列满:
- 若当前线程数 < maximumPoolSize,创建新线程处理任务。
- 否则,执行拒绝策略(如抛异常、调用者执行等)。
常见拒绝策略(RejectedExecutionHandler)
策略 | 行为 |
---|---|
AbortPolicy | 默认策略,抛出 RejectedExecutionException 异常 |
CallerRunsPolicy | 由提交任务的线程自己执行该任务 |
DiscardPolicy | 默默丢弃任务,不抛异常 |
DiscardOldestPolicy | 丢弃队列中最老的任务,然后尝试重新提交新任务 |
使用线程池的最佳实践
实践建议 | 说明 |
---|---|
明确任务性质 | CPU密集型 vs IO密集型,决定线程池大小 |
避免无界队列 | 如 LinkedBlockingQueue 不传容量会导致任务堆积 |
设置合适的拒绝策略 | 避免系统崩溃,提供降级处理 |
自定义线程工厂 | 方便调试和日志追踪(如命名线程) |
关闭线程池 | 使用 shutdown() 或 shutdownNow() 避免资源泄漏 |
示例代码:完整使用线程池
java
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 20; i++) {
final int taskNo = i;
executor.execute(() -> {
System.out.println("正在执行任务 " + taskNo + ",线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown(); // 关闭线程池
}
}
以上就是四种创建线程的方法,其间如果涉及多线程对共享资源的处理,还需要使用同步代码块或同步方法以及使用wait()、notify()/notifyAll()函数来完成进程间通信从而解决多线程对共享资源的使用导致的并发安全问题。