1、实现多线程的方式

在Java中,实现多线程有四种方式:

  • 继承Thread类,重写run()方法;
  • 实现Runnable接口,重写run()方法;
  • 实现Callable接口,重写run()方法,并且可以配合FutureTask类,拿到返回结果,可以处理异常;
  • 通过线程池的方式,给线程池提交任务;

1.1 实现多线程方式一:继承Thread类

可以创建一个类,使这个类继承Thread类,然后重写run()方法。
使用时,直接new出继承了Thread类的类对象,调用start()方法。

  1. package com.atguigu.myself.create;
  2. public class MyThreadTest {
  3. public static void main(String[] args) {
  4. MyThread myThread = new MyThread();
  5. myThread.run();
  6. System.out.println(Thread.currentThread().getName()+"正在运行");
  7. }
  8. }
  1. package com.atguigu.myself.create;
  2. public class MyThreadTest {
  3. public static void main(String[] args) {
  4. MyThread myThread1 = new MyThread();
  5. MyThread myThread2 = new MyThread();
  6. myThread1.start();
  7. myThread2.start();
  8. System.out.println(Thread.currentThread().getName()+"正在运行");
  9. }
  10. }

1.2 实现多线程方式二:实现Runnable接口

让一个类实现Runnable接口,重写run()方法。
实现了Runnable接口的类的实例对象,可以作为Thread类的带参构造器的参数,因此使用时,调用Thread类的有参构造器,参数传入实现了Runnable接口的类的实例对象,创建出Thread类对象后,还是通过Thread类对象的start()方法来调用执行。
当然,此时,也可以在创建Thread类对象时,以匿名内部类的方式,实现run()方法;

  1. package com.atguigu.myself.create;
  2. public class MyRunnable implements Runnable {
  3. @Override
  4. public void run() {
  5. for (int i = 0; i <= 100; i++) {
  6. System.out.println(Thread.currentThread().getName()+"打印的是:"+i);
  7. }
  8. }
  9. }
  1. package com.atguigu.myself.create;
  2. public class MyRunnableTest {
  3. public static void main(String[] args) {
  4. MyRunnable myRunnable = new MyRunnable();
  5. Thread thread1 = new Thread(myRunnable);
  6. thread1.start();
  7. new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. for (int i = 0; i <= 100; i++) {
  11. System.out.println(Thread.currentThread().getName()+"打印的是:"+i);
  12. }
  13. }
  14. }).start();
  15. System.out.println(Thread.currentThread().getName()+"正在运行");
  16. }
  17. }

1.3 run()与start()区别

我们通过继承Thread类或实现Runnable接口去实现多线程时,重写的是run()方法,启动线程时调用的是start()方法
重写run()方法是封装要被线程执行的代码逻辑,直接调用run()方法,只是普通的方法调用;而start()方法才是会先启动一个新的线程,然后再由JVM调用启动了的这个线程的run()方法。

1.4 继承Thread类和实现Runnable接口如何选择?

尽量选择实现Runnable接口的方式来实现多线程。java中类是单继承的,如果继承Thread类,会有单继承的限制;实现Runnable接口的方式天然就能体现共享数据的概念,假设三个窗口卖票,窗口类实现Runnable接口,里面定义了票数100,那么我们通过Thread的有参构造器创建三个线程,就相当于三个线程共享着窗口类中的数据。
实际上Thread类自己也实现了Runnable接口,它里面的run()方法就是重写了Runnable接口中的run()方法。

1.5 实现多线程方式三:实现Callable接口

我们可以让一个类实现Callable接口,然后重写run方法,以此来实现多线程。同时,这种方式可以配合FutureTask类使用,获取线程运行到返回结果以及处理异常。
使用时,先创建出实现了Callable接口的类对象,将此对象作为参数,构建出FutureTask类的对象;
FutureTask类有泛型,泛型传入线程逻辑代码执行的返回结果的类型
然后将FutureTask对象作为参数,通过Thread类的有参构造器创建出Thread类对象,调用start()方法。
可以通过FutureTask对象的get()方法获取返回结果,这个方法会阻塞等待整个线程执行完成来获取返回结果。

  1. package com.atguigu.myself.create;
  2. import java.util.concurrent.Callable;
  3. public class MyCallable implements Callable<Integer> {
  4. @Override
  5. public Integer call() throws Exception {
  6. System.out.println("当前线程:"+Thread.currentThread().getId());
  7. int i = 10/2;
  8. System.out.println("运行结果:"+i);
  9. return i;
  10. }
  11. }
  1. package com.atguigu.myself.create;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. public class MyCallableTest {
  5. public static void main(String[] args) throws ExecutionException, InterruptedException {
  6. MyCallable myCallable = new MyCallable();
  7. FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
  8. Thread thread0 = new Thread(futureTask);
  9. thread0.start();
  10. System.out.println("----------------------");
  11. FutureTask<Integer> futureTask2 = new FutureTask<>(new MyCallable());
  12. new Thread(futureTask2).start();
  13. Integer integer = futureTask.get();
  14. Integer integer2 = futureTask2.get();
  15. System.out.println("---integer:"+integer);
  16. System.out.println("---integer2:"+integer2);
  17. System.out.println(Thread.currentThread().getName()+"正在运行");
  18. }
  19. }

1.6 实现多线程方式四:线程池

实际开发中,用到多线程的地方,其实基本上都是使用线程池,将所有的多线程异步任务交给线程池执行。
这部分内容很多,后续新开篇幅来记录。

2、线程的生命周期

2.1 线程生命周期的状态

在Thread类中有个内部类State,描述了Java中线程的生命周期的六种状态:

  • NEW:新建状态(初始状态);此时线程被构建,当一个Thread类或其子类的对象被声明并创建时,新生的线程处于新建状态,但是还没有调用start()方法;
  • RUNNABLE:运行状态;Java线程将操作系统中就绪和运行两种状态笼统的称为运行状态,此时在JVM中执行的线程处于这个阶段;这个阶段又可细分为
    • READY:就绪状态;线程对象调用start()方法,等待JVM的调度,但此时还没有运行;
    • RUNNING:运行中,线程已获得JVM调度,处于运行中。
  • BLOCKED:阻塞状态;表示线程阻塞于锁;在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态;
  • WAITING:等待状态;表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做处一些特定动作(通知或中断);
  • TIMED_WAITING:超时等待状态;该状态不同于WAITING,它是可以在指定时间自行返回的;
  • TERMINATED:终止状态,表示当前线程已经执行完毕。

线程在生命周期中并不是固定处于某一个状态,而是随着代码的执行在不同状态之间切换。
而在此基础上,可以划分为以下五种状态:新建,就绪,运行,阻塞,死亡。
处于阻塞状态的线程,想要再次运行,需要先到就绪状态,再到运行状态。
实现多线程的方式及生命周期 - 图1

2.2 与线程声明周期相关的方法

2.2.1 sleep()方法

sleep()是Thread类下的方法;线程调用sleep()方法会进入超时等待状态,等设定好的时间到了,线程会进入就绪状态。(如果加了锁,sleep()方法不会释放锁。)
实现多线程的方式及生命周期 - 图2

2.2.2 yield()方法

yield()是Thread类下的方法;作用是暂停当前正在执行的线程对象,并执行其他线程,但是这个其他线程也包括他自己。yield()无法保证达到让步目的,因为让步的线程还有可能被线程调度程序再次选中,不确保真正让出;yield()只是让线程重新回到就绪状态,但是执行yield()的线程,可能刚回到就绪状态,又被马上选中运行。
实现多线程的方式及生命周期 - 图3

2.2.3 join()方法

join()方法是Thread类的方法;一个线程调用了join()方法,则会等待该线程执行完毕,才会执行别的线程。
join()方法有带参数的也有无参的,如果一个线程调用了无参的join()方法,那么其他线程会从运行状态进入等待状态,等调用join()方法的线程执行完毕了,回到就绪状态,获取执行权后执行;如果一个线程调用了有参的join()方法,参数是一个时间参数,那么其他线程会进入计时等待状态,等调用join()方法的线程执行完毕了再回到就绪状态,获取执行权后执行。

2.2.4 与阻塞状态相关的方法

首先,在Java中,阻塞状态与等待状态没有那么明细的区分,上述sleep()、yield()、join()方法使线程进入等待状态,也可看做一种阻塞。
严格上来说,阻塞状态与”锁”有关,关于锁后续来讲解。比如wait()方法,会使线程进入阻塞状态,但也可以说,是等待状态。