关于Java多线程

语言特性

Posted by YD Blog on February 20, 2023

关于Java多线程

线程与进程

  • 进程和线程基本概念
    • 进程:进程是操作系统资源分配的基本实体
    • 线程:线程是CPU调度和分配的基本单位
  • 进程和线程的关系
    • 一个线程只能属于一个进程,但是一个进程可以有多个线程(至少一个线程),一个线程的进程叫做单线程进程,多个线程的进程叫做多线程进程
    • 资源分配给进程之后,进程内部的线程都可以共享该进程的资源
    • 在处理机上运行的是线程
    • 线程在执行的过程中需要协作同步,不同进程的线程需要利用消息通信来实现同步
  • 进程和线程的区别
    • 基本区别:进程是操作系统分配资源的基本实体,线程是CPU调度的基本单位
    • 开销方面:每个进程都有自己独立的代码和数据空间,因此进程之间的切换会有较大的开销。但是线程在进程的地址空间内部运行,因此同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,因此线程之间的切换开销小。
    • 所处环境:在操作系统中能同时运行多个进程,在同一个进程中有多个线程同时执行
    • 内存分配:系统在运行的时候会给每个进程分配不同的内存空间,但是不会给线程分配,线程使用的资源均来自于进程
    • 包含关系:线程是进程的一部分,没有线程的进程叫做单线程进程,有多个线程的进程叫做多线程进程

Java与多线程的基本关系

  • Java程序天生就是多线程程序,我们可以通过JMX来看一下一个普通的Java程序有哪些线程,代码如下:
public class MultiThread {
	public static void main(String[] args) {
		// 获取 Java 线程管理 MXBean
		ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
		// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
		ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
		// 遍历线程信息,仅打印线程 ID 和线程名称信息
		for (ThreadInfo threadInfo : threadInfos) {
			System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
		}
	}
}
  • 上述程序的输出如下:
[6] Monitor Ctrl-Break //监听线程转储或“线程堆栈跟踪”的线程
[5] Attach Listener //负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //在垃圾收集前,调用对象 finalize 方法的线程
[2] Reference Handler //用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收的线程
[1] main //main 线程,程序入口
  • 能够得出结论:一个 Java 程序的运行是 main 线程和多个其他线程同时运行

如何实现多线程编程

  • 若想在Java中实现多线程的定义,那么就需要实现一个专门的线程主体类进行线程执行任务的定义,这个主题类是需要实现特定的接口或继承特定的父类进行实现

通过继承 Thread 类实现多线程

  • Java提供有一个java.lang.Thread类作为线程主体类的父类,一个类只要继承了此类就表示这个类是一个线程主体类了,但不是说这个类就可以直接实现多线程处理了,因为还需要覆写Thread类中的run方法作为多线程的主方法。
/**
 * 一个类继承Thread类即为线程主体类
 * 需要覆写run方法作为线程的主方法
 */
public class ThreadDemo extends Thread{

    private String title;

    public ThreadDemo(String title) {
        this.title = title;
    }

    @Override
    public void run() {
        for (int i = 0;i<10;i++){
            System.out.println(title + "执行:i=" + i);
        }
    }
}

编写测试:

/**
 * 多线程编程测试
 */
public class ThreadDemoTest {
    @Test
    public void threadTest() {
        ThreadDemo threadDemo0 = new ThreadDemo("A");
        ThreadDemo threadDemo1 = new ThreadDemo("B");
        ThreadDemo threadDemo2 = new ThreadDemo("C");
        ThreadDemo threadDemo3 = new ThreadDemo("D");

        threadDemo0.start();
        threadDemo1.start();
        threadDemo2.start();
        threadDemo3.start();
    }
}
  • 运行后可发现输出明显为异步运行,即多线程
  • 可以发现调用的是start方法而不是run,这是由于本质上run方法只是一个普通方法,需要使用start方法调用系统的线程管理算法分配资源来异步的运行run方法
  • 任何情况下,只要手动定义了多线程覆写run方法,就需要使用start方法来启动。

可以查看start的源代码来简单解析一下这个问题:

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         * 此方法不会为VM创建/设置的主方法线程或“系统”组线程调用。
         * 将来添加到此方法的任何新功能也可能必须添加到VM中。
         *
         * A zero status value corresponds to state "NEW".
         * 零状态值对应于状态“NEW”。
         * 
         * threadStatus这个变量为0则表示这个线程主体类未运行过
         * 下面这个语句是用来判断这个线程任务是否正在运行或者已经运行完成
         * 如果未运行则运行,否则抛出IllegalThreadStateException异常
         * IllegalThreadStateException指非法线程状态异常,是RuntimeException的子类,故不会被方法抛出
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. 
         *
         * 通知组此线程即将启动,以便将其添加到组的线程列表中,并减少组的未启动计数。
         */
        group.add(this);

        boolean started = false;
        try {
            start0();//调用start0方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack 
                  什么也不做。如果start0抛出了一个Throwable,那么它将被传递到调用堆栈
                */
            }
        }
    }

    private native void start0();
    /* native表示这里是本地操作系统的调用,此方法由JVM实现
    */

基于 Runnable 接口实现多线程

  • 实现Runnable接口的run方法
  • Runnable接口没有start方法,使用时应该调用Thread类的构造方法,把实现Runnable接口的run方法的对象传入,再调用start方法
public class RunnableDemo implements Runnable{

    private String title;

    public RunnableDemo(String title) {
        this.title = title;
    }

    @Override
    public void run() {
        for (int i = 0;i<10;i++){
            System.out.println(title + "执行:i=" + i);
        }
    }
}

public class RunnableDemoTest {
    @Test
    public void runTest() {
        RunnableDemo runnableDemo0 = new RunnableDemo("A");
        RunnableDemo runnableDemo1 = new RunnableDemo("B");
        RunnableDemo runnableDemo2 = new RunnableDemo("C");

        Thread thread0 = new Thread(runnableDemo0);
        Thread thread1 = new Thread(runnableDemo1);
        Thread thread2 = new Thread(runnableDemo2);

        thread0.start();
        thread1.start();
        thread2.start();
    }
}
  • 由于只是实现了Runnable接口对象,所以此时线程主体类上就不再有单继承局限,故这样的设计才是一个标准型的设计
  • 由于JDK1.8开始,Runnable接口使用了函数式接口的定义,所以也可以直接利用Lambda表达式实现多线程定义
@Test
    public void lambdaRunTest() {
        for (int x = 0; x < 3;x ++) {
            String title = "线程" + x + "执行:";
            Runnable run = () -> {
                for (int y = 0; y < 10; y++) {
                    System.out.println(title + y);
                }
            };
            new Thread(run).start();
        }
    }

当然上面的代码还可以更简化,比如:

    @Test
    public void lambdaRunTest() {
        for (int x = 0; x < 3;x ++) {
            String title = "线程" + x + "执行:";
            new Thread(() -> {
                for (int y = 0; y < 10; y++) {
                    System.out.println(title + y);
                }}).start();
        }
    }
  • 故对于多线程的实现,有限考虑Runnable接口实现,且永远通过Thread的start方法启动

Thread 和 Runnable 的关系

  • 经过一系列的分析之后可以发现,在多线程的实现过程之中已经有了两种做法:Thread 类、Runnable 接口,如果从代码的结构本身来讲肯定使用 Runnable 是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充
  • 但是从结构上也需要来观察 Thread 与 Runnable 的联系
  • 打开Thread类的定义: public class Thread implements Runnable 可以发现Thread也是继承了Runnable接口的子类,那么在之前继承 Thread 类的时候实际上覆写的还是Runnable接口的run方法
  • 多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理
  • 在进行Thread启动多线程的时候调用的是start方法,而后找到的是run方法
  • 但通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start方法执行的时候会调用Thread类中的run方法,而这个run方法去调用Runnable接口子类被覆写过run方法

多线程开发

  • 按照面向对象思想,应当将继承了Runnable接口的子类抽象为资源,而将多个Thread对象抽象为线程来访问同一个资源,类似如下程序:
public class ShopDemo implements Runnable{

    private int item;

    public ShopDemo(int item) {
        this.item = item;
    }

    @Override
    public void run() {
        for ( int i = 0; i < 100; i++) {
            if ( this.item > 0) {
                System.out.println("商品被购买,还剩" + --item + "件");
            } else {
                System.out.println("商品已售尽,请下次再来");
            }
        }
    }
}

public class ShopDemoTest {
    @Test
    public void shoppingTest() {
        ShopDemo shopDemo = new ShopDemo(200);

        new Thread(shopDemo).start();
        new Thread(shopDemo).start();
        new Thread(shopDemo).start();
    }
}
  • 如上就是模拟抢购时多方顾客购买同一款商品时的操作

Callable 实现多线程开发

  • 从最传统的开发来讲如果要进行多线程的实现肯定依靠的就是Runnable
  • 但是Runnable接口有一个缺点: 当线程执行完毕之后后无法获取一个返回值,所以从JDK1.5之后就提出了一个新的线程实现接口: java.util.concurrent.Callable接口
  • Callable接口提供了一个有返回值的call方法作为线程执行的方法,这个返回值的类型为泛型
  • 另外,Callable接口也需要一个线程主体类来执行call方法,就像Thread和Runnable的关系一样
  • 这个线程主体类就是FutureTask
public class CallableDemo implements Callable<String> {

    private String title;

    public CallableDemo(String title) {
        this.title = title;
    }

    @Override
    public String call() throws Exception {
        for (int i = 0;i<10;i++){
            System.out.println(title + "执行:i=" + i);
        }
        return title+"执行完了";
    }
}

public class CallableDemoTest {
    @Test
    public void callTest() throws ExecutionException, InterruptedException {
      // Callable实体类
        CallableDemo callableDemo0 = new CallableDemo("A");
        CallableDemo callableDemo1 = new CallableDemo("B");
        CallableDemo callableDemo2 = new CallableDemo("C");

      // 初始化FutureTask对象用于接收返回值
        FutureTask<String> futureTask0 = new FutureTask<>(callableDemo0);
        FutureTask<String> futureTask1 = new FutureTask<>(callableDemo1);
        FutureTask<String> futureTask2 = new FutureTask<>(callableDemo2);

      // 使用Thread的start方法运行多线程
        new Thread(futureTask0).start();
        new Thread(futureTask1).start();
        new Thread(futureTask2).start();

      // 使用FutureTask对象的get方法获取返回值
        System.out.println(futureTask0.get());
        System.out.println(futureTask1.get());
        System.out.println(futureTask2.get());
    }
}

Runnable 与 Callable 的区别

  1. Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的
  2. java.lang.Runnable接口之中只提供有一个run()方法,并且没有返回值
  3. java.util.concurrent.Callable接口提供有call()方法,可以有返回值

线程运行状态

  • 对于多线程的开发而言,编写程序的过程之中总是按照: 定义线程主体类,而后通过Thread类进行线程,但是并不意味着你调用了start方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行的状态
    1. 任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start,但是启动的时候实际上若干个线程都将进入到一种就绪状态,现在并没有执行
    2. 进入到就绪状态之后就需要等待进行资源的调度,当某一个线程调度成功之后侧进入到运行状态(执行run方法),但是所有的线程不可能一直持续执行下去,中间需要产生一些暂停的状态,例如:某个线程执行一段时间之后就需要让出资源;而后这个线程就进入到阻塞状态随后重新回归到就绪状态
    3. run方法执行完毕之后,实际上该线程的主要任务也就结束了,那么此时就可以直接进入到停止状态

线程的命名和取得

  • 多线程的运行状态是不确定的,那么在程序开发之中为了可以获取到一些需要使用的线程就只能依靠线程的名字来进行操作
  • 所以线程的名字是一个至关重要的概念,这样在Thread类之中就提供有线程名称的处理
    • 构造方法:public Thread(Runnable target,String name)
    • 设置名字:public final void setName(String name)
    • 取得名字:public final String getName()
  • 对于线程对象的获得是不可能只靠一个this来完成的,因为线程的状态不可控,但是有一点是明确的,所有的线程对象都一定要执行run方法,那么这个时候可以考虑获取当前线程,在Thread类里面提供有获取当前线程的一个方法
    • public static native Thread currentThread().getName()使用这个方法即可
  • 值得一提的是即使没有手动为线程命名,它也会有一个默认名字
  • 主方法也是一个线程
  • 在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交给子线程处理

线程的休眠

  • 如果希望某一个线程可以暂缓执行,那么可以使用休眠的处理。
  • 在Thread类中定义的休眠的方法如下:
    • 休眠1:public static void sleep(long millis)throws InterruptedException
    • 休眠2:public static void sleep(long mills,int nanos)throwsInterruptedException
  • 在进行休眠的时候有可能会产生中断异常InterruptedException,中断异常属于Exception的子类,所以证明该异常必须进行休眠处理
  • 休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在你有多个线程对象,那么休眠也是有先后顺序的,但是由于处理的时间非常短所以感觉上就是若干个线程一起进行了休眠然后一起进行了自动唤醒

线程的中断

  • 在之前发现线程的休眠里面提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其他线程完成的
  • 在Thread类里面提供有这种中断执行的处理方法:
    • 判断线程是否被:public boolean isInterrupted()
    • 中断线程执行:public void interrupt()
  • 所有正在执行的线程都是可以被中断的中断线程必须进行异常的处理

线程的强制执行

  • 线程的强制执行指的是当满足于某些条件之后,某一个线程对象可以一直独占资源一直到该线程的程序执行结束
  • 线程的强制执行方法:public final void join() throws InterruptedException
  • 在进行线程强制执行的时候一定要获取强制执行对象之后才可以执行jion()调用