一、线程的创建和使用

1. 方式一:继承Thread类创建多线程

1.1 创建步骤

  • 创建一个继承于Thread类的子类
  • 重写Thread类的run()方法,将此线程要完成的操作写在run()方法中
  • 创建Thread类的子类对象
  • 通过此对象调用start()方法

    1. public class Test extends Thread {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 100; i++) {
    5. if(i % 2 == 0){
    6. System.out.println(i + " 子线程");
    7. }
    8. }
    9. }
    10. public static void main(String[] args) {
    11. Test p = new Test();//创建Thread子类对象
    12. p.start();//启动子线程,此时main方法主线程和子线程并发执行
    13. for (int i = 0; i < 100; i++) {
    14. if(i % 2 != 0){
    15. System.out.println(i + " main方法");
    16. }
    17. }
    18. }
    19. }

    1.2 Thread类的特性

    JVM允许程序运行多个线程,通过java.lang.Thread类体现,所有的线程对象都必须是Thread类或其子类的实例。

  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,run()方法的方法体就代表了线程需要完成的任务,常把run()方法称为线程执行体

  • 通过该Thread对象的start()方法来启动线程,而不是直接调用run()方法。
  • start()方法有两个作用:1.启动当前线程。2.调用当前线程的run()方法。
  • main()方法是Java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体。

    1.3 创建线程的两个问题

  • 直接调用run()方法不会创建一个新的线程,只是相当于调用了一个普通方法,想要创建一个新线程来调用这个方法,只能使用start()。

  • 如果想要再创建一个子线程,已经调用start()的对象不能再次调用start()方法,会报IllegalThreadStateException异常。一个对象只能使用一次start()方法,要想再创建一个线程,只能再创建一个对象。
  • 使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

    1. public class Test extends Thread {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 100; i++) {
    5. if(i % 2 == 0){
    6. System.out.println(i + " " + getName());
    7. }
    8. }
    9. }
    10. public static void main(String[] args) {
    11. Test p = new Test();//创建Thread子类对象
    12. p.start();//启动子线程,此时main方法主线程和子线程并发执行
    13. //p.start();//IllegalThreadStateException异常
    14. Test p1 = new Test();
    15. p1.start();//重新创建一个线程只能再创建一个对象
    16. for (int i = 0; i < 100; i++) {
    17. if(i % 2 != 0){
    18. System.out.println(i + " " + Thread.currentThread().getName());
    19. }
    20. }
    21. }
    22. }

    上面程序使用到了如下两个方法:

  • Thread.currentThread()是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。

  • getName():该方法是Thread的实例方法,该方法返回调用该方法的线程名字。
  • 程序可以通过setName(String name)方法为线程设置名字,在默认情况下,主线程的名字为main,用户启动的多个线程的名字依次为Thread-0、Thread-1、Thread-2等。

    1.4 继承Thread类创建多线程练习

    练习:创建两个分线程,一个遍历100以内的偶数,另一个遍历100以内的奇数。

    1. //方式一:分别创建两个不同的Thread子类。
    2. public class Test {
    3. public static void main(String[] args) {
    4. new Thread1().start();
    5. new Thread2().start();
    6. }
    7. }
    8. class Thread1 extends Thread{
    9. @Override
    10. public void run() {
    11. for (int i = 0; i < 100; i++) {
    12. if(i % 2 == 0){
    13. System.out.println(Thread.currentThread().getName() + " " + i);
    14. }
    15. }
    16. }
    17. }
    18. class Thread2 extends Thread{
    19. @Override
    20. public void run() {
    21. for (int i = 0; i < 100; i++) {
    22. if(i % 2 != 0){
    23. System.out.println(Thread.currentThread().getName() + " " + i);
    24. }
    25. }
    26. }
    27. }
    1. //方式二:使用匿名内部类简化,在实际开发中,如果一个线程只需使用一次,
    2. //则用这种方法,把上面四个步骤合并在一起。
    3. public class Test {
    4. public static void main(String[] args) {
    5. new Thread(){
    6. @Override
    7. public void run() {
    8. for (int i = 0; i < 100; i++) {
    9. if(i % 2 == 0){
    10. System.out.println(Thread.currentThread().getName() + " " + i);
    11. }
    12. }
    13. }
    14. }.start();
    15. new Thread(){
    16. @Override
    17. public void run() {
    18. for (int i = 0; i < 100; i++) {
    19. if(i % 2 != 0){
    20. System.out.println(Thread.currentThread().getName() + " " + i);
    21. }
    22. }
    23. }
    24. }.start();
    25. }
    26. }

    1.5 Thread类的常用方法

    | void start() | 启动线程,并执行对象的run()方法 | | —- | —- | | run() | 通常需要重写Thread类的此方法,将线程在被调度时要执行的操作声明在此方法中 | | String getName() | 返回线程名称 | | void setName(String name) | 设置线程名称 | | static Thread currentThread() | Thread的静态方法,返回当前线程。在Thread子类中就是this,通常用于主线程和Runable实现类 | | static void yield() | 线程让步:暂停当前正在执行的线程,把执行机会释放给优先级相同或更高的线程,如果队列中没有同优先级的进程则忽略此方法。(机会释放后不是说一定会执行其他线程,可能cpu在调度时又分配给这个线程执行) | | join() | 在当前线程中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完毕为止。这样,优先级低的线程也能先执行,并且抛出InterruptedException异常。 | | static void sleep(long millis) | 令当前线程在指定时间内放弃对cpu的控制,使其他线程有机会被执行,时间到后重新排队,并且抛出InterruptedException异常。 | | stop() | 强制线程生命周期结束,不推荐使用 | | boolean isAlive() | 判断线程是否还活着 |

  1. public class Test {
  2. public static void main(String[] args) {
  3. MyThread p = new MyThread("线程一");
  4. //p.setName("线程一");
  5. p.start();
  6. //给主线程命名
  7. Thread.currentThread().setName("主线程");
  8. for (int i = 0; i < 10; i++) {
  9. System.out.println(Thread.currentThread().getName() + ":" + i);
  10. //i为3后,插入子线程,直到子线程执行完
  11. if(i == 3){
  12. try {
  13. p.join();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. //判断子线程是否还存活,因为上面使用join加入子线程,此时子线程已经结束
  20. System.out.println(p.isAlive());
  21. }
  22. }
  23. class MyThread extends Thread{
  24. //使用构造器设置线程名
  25. public MyThread(String name){
  26. super(name);
  27. }
  28. @Override
  29. public void run() {
  30. for (int i = 0; i < 10; i++) {
  31. try {
  32. sleep(1000);//阻塞一秒
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. System.out.println(getName() + ":" + i);
  37. //当i大于1时,释放当前线程的cpu执行权
  38. if(i > 1){
  39. this.yield();//this可省略
  40. }
  41. }
  42. }
  43. }

2.方式二:实现Runable接口创建多线程

2.1 创建步骤

  • 创建一个实现了Runable接口的类
  • 实现类去实现Runable接口中的抽象run()方法
  • 创建Runable实现类的的对象
  • 将此对象作为参数传入到Thread类的构造器中,创建Thread类的对象
  • 通过Thread类的对象调用start()方法启动该线程

    1. class MyThread implements Runnable{
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 10; i++) {
    5. System.out.println(i);
    6. }
    7. }
    8. }
    9. public class Test {
    10. public static void main(String[] args) {
    11. MyThread myThread = new MyThread();
    12. Thread p = new Thread(myThread);//调用Thread类的含Runable对象的构造器
    13. p.setName("线程一");
    14. p.start();
    15. //再启动一个线程,可以在创建Thread对象时为该对象指定一个名字
    16. Thread p1 = new Thread(myThread,"线程二");
    17. p1.start();
    18. }
    19. }

    2.2 两种方式的比较

  • Runable接口中只包含一个run()抽象方法。

  • 当线程类实现Runable接口时,如果想获取当前线程,只能使用Thread.currentThread()方法;而继承方式中获得当前线程对象直接使用this即可。
  • 采用Runable接口的方式创建的多个线程可以共享实现类的实例变量,即上面的线程一和线程二共享同一个Runable对象,Runable对象里定义的实例变量可以被这两个变量共享。
  • 开发中,优先选择第二种方式,原因:
  • 1)实现的方式没有单继承的局限性(如果子类又要用多线程,又要继承其他父类,但因为java单继承的限制,无法达到这一目的);
  • 2)实现的方式更适合处理多个线程共享数据的情况。
  • 相同点:两种方式都要重写run()方法。

    1. class MyThread implements Runnable{
    2. private int i;//该变量可以被多个线程共享
    3. @Override
    4. public void run() {
    5. for ( ; i < 10; i++) {
    6. System.out.println(Thread.currentThread().getName() + ":" + i);
    7. }
    8. }
    9. }
    10. public class Test {
    11. public static void main(String[] args) {
    12. MyThread myThread = new MyThread();
    13. Thread p = new Thread(myThread);
    14. p.setName("线程一");
    15. p.start();
    16. //再启动一个线程,可以在创建Thread对象时为该对象指定一个名字
    17. Thread p1 = new Thread(myThread,"线程二");
    18. p1.start();
    19. }
    20. }

    3.方式三:使用Callable和Future创建线程

    3.1 Callable接口简介

    Callable接口是Runable接口的增强版,其提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更加强大。

  • call()方法可以有返回值。

  • call()方法可以声明抛出异常,被外面的操作捕获,获取异常信息;而run()方法只能使用try…catch…,因为Thread类中的run()方法没有抛出异常,继承Thread类的子类也不能抛出异常。
  • 支持泛型的返回值。

    3.2 创建步骤

    创建并启动有返回值的线程的步骤如下:

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且有返回值,再创建Callable实现类的实例。

  • 将此Callable实现类对象作为参数传递到FutureTask的构造器中,创建FutureTask对象。
  • 使用FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,即Callable中call()方法的返回值。

Future接口:

  • 可以对具体的Runable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask类是Future接口的唯一实现类。
  • FutureTask接口同时实现了Runable、Future接口,它既可以作为Runable被线程执行,又可以作为Future得到Callable的返回值。

    1. class MyThread implements Callable {
    2. @Override
    3. //重写call()方法,该方法有返回值,且是线程执行体
    4. public Object call() throws Exception {
    5. int sum = 0;
    6. for (int i = 0; i < 10; i++) {
    7. sum += i;
    8. System.out.println(i);
    9. }
    10. return sum;
    11. }
    12. }
    13. public class Test {
    14. public static void main(String[] args) {
    15. MyThread myThread = new MyThread();//创建Callable对象
    16. FutureTask futureTask = new FutureTask(myThread);//将Callable对象传入到FutureTask构造器中
    17. new Thread(futureTask).start();//把FutureTask对象作为参数传入到Thread构造器中,启动子线程
    18. try {
    19. //get()方法的返回值即为Callable实现类中重写的call()方法的返回值
    20. Object sum = futureTask.get();//该方法会抛出异常需要处理
    21. System.out.println("总和为" + sum);//打印输出该返回值
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. } catch (ExecutionException e) {
    25. e.printStackTrace();
    26. }
    27. }
    28. }

    二、线程的生命周期

    1.线程的5种状态

    在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Runtime)、阻塞(Blocked)和死亡(Dead)5种状态。

  • 新建:当程序中使用new关键字创建一个Thread类或其子类对象时,该线程就处于新建状态。

  • 就绪:处于新建状态的线程调用start()方法后,将进入等待队列等待cpu执行,此时已具备运行条件,但该线程并未真正执行,还要等待分配cpu。所以,不要觉得调用start()方法后,线程就马上开始执行了。
  • 运行:当就绪的线程被调度并获得cpu时,线程进入运行状态。
  • 阻塞:在某些特殊情况下,线程在运行过程中被中断,放弃cpu并停止执行,线程进入阻塞状态。
  • 死亡:线程完成了它的全部工作或被强制中止或异常结束,线程死亡。

    2.线程阻塞和死亡的原因

    2.1 线程阻塞

  • 线程调用sleep()方法主动放弃所占用的处理器。

  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有。
  • 线程等待某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,尽量避免使用此方法。

被阻塞的线程在合适的时候会重新进入就绪状态等待cpu调度。如:

  • 调用sleep()方法的线程经过了指定的时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待的通知发出。
  • 处于挂起状态的线程被调用了resume()恢复方法。

    2.2 线程死亡

  • run()方法或call()方法执行完成,线程正常结束。

  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法结束该线程,该方法容易导致死锁,不推荐使用。

为了测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,返回true;当线程处于新建、死亡两种状态时,返回false。

2.3 start()方法注意事项

  • 调用start()方法后,只是表示线程可以开始运行,但并不是马上就被执行,还要等待调度。
  • 只能对新建状态的线程调用start()方法,调用了线程的run()方法后,该线程将不再处于新建状态,不要再次调用该线程的start()方法,否则将引发IllegalThreadStateException异常。
  • 如果希望调用start()方法后线程立即开始执行,程序可以使用Thread.sleep(1)让当前线程睡眠1毫秒,cpu就会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。

    3.线程状态切换图

    image.png