线程详解

2021/7/2 23:24:07

本文主要是介绍线程详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

 

 

线程概述

运行一个音乐播放器播放一首歌,音乐播放器就是一个进程,在程序执行时,既有声音的输出,同时还有该歌曲的字幕展示,这就是进程中的两个线程

线程与进程

程序进入内存就变成了进程,进程就是处于运行中的程序

进程特征:

  • 独立性:每个进程都有自己的私有地址,一个进程不能直接访问其他进程
  • 动态性:进程有自己的生命周期和不同状态,而程序不具备
  • 并发性:多个进程可以在单个处理器上并发执行,进程之间互不影响

并发: 进程在cpu中切换执行  并行:进程在cpu上一起执行

对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,
CPU不断地在这些进程之间轮换执行,虽然CPU在多个进程间轮换执行,但是我们感觉到好像有多个进程在同时进行

线程是进程的执行单元,对于绝大多数的应用程序来说,通常仅要求有一个主线程,
但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程(子线程),
每个线程也是相互独立的

线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,
但不拥有系统资源,它与父进程的其他线程共享该进程所有拥有的全部资源

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。


 

 

多线程的优势

  • 进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其他的每个线程的状态
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,提高运行效率
  • 线程共享的环境包括:进程代码段、进程的公有数据
  • 进程之间不能共享内存,但线程之间共享内存非常容易
  • 系统创建进程是需要为该进程重新分配系统资源,但创建线程则代价小得多

 

线程的创建与启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

每个线程的作用是完成一定的任务,实际上就是执行一段程序代码。

Java使用线程执行体来代表这段程序代码。

Ø  继承Thread创建线程

  1. 定义Thread类的子类,并重写该类的run()方法,run()方法的方法体代表线程需要完成的任务。因此把run方法称为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程。
public class Test2 extends Thread{

    // 重写run方法,run方法的方法体就是子线程的执行体
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 继承Thread类后,从父类继承的getName方法可以获取当前线程的名称
            System.out.println("线程名称:"+this.getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            // 调用Thread的currentThread()方法获取当前线程对象
            // 这里就不能用this来获取name了
            System.out.println("线程名称;"+Thread.currentThread().getName()+"="+i);

            //创建两个子线程,并运行
            if (i == 20){
                new Test2().start();
                new Test2().start();
            }
        }

    }
}

线程是以抢占式的方式运行的,虽然只创建了两个线程实例,实际上有三个线程在运行
(两个子线程,一个主线程main)

通过setName(String name)的方式来为线程设置名称,也可以通过getName的方式来得到线程的名称。
在默认情况下,主线程的名称为main,用户启动的多线程的名称依次为Thread-0,Thread-1,Thread-3..Thread-n

 

 

实现Runnable接口创建线程

  1. 定义Runnable接口的实现类,并重写该接口的run方法
  2. 创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。

  3. 调用线程对象的start()方法来启动该线程
public class Test3 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i < 100;i++) {
            // 实现了Runnable接口的类其本质并不是线程类,因此没有getName方法,
            // 因此需要通过Thread类来获取当前线程,仅仅是一个任务体,仍需交给Thread去执行
            System.out.println("线程名称:"+Thread.currentThread().getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        /*new一个实现了Runnable接口的实例,这个实例不是线程对象
        * 不能test3.start()来运行子线程,执行run方法体
        * 实际的线程对象要通过new Thread()来获取,只不过对于实现了Runnable接口的实现类的实例
        *   作为参数传入到new Thread(test3).start()来执行子线程
        *   意义是让线程对象来执行test3实例的run方法体
        * */
        Test3 test3 = new Test3();
        new Thread(test3).start();
        new Thread(test3).start();
    }

}

 

又因为Runnable是一个函数式接口,所以可以使用lamda表达式来进行代码编写

public class Test4 {




    public static void main(String[] args) {

        /*
        * 用lamda表达式的写法,{}里面写的就是run方法体
        * 将runnable传入到 new Thread(runnable,"子线程1")里面,就表示了创建了子线程
        *   并执行run方法体,第二个参数是为子线程起名字
        * */
        Runnable runnable = ()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("线程名字:"+Thread.currentThread().getName()+"="+i);
            }
        };

        Test4 test4 = new Test4();
        new Thread(runnable,"子线程1").start();
        new Thread(runnable,"子线程2").start();
    }

}

 

 

通过对比上面两种创建线程的方式,继承Thread 和 实现Runnable接口,第一种主线程和子线程

分别执行一遍任务。第二种主线程和子线程共同完成一个任务。

 

Ø  使用Callable&Future创建线程

在Java 1.5开始,Java提供了Callable接口,该接口实际上可看成是Runnable接口的增强版,Callable接口提供了一个call()的方法可以作为线程的执行体,但call()方法比run()方法功能更加强大。

这是因为:

1. call()方法可以有返回值

2. call()方法可以声明抛出异常

因此我们可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。但是存在以下两个问题:

1. Callable接口是Java 5提供的一个新的接口而不是Runnable接口的子接口,所以Callable对象不能直接作为Thread类的target目标执行类。

2. call()方法还有一个返回值——call()方法并不是直接调用,它是作为线程执行体被调用的。如何获取call()方法的返回值?

为了解决以上两个问题,Java 1.5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runable接口——可以作为Thread类的target。

 

 

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

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且该call()方法有返回值
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。

  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程
  4. 调用FutureTask对象的get()方法来获得子线程结束后的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 实现Callable接口时指定的泛型为返回值的类型
 */
public class Test5 implements Callable<Integer> {

    /*
    * 对于实现了Callable接口的类,重写的call方法就是子线程要执行的方法体
    *   这个call方法与run方法的区别就是,call有返回值,可以声明抛出异常
    *
    * 实现的Callable接口可以看做是Runnable接口的增强版,所以可以提供一个Callable对象作为target传给线程对象
    *   但是问题就是,Callable接口不是Runnable接口的子接口,不能直接作为target
    *   call方法有返回值
    *
    * */
    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 100; i++) {
            /*与实现了Runnable接口的run方法相似,也是不能使用this来获取name*/
            System.out.println("当前线程名称:"+Thread.currentThread().getName()+" "+i);
            Thread.sleep(200);
        }
        return 100;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*创建Callable对象,因为当前类实现了Callable接口  多态*/
        Callable<Integer> callable = new Test5();
        // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        /*创建线程对象*/
        new Thread(futureTask).start();
        // 获取线程结束后的返回值
        System.out.println("线程执行结束后的返回值:"+futureTask.get());

    }
}

 

Callable接口是一个函数式接口,所以可以用lamda表达式写法

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test6 {

    public static void main(String[] args) throws Exception {
        /*
        * Callable接口也是一个函数式接口,所以可以用lamda表达式写法
        * {} 里面写的就是call的方法体
        * */
        Callable<Integer> callable = ()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("线程名称:"+Thread.currentThread().getName()+"="+i);
            }
            return 100;
        };

        // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        /*创建线程对象  并将FutureTask封装好的Callable对象传入线程对象中*/
        new Thread(futureTask).start();

        // 获取线程结束后的返回值
        System.out.println("线程执行结束后的返回值:"+futureTask.get());

    }

}

 

 

Ø  创建线程的三种方式比较

通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常,并且Callable需要FutureTask来进行封装成Thread可识别的target目标。因此可以将实现Runnable接口和实现Callable接口归纳为一种方式。这种方式与继承Thread方式之间的主要差别如下

 

 

线程的声明周期

当线程被创建并启动后,并不是一启动就进入了执行状态,也不是一直处于执行状态,

在线程的生命周期中,它要经历新建、就绪、运行、阻塞和死亡5种状态。

尤其是当线程启动以后,它不可能一直占用CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会在运行、阻塞之间切换。

 

新建状态:当new了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样仅仅由Java虚拟机为其分配内存,并初始化其他成员变量的值

就绪状态:当线程对象调用了start方法之后,该线程就处于就绪状态,Java虚拟机会为这个线程对象创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于什么时候开始运行,则取决于JVM里的线程调度器的调度。

运行状态:处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程就处于运行状态

阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
  • 线程在等待某个通知(notify)

  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

死亡状态

  • run()或call()方法执行完成,线程正常结束
  • 线程抛出一个未捕获的Exception或者直接Error错误
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动之后,它就拥有和主线程相同的地位

不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程不可以再次作为线程执行。

 

控制线程

 



这篇关于线程详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程