Java 多线程基本概念及创建线程

Java 多线程基本概念及创建线程


1 进程(process)与线程(Thread)

  • 程序:指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

  • 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。

  • 线程:一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。


2 Java 多线程编程核心概念

  • 单核 CPU 采用频繁切换线程的方法,给人一种多线程的感觉。

  • 多核 CPU 是真正的多线程,可以真正的并发执行多个线程。

  • 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

  • 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销,可以达到充分利用 CPU 的目的。

  • Java 给多线程编程提供了内置的支持。

  • Java 程序运行时中至少有2个线程并发,一个是主线程调用 main() 方法,一个是垃圾回收线程负责看护和回收垃圾。如果出现了运行时异常,还会多一个处理异常的线程。

  • 进程 A 与进程 B 的内存资源不共享。

  • Java 中线程 A 和线程 B 共享堆内存和方法区内存,但是栈内存独立,一个线程一个栈,互不干扰。


3 线程生命周期

在这里插入图片描述

  • 创建状态:线程对象刚刚 new 出来。

  • 就绪状态:

    • 线程对象调用了 start() 方法。
    • 就绪状态也称可运行状态,可以抢夺 CPU 时间片的权利( CPU 时间片就是执行权)。
    • 抢夺到 CPU 时间片后,就会调用 run() 方法。
  • 运行状态:

    • run() 方法开始执行,就标志着线程进入了运行状态。
    • 当占有的时间片用完后,会重新进入就绪状态,继续抢夺时间片。
    • 再次抢夺到时间片后,会重新进入 run() 方法,并接着上一次的代码继续往下执行。
  • 阻塞状态:

    • 遇到阻塞事件时,会进入阻塞状态,例如:用户输入、线程休眠等。
    • 进入阻塞状态的线程会释放当前占有的 CPU 时间片。
    • 阻塞解除后,会回到就绪状态,再次抢夺 CPU 时间片。
  • 死亡状态:

    • run() 方法执行完毕,进入死亡状态。

需要注意的是:

  • 线程对象每次抢夺到的时间片的长度是不一致的,所以在一些用循环演示多线程的实例中会出现每次执行的次数都不一样。

  • 我对 run() 方法的理解,分支线程中的 run() 方法类似于主线程中的 main() 方法,都是程序执行的入口。


4 创建线程

实现多线程的方式:

  • 继承 java.lang.Thread 类,重写 run() 方法,创建线程对象,使用 start() 方法启动线程。

  • 实现 java.lang.Runnable 接口,重写 run() 方法,将 java.lang.Runnable 实现类对象 r 传入 java.lang.Thread 类对象 t,用 t 调用 start() 方法启动线程。

  • 实现 java.lang.Callable 接口,重写 call 方法,将 java.util.concurrent.Callable 的实现类对象 c 传入 java.util.concurrent.FutureTask 构造器,将 java.util.concurrent.FutureTask 的实例化对象 f 传入 java.lang.Thread 构造器,创建线程对象 t。


4.1 继承 Thread 创建线程对象

Thread API 文档

步骤:

  • 定义线程类,该类需要继承 java.lang.Thread 类。

  • 重写 run() 方法,编写线程执行体。

  • 创建定义类对象,调用 start() 方法启动线程。

public class ThreadTest {
    //  主线程
    public static void main(String[] args) {
        //  创建线程对象
        MyThread myThread = new MyThread();
        //  直接调用 run() 方法
        //  这样不能实现迸发,没有开辟新的栈空间,相当于只是普通对象调用普通的方法。
        //myThread.run();

        //  启动线程,开启新的栈空间 start() 方法
        myThread.start();

        for (int i = 0; i < 1000; i++){
            System.out.println("<**************main*************>" + i);
        }
    }
}

// 定义线程类,继承 java.lang.Thread ,重写 run() 方法
class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            System.out.println("<-----------myThread----------->" + i);
        }
    }
}

运行结果:

<**************main*************>0
<**************main*************>1
<**************main*************>2
<**************main*************>3
<**************main*************>4
<**************main*************>5
<**************main*************>6
<-----------myThread----------->0
<-----------myThread----------->1
<**************main*************>7
...

注意:

  • 线程开启不一定立即执行,由 CPU 调度执行,也就是说谁抢到 CPU 时间片就执行谁。

  • 当线程对象调用 start() 方法后,会自动调用 run() 方法。

  • 分支线程中 run() 方法的地位等价于主线程中 main() 方法的地位,都是程序的入口。


4.2 实现 Runnable 接口创建线程对象

Runnable API 文档

步骤:

  • 定义线程类实现 java.lang.Runnable 接口。

  • 重写 run() 方法,编写线程执行体。

  • 创建定义类对象,并作为参数传入 java.lang.Thread 对象。

  • 使用 java.lang.Thread 对象调用 start() 方法启动线程。

public class RunnableTest {
    public static void main(String[] args) {
        //  创建 Runnable 实现类
        MyRunnable r = new MyRunnable();
        //  创建线程
        Thread t = new Thread(r);
        //  启动线程
        t.start();

        //  继续执行主线程
        for (int i = 0; i < 1000; i++){
            System.out.println("<*************main*************>" + i);
        }
        //  匿名内部类写法
        /*
        Thread t2 = new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++){
                    System.out.println("<-----------Runnable2----------->" + i);
                }
            }
        });
        t2.start();
         */
    }
}
//  创建类实现 Runnable 接口,重写 run() 方法
class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            System.out.println("<-----------myRunnable--------->" + i);
        }
    }
}

运行结果:

<*************main*************>31
<*************main*************>32
<-----------myRunnable----------->1
<*************main*************>33
<-----------myRunnable----------->2
<*************main*************>34
<-----------myRunnable----------->3
<*************main*************>35
<-----------myRunnable----------->4
<-----------myRunnable----------->5
<-----------myRunnable----------->6
<*************main*************>36
<-----------myRunnable----------->7
<-----------myRunnable----------->8
<*************main*************>37

注意:

  • java.lang.Runnable 只是一个接口,接口中只有一个 run() 方法。

  • 实现了 java.lang.Runnable 接口的类不具备多线程能力,只是一个可执行类。

  • 可以将 java.lang.Runnable 实现类的对象 r 传入 java.lang.Thread 创建线程对象 t。

  • 此时就具备多线程能力了,使用 t.start() 启动线程,开辟新的栈空间。

两种方式对比:

  • 继承 Thread

    • 子类继承 Thread 类具备多线程能力

    • 启动线程:子类对象.start()

    • 特点:单继承,不易于扩展,具有局限性。

  • 实现 Runnable 接口

    • 实现 Runnable 接口的类本身不具备多线程能力

    • 启动线程:使用 Thread 作代理启动,将 Runnable 接口实现类对象作为参数创建 Thread 类对象,然后调用 start() 方法启动线程

    • 特点:实现类可以实现多个接口,避免了单继承的局限性,方便灵活。


4.3 实现 Callable 接口创建线程对象

Callable API 文档

FutureTask API 文档

  • java.util.concurrent 包是 JDK8 的新特性,是线程并发包
  • FutureTask<V> 类是可以包装 CallableRunnable 的对象,实现了 Runnable 接口
  • 使用 Callable 接口,可以获取线程返回值,线程方法为:V call() throws Exception

步骤:

  • 定义线程类,实现 Callable 接口,重写 call 方法

  • 创建 FutureTask 对象,包装 Callable 实现类对象

  • 创建 Thread 对象,使 FutureTask 具备多线程能力

  • 启动多线程 start() 方法

public class CallableTest2 {
    public static void main(String[] args) throws Exception {
        // 1. 创建 Callable 实现对象
        MyCallable callable = new MyCallable();
        // 2. 创建未来任务类对象,接受 Callable 实现对象
        FutureTask<Integer> task = new FutureTask<>(callable);
        // 3. 创建线程对象,使 FutureTask 具备多线程能力
        Thread t = new Thread(task);
        // 4. 启动线程
        t.start();
        // 5. 在 main 线程中获取 t 线程的执行结果
        Object obj = task.get();
        System.out.println(obj);
    }
}

// 实现 Callable<Integer> 接口,重写 call 方法
class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println("call method begin...");
        // 3 秒后获取结果
        try {
            Thread.sleep(1000 * 3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int a = 100;
        int b = 200;
        System.out.println("call method end...");
        return a + b;  // 会自动装箱 int -> Integer
    }
}
  • 前两种多线程方式重写的是 public abstract void run() 没有返回值。

  • 这种多线程方式重写的是 V call() throws Exception 方法,返回值为泛型。

  • 在 a 线程中调用 b 线程调用 get() 方法会使 a 线程进入阻塞状态,直到 b 线程执行完毕并返回结果后,a 线程回到就绪状态。

  • 这种多线程实现方式唯一好处就是可以返回线程执行结果。

  • 由于会使获取结果的线程进入阻塞状态,所以降低了效率。


参考文章:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值