继承Thread类
通过继承Thread并且重写其run()
方法,run()
方法中定义需要执行的任务。
创建后的子类通过调用start()
方法即可执行线程方法。
通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量。创建的是不同的Thread对象,自然不共享资源。
/**
* @Author: Forward Seen
* @CreateTime: 2022/03/22 20:32
* @Description:
* 1.定义MyThread类继承Thread
* 2.重写run方法
* 3.创建MyThread对象
* 4.调用start()方法启动线程
*/
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" is running :"+i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
根据结果我们可以发现,多线程之间(调用start()方法启动)是并行的关系,或者说同时执行,并不是先执行t1线程在执行t2线程,执行的顺序是个概率问题。
实现Runnable接口
需要先定义一个类实现Runnable接口并重写该接口的run()方法,此run()方法是线程执行体。接着创建Runnable实现类的对象,作为创建Thread对象的参数target,此Thread对象才是真正的线程对象。
利用实现Runnable接口的线程类创建对象,可以实现线程之间的资源共享。
/**
* @Author: Forward Seen
* @CreateTime: 2022/03/22 20:47
* @Description:
* 1.定义一个类UserRun实现Runnable接口
* 2.重写run()方法
* 3.创建UserRun类对象
* 4.创建Thread对象,把UserRun对象作为构造方法的参数
* 5.启动线程
*/
public class UserRun implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running :" + i);
}
}
}
public class UserRunTest {
public static void main(String[] args) {
UserRun userRun = new UserRun();
new Thread(userRun).start();
new Thread(userRun).start();
}
}
Runnable属于功能性接口,所以我们还可以通过匿名内部类以及lambda表达式的方式实现:
public class UserRunTest {
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running :" + i);
}
}).start();
}
}
实现Callable接口
Callable接口如同Runnable接口的升级版,其提供的call()
方法将作为线程的执行体,同时允许有返回值。
Callable对象不能直接作为Thread对象的target,因为Callable接口不是Runnable接口的子接口。
对于这个问题的解决方案,就引入Future接口,此接口可以接受call()
的返回值,RunnableFuture接口是Future接口和Runnable接口的子接口,可以作为Thread对象的target。
/**
* @Author: Forward Seen
* @CreateTime: 2022/03/23 10:50
* @Description:
* 1.定义类MyCallable实现Callable接口
* 2.实现call()方法
* 3.创建MyCallable的对象
* 4.创建RunnableFuture接口的子类FutureTask的对象,构造函数的参数的对象是MyCallable对象
* 5.创建Thread类的对象,构造函数的参数是FutureTask的对象
* 6.启动线程
*/
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName()+" is called!");
return "学习";
}
}
public class MyCallableTest {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
使用匿名内部类:
public class MyCallableTest {
public static void main(String[] args) {
new Thread(new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName()+" is " +
"called");
return "学习";
}
})).start();
}
}
继承TimerTask
Timer和TimerTask可以作为实现线程的另一种方式。
Timer是一种线程设施,用于安排以后在后台线程中执行的任务。可安排任务执行一次,或定期重复执行,可以看成一个定时器,可以调度TimerTask。
TimeTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力。
/**
* @Author: Forward Seen
* @CreateTime: 2022/03/23 13:45
* @Description:
* 步骤:
* 1.定义类UserTask,继承抽象类TimerTask
* 2.创建UserTask类的对象
* 3.创建Timer类的对象,设置任务的执行策略
*/
public class MyTask extends TimerTask {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" is running:"+new Date());
}
}
public class MyTaskTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+" is running:"+new java.util.Date());
Timer timer = new Timer();
timer.schedule(new MyTask(),5000,3000);
}
}
根据结果我们可以发现,每隔三秒定时调度这个Timer-0的线程。
通过线程池启动多线程
什么是线程池
首先我们应该了解什么是线程池,其实线程池就像一个池子,池子里面放着固定数量的线程。如图,该线程池中有7个线程,它们不会自动创建和销毁,当需要用到线程时,直接从线程池中调取一个线程,当这个线程用完之后再放回到线程池中。当一次性来了两个任务,那么就从线程池中拿出两个线程来执行,执行完后直接放回池中。当一次性来了10个任务,而线程池中一共就7个线程,那么剩下3个任务就等待,当有线程空闲下来才会去处理。
线程池的优点:
提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行。
降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁的消耗。
方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
Java中可以通过Executors的工具类可以创建线程池。
- FixThreadPool固定大小的线程池
public class FixThreadPoolTest {
public static void main(String[] args) {
// 1.创建固定大小的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
// 2.使用线程池执行任务
for (int i = 0; i < 5; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName()+
":"+j);
}
}
});
}
// 3.关闭线程池
pool.shutdown();
}
}
根据结果,我们发现,5个任务,只有3个线程在执行,因为线程池限制了线程的数量,并且每个线程执行完一个任务才去执行下一个任务。
- SingleThreadExecutor单线程池
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 4; j++) {
System.out.println(Thread.currentThread().getName() +
":" + j);
}
}
});
}
pool.shutdown();
}
}
对于单线程的结果,我们发现,它是按照任务提交顺序执行的,执行完当前任务才去执行下一个任务。
- CachedThreadPool缓存线程池
缓存线程池中的线程数量不是固定的,而是动态的。它给到了最大值,也就是int类型所能表示的最大值。当线程池中没有可用的线程时,它会自动创建一个线程,并且该线程池中的线程可以缓存、重复利用和回收,回收的默认时间为1min。
public class CachedThreadPoolTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 3; j++) {
System.out.println(Thread.currentThread().getName()+
" : "+j);
}
}
});
}
pool.shutdown();
}
}
根据结果,我们有5个任务,所以该线程池创建了5个线程。
- ScheduledThreadPool周期性线程池,支持定时及周期性执行任务。
参数是核心线程数,核心线程数指的是线程池中最小的线程数量,也就是说,核心线程创建后不会被回收。
ScheduledThreadPool有个scheduledAtFixedRate()
方法,有4个参数,第一个参数是线程对象,第二个参数是首次执行任务的延迟时间,第三个参数表示每隔多长时间执行run()中的任务,最后一个参数是时间单位。
public class ScheduledThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
pool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+new Date());
}
},5,3, TimeUnit.SECONDS);
}
}
- WorkStealingPool任务窃取线程池。
之前的线程池中的线程都是共享任务队列的,而WorkStealingPool是每个线程都有自己的任务队列。
假如有一个任务队列,其中有一个很大的任务,剩下的任务都很小,由线程池分配任务给线程,那么拿到这个大的任务的线程压力就会很大,这样就会导致cpu负载不均衡。而WorkStealingPool是每个线程都有自己的任务队列,一旦线程发现自己的任务执行完了,他就会到别的线程中获取任务来执行,所以叫“窃取”。WorkStealingPool可以更加合理得使用CPU,非常适用于很耗时间的任务中,也可以充分利用多核CPU的优势。
public class WorkStealingPoolTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("--start--");
ExecutorService pool = Executors.newWorkStealingPool();
for (int i = 0; i < 10; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
//让主线程等待子线程执行完毕
Thread.sleep(2000);
System.out.println("--end--");
}
}
如果其中的某个任务很大,该线程池会把这个大任务拆分成小任务分配到空闲的线程中,等这些小任务执行完成后会将这些结果合并。