1、JAVA构建线程的所有方式
- 通过继承Thread类创建线程。这种方式需要定义一个子类,继承Thread类,并重写run()方法,然后创建子类的对象,并调用start()方法启动线程。
- 代码示例:
//定义一个子类,继承Thread类
class MyThread extends Thread {
//重写run()方法
@Override
public void run() {
//线程要执行的逻辑
System.out.println("Hello, I am a thread.");
}
}
//在主方法中创建子类对象,并调用start()方法启动线程
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread(); //创建子类对象
t.start(); //启动线程
}
}
- 通过实现Runnable接口创建线程。这种方式需要定义一个类,实现Runnable接口,并实现run()方法,然后创建该类的对象,并作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法启动线程。
- 代码示例:
//定义一个类,实现Runnable接口
class MyRunnable implements Runnable {
//实现run()方法
@Override
public void run() {
//线程要执行的逻辑
System.out.println("Hello, I am a thread.");
}
}
//在主方法中创建该类对象,并作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法启动线程
public class Main {
public static void main(String[] args) {
MyRunnable r = new MyRunnable(); //创建该类对象
Thread t = new Thread(r); //创建Thread对象
t.start(); //启动线程
}
}
- 通过实现Callable接口创建线程。这种方式需要定义一个类,实现Callable接口,并实现call()方法,然后创建该类的对象,并作为参数传递给FutureTask类的构造器,创建FutureTask对象,并作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法启动线程。这种方式可以获取线程的返回值和异常。
- 代码示例:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) throws Exception {
// 创建一个Callable任务
MyCallable task = new MyCallable();
// 使用FutureTask包装Callable任务
FutureTask<Integer> future = new FutureTask<>(task);
// 创建一个线程并执行FutureTask
Thread thread = new Thread(future);
thread.start();
// 获取Callable任务的返回结果
Integer sum = future.get();
System.out.println("The sum is: " + sum);
}
}
// 实现Callable接口的类
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
- 通过使用线程池创建线程。这种方式可以避免频繁地创建和销毁线程,提高性能和资源利用率。可以使用Executor框架提供的各种线程池,如FixedThreadPool, CachedThreadPool, ScheduledThreadPool等,或者自定义线程池。然后将实现了Runnable或Callable接口的任务对象提交给线程池执行。
2、run和start的区别是什么
在java线程中,run和start的区别主要有以下几点:
- run方法是线程类或者Runnable接口的一个普通方法,它定义了线程要执行的任务,但是直接调用它并不会启动一个新的线程,而是在当前线程中顺序执行run方法里的代码
- start方法是线程类的一个特殊方法,它的作用是创建一个新的线程,并让这个线程进入就绪状态,等待CPU的调度。当CPU把时间片分配给这个线程后,它就会自动调用run方法来执行任务
- 一个线程对象只能调用一次start方法,否则会抛出IllegalThreadStateException异常。但是可以多次调用run方法,只是没有多线程的效果
- 调用start方法可以实现多线程的并发效果,提高程序的效率和响应性。而调用run方法只能实现单线程的顺序执行,没有多线程的优势
3、线程池
3.1、ThreadPoolExecutor的使用
ThreadPoolExecutor是一个实现了ExecutorService接口的类,它可以创建和管理一个线程池,执行Runnable或Callable任务
要使用ThreadPoolExecutor,你可以直接创建它的实例,或者使用Executors类的工厂方法来获取它的实例
下面是一个例子,它直接创建了一个ThreadPoolExecutor的实例,并设置了核心线程数、最大线程数、空闲线程存活时间、任务队列和拒绝策略
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建一个有四个核心线程、六个最大线程、一秒空闲存活时间、有十个容量的阻塞队列和默认拒绝策略的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 6, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// 提交十五个打印任务
for (int i = 0; i < 15; i++) {
executor.execute(new PrintTask("Task " + i));
}
// 关闭线程池
executor.shutdown();
}
}
// 定义一个打印任务类,实现Runnable接口
class PrintTask implements Runnable {
private String name;
public PrintTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + " is running on " + Thread.currentThread().getName());
}
}
输出结果如下图所示
你可以看到,当提交第十二个任务时,由于队列已满,而且没有空闲的线程,所以触发了拒绝策略,抛出了RejectedExecutionException异常。这说明你需要根据你的任务数量和性能要求来合理地配置线程池的参数。
3.2 FixedThreadPool线程池的使用例子
FixedThreadPool的核心线程数和最大线程数都被设置成相同的固定值。
使用无界队列LinkedBlocking作为线程池的工作队列,队列的容量为Integer.MAXVALUE,由于使用无界队列,线程池并不会拒绝任务。
FixedThreadPool是一种线程池,它可以创建一个固定数量的线程来执行任务。如果所有的线程都在忙,那么新的任务就会在队列中等待。如果有空闲的线程,那么队列中的任务就会被分配给它们
下面是一个简单的例子,它创建了一个有两个线程的FixedThreadPool,并提交了五个打印任务
执行流程如下:
1、如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务
2、在线程池中完成预热之后,将任务加入任务队列当中
3、线程执行完1中的任务后,会不断从任务队列中拿取任务执行
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个有两个线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交五个打印任务
for (int i = 0; i < 5; i++) {
executor.execute(new PrintTask("Task " + i));
}
// 关闭线程池
executor.shutdown();
}
}
// 定义一个打印任务类,实现Runnable接口
class PrintTask implements Runnable {
private String name;
public PrintTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + " is running on " + Thread.currentThread().getName());
}
}
输出如下
你可以看到,只有两个线程在交替执行五个任务。这样可以避免创建过多的线程,提高资源利用率和性能。
3.3 SingleThreadExecutor
SingleThreadExecutor是一种ExecutorService,它使用一个单独的线程来执行所有提交的任务。它类似于一个线程数为1的FixedThreadPool,但是它有一些额外的特性,比如保证任务的顺序执行和维护一个隐藏的任务队列使用SingleThreadExecutor可以避免多线程的同步问题,比如在访问共享文件系统时2下面是一个使用SingleThreadExecutor的例子:
同样,SingleThreadExecutor采用的也是无界队列LinkedBlockingQueue作为线程池的工作队列。同样不会拒绝任务。
1、如果当前运行线程数少于核心线程数,则创建一个新线程来执行任务
2、当线程池完成预热之后,将任务加入无界队列中
3、线程执行完1中的任务后,会从任务队列中拿任务来继续执行
// 创建一个SingleThreadExecutor
ExecutorService executor = Executors.newSingleThreadExecutor ();
// 提交三个任务
executor.execute (new Runnable () {
@Override
public void run () {
System.out.println ("Task 1");
}
});
executor.execute (new Runnable () {
@Override
public void run () {
System.out.println ("Task 2");
}
});
executor.execute (new Runnable () {
@Override
public void run () {
System.out.println ("Task 3");
}
});
// 关闭executor
executor.shutdown ();
输出结果如下
3.4 CachedThreadPool
CachedThreadPool是一个可缓存的线程池,它的工作机制如下:
- CachedThreadPool的核心线程数为0,最大线程数为Integer.MAX_VALUE,空闲线程的存活时间为60秒
- CachedThreadPool使用SynchronousQueue作为任务队列,这是一个没有容量的队列,每个插入操作必须等待另一个线程的移除操作;即主线程提交任务后,任务必须在队列中等待有线程过来处理它,否则无法提交新任务
- 当有新任务提交时,如果有空闲的线程,就从队列中取出任务执行;如果没有空闲的线程,就创建一个新的线程执行任务
- 当执行完任务的线程空闲超过60秒时,就会被回收销毁;这样可以避免长时间保持空闲的线程占用资源
代码示例 :
// 创建一个可缓存的线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 定义一个简单的任务
Runnable task = () -> {
System.out.println("Hello from " + Thread.currentThread().getName());
};
// 提交10个任务到线程池
for (int i = 0; i < 10; i++) {
cachedPool.execute(task);
}
// 关闭线程池
cachedPool.shutdown();
CachedThreadPool适合执行大量的短期小任务,或者负载较轻的服务器。但是要注意控制并发的任务数,否则可能会创建过多的线程,导致性能问题。另外,CachedThread不适合执行长时间或不确定时间的任务,例如IO操作。
3.5 ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是一个可以执行定时任务或周期性任务的线程池,它的工作机制如下:
- ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,但是它的最大线程数为Integer.MAX_VALUE,即无界的,它的核心线程数可以指定,空闲线程的存活时间为0123。
- ScheduledThreadPoolExecutor使用DelayedWorkQueue作为任务队列,这是一个有序队列,它会根据每个任务的下次执行时间来排序,越早执行的任务越靠前42。
- 当有新任务提交时,如果有空闲的核心线程,就从队列中取出任务执行;如果没有空闲的核心线程,就创建一个新的线程执行任务;如果达到了最大线程数,就把任务放入队列等待123。
- ScheduledThreadPoolExecutor提供了四种方法来执行定时任务或周期性任务:
- schedule(Runnable command, long delay, TimeUnit unit):在指定的延迟时间后执行一次任务
- schedule(Callable<V> callable, long delay, TimeUnit unit):在指定的延迟时间后执行一次任务,并返回结果
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):在指定的初始延迟时间后开始周期性地执行任务,每次执行的间隔时间为period,不受任务执行时间的影响
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):在指定的初始延迟时间后开始周期性地执行任务,每次执行的间隔时间为上次任务结束后到下次任务开始前的时间
代码示例:
// 创建一个核心线程数为2的定时线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 定义一个简单的任务
Runnable task = () -> {
System.out.println("Hello from " + Thread.currentThread().getName());
};
// 在延迟5秒后执行一次任务
scheduledPool.schedule(task, 5, TimeUnit.SECONDS);
// 在延迟10秒后开始周期性地每隔15秒执行一次任务
scheduledPool.scheduleAtFixedRate(task, 10, 15, TimeUnit.SECONDS);
// 在延迟20秒后开始周期性地每隔10秒加上上次任务执行时间执行一次任务
scheduledPool.scheduleWithFixedDelay(task, 20, 10, TimeUnit.SECONDS);
ScheduledThreadPoolExecutor适合执行需要定时或周期性触发的任务,例如定时清理缓存、定时发送消息等但是要注意控制并发的任务数和执行时间,否则可能会导致任务堆积或错过预期的触发时间。另外,ScheduledThreadPoolExecutor不支持修改已经提交的定时或周期性任务的触发时间或频率
4、AQS队列
AQS队列是一个双向链表,用来存放等待获取锁的线程节点。每个节点有一个waitStatus属性,表示节点的等待状态,有以下几种取值:
- CANCELLED (1):表示当前节点已取消调度。当超时或者中断情况下,会触发变更为此状态,进入该状态后的节点不再变化。
- SIGNAL (-1):表示当前节点释放锁的时候,需要唤醒下一个节点。或者说后继节点在等待当前节点唤醒,后继节点入队时候,会将前驱节点更新给signal。
- CONDITION (-2):表示当前节点在条件队列中。当其他线程调用了condition的signal方法后,condition状态的节点会从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE (-3):表示当前场景下后续的acquireShared能够得以执行。共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继的后继节点。
- 0 :新节点入队时候的默认状态。
- 独占锁:每次只能一个线程持有锁,比如ReentrantLock就是独占锁。独占锁中,只有头结点才能尝试获取锁,其他节点都会被阻塞。当头结点释放锁时,会将其后继节点的waitStatus设置为SIGNAL,并唤醒后继节点。后继节点被唤醒后,会检查自己是否是头结点的后继节点,如果是,则尝试获取锁;如果不是,则自旋等待直到成为头结点的后继节点或者被取消。
- 共享锁:允许多个线程持有锁,并发访问共享资源,比如ReentrantReadWriteLock。共享锁中,头结点和其后继节点都可以尝试获取锁,如果成功,则向后传播唤醒其他共享模式的节点;如果失败,则检查自己的waitStatus是否为SIGNAL,如果是,则阻塞等待被唤醒;如果不是,则自旋等待直到被设置为SIGNAL或者被取消。
因此,AQS队列中的节点并不是一直在自旋,而是根据自己的waitStatus和锁的模式来决定是否自旋、阻塞或者尝试获取锁。
5、park\unpark\wait\notify的区别
- thread.park是LockSupport类的方法,用于阻塞和唤醒线程,不需要获取对象的锁;wait是Object类的方法,用于让线程等待和通知,必须在同步块或者同步方法中调用。
- thread.park不会抛出InterruptedException异常,但是会响应中断,即当线程被中断时,thread.park会立即返回;wait会抛出InterruptedException异常,需要调用者处理。
- thread.park可以先于unpark调用,即先让线程获取一个许可证,然后再调用park时不会阻塞;wait必须先于notify或者notifyAll调用,否则可能导致线程永久等待。
- thread.park不会释放持有的锁;wait会释放持有的锁,让其他线程有机会获取锁。