基于Java的多线程的创建

简介: 基于Java的多线程的创建

在Java中使用多线程非常简单,我们先学习如何创建和使用线程,然后结合案例再深入剖析线程的特性。

一、Thread 类介绍

该如何创建线程呢?通过API中搜索,查到Thread类。通过阅读Thread类中的描述,知道Thread类用来描述线程,使其具备线程应该有功能。Java虚拟机允许应用程序并发地运行多个执行线程。

构造方法

常用方法

继续阅读,发现创建新执行线程有两种方法。

  1. 一种方法是将类声明为Thread的子类。该子类应重写 Thread 类的run()方法。创建对象,开启线程。run()方法相当于主线程的main()方法。
  2. 另一种方法是声明一个实现Runnable接口的类。该类然后实现run()方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

二、创建线程方式一:继承Thread类

继承Thread类实现多线程的步骤:

  1. 定义一个继承于Thread类的子类,则该子类就具备线程功能。
  2. 在子类中重写Thread类的run()方法,并在重写的run()方法中封装任务。
  3. 实例化继承于Thread类的子类对象,并且还可以给创建的线程进行命名。
  4. 通过Thread的实例来调用start()方法启动线程,则默认就会调用run()方法。
    【示例】继承Thread类实现多线程
/**
 * 自定义线程类
 */
class TestThread extends Thread {
  public TestThread() {}
  public TestThread(String name) {
    super(name); // 设置线程名字
  }
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      // 通过Thread的getName()方法获取线程名字
      System.out.println(this.getName() + "--->" + i);
    }
  }
}
/**
 * 测试类
 */
public class Test {
  public static void main(String[] args) {
    // 创建线程对象
    TestThread th1 = new TestThread("A线程");
    TestThread th2 = new TestThread("B线程");
    // 启动线程
    th1.start(); // 注意:一个线程对象的start()方法只能被调用一次。
    th2.start();
    for(int i = 0; i < 10; i++) {
      System.out.println("主线程:" + i);
    }
  }
}

运行以上案例代码,输出结果如下:

三、【扩展】创建线程深度剖析

思考1:线程对象调用run方法和调用start方法区别?

线程对象调用run()方法不开启线程,仅是对象调用方法并在主线程中执行。线程对象调用start()开启线程,并让JVM调用run()方法在开启的线程中执行。

思考2:我们为什么要继承Thread类,然后调用start方法开启线程呢?

继承Thread类:因为Thread类用来描述线程,具备线程应该有功能。那为什么要创建继承于Thread的子类对象,而不是直接创建Thread类的对象呢?如下代码:

public static void main(String[] args) {
  Thread th = new Thread();
  th.start();
}

以上代码语法上没有任何问题,但是该start()调用的是Thread类中的run()方法,而这个run()方法没有做什么事情,更重要的是这个run()方法中并没有定义我们需要让线程执行的代码。

创建线程的目的就是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。对于之前所讲的主线程,它的任务定义在main方法数中。自定义线程需要执行的任务都定义在run()方法中。

Thread类run()方法中的任务并不是我们所需要的,只有重写这个run()方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可,所以进行了重写run()方法动作。

思考3:多线程执行时,到底在内存中是如何运行的呢?

多线程执行时,每一个执行线程都有一片自己所属的栈内存空间,用于方法的压栈和弹栈。

在多线程中,每个线程都有自己独立的栈内存,但是都是共享的同一个堆内存。在某个线程中程序执行出现了异常,那么对应线程执行终止,但是不影响别的线程执行。

main方法执行完毕之后,虚拟机有可能不会立即结束,只有等所有的线程都执行完毕之后,虚拟机才会结束!

思考4:开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?

查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象,还有个方法是获取当前线程对象的名称。

想要获取运行时线程名称,必须先要得到运行时线程对象(这里的线程对象和继承Thread子类对象是不一样的)。在线程类方法当中有一个方法,叫做currentThread(),返回thread类型,静态的,类名可以直接调用。

【示例】获取当前线程对象和线程名称
/**
 * 自定义线程类
 */
class TestThread extends Thread {
  private String name; // 定义一个普通成员变量
  public TestThread(String name) {
    this.name = name; // 给成员变量赋值
  }
  @Override
  public void run() {
    // 获取当前线程名称
    String threadName = Thread.currentThread().getName();
    System.out.println(name + "线程名称:" + threadName);
  }
}
/**
 * 测试类
 */
public class Test {
  public static void main(String[] args) {
    // 获取主线程对象
    Thread main = Thread.currentThread();
    // 获取主线程名称
    String name = main.getName();
    System.out.println("主线程名称:" + name);
    // 创建线程对象
    TestThread th1 = new TestThread("th1");
    TestThread th2 = new TestThread("th2");
    // 启动线程
    th1.start(); 
    th2.start();    
  }
}

运行以上案例代码,输出结果如下:

通过运行结果观察,发现主线程的名称为:main。自定义的线程名字默认为:Thread-加上编号,编号从0开始递增,th1线程对应的名称为:Thread-0,th2线程对应的名称为Thread-1。

那么自定义线程的默认名字是怎么来的呢? 通过对Thread类的源码分析,我们发现调用Thread类的构造方法时,默认就给该线程对象定义了一个名字,格式为:Thread-加上编号。

由此,我们也可以得出一个结论:当我们创建线程子类对象的时候,它们在创建的同时已经完成了名称的定义。

【示例】获取线程对象的名称
public class Test {
  public static void main(String[] args) {
    // 创建线程对象
    Thread th1 = new Thread();
    Thread th2 = new Thread();
    // 获取创建线程对象的名称
    System.out.println("th1线程对象名称:" + th1.getName());
    System.out.println("th2线程对象名称:" + th2.getName());   
  }
}

运行以上案例代码,输出结果如下:

思考5:可以手动的设置线程名称吗?

自定义的线程名字默认为:Thread-加上编号,如果我们想要修改默认的线程名字,可以在创建线程对象的时候设置线程的名称,也可以使用Thread类提供的setName()方法来实现。

【示例】设置线程对象的名称

public class Test {
  public static void main(String[] args) {
    // 创建线程对象
    Thread th = new Thread();
    // 设置线程的名称
    th.setName("线程A");
    // 获取创建线程对象的名称
    System.out.println("th线程对象名称:" + th.getName());
  }
}

运行以上案例代码,输出结果如下:

四、创建线程方式二:实现Runnable接口

使用继承Thread类的方式来创建线程有一个缺点,那就是自定义的类继承Thread类后就不能继承别的父类,如果还想继承别的父类那么可以选用第二种创建线程的方式。

在开发中,我们更多的是通过Runnable接口实现多线程,使用这种方式避免了Java单继承的局限性,所以实现Runnable接口方式要通用一些。

查看 Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务,并且包含了一个run()的无参抽象方法,需要Runnable接口的实现类来重写该方法。

接口中的方法

Thread类构造方法

实现Runnable接口实现多线程的步骤:

  1. 定义一个类并实现Runnable接口,然后重写Runnable接口的run()方法。
  2. 通过Thread类创建线程对象,并把Runnable接口的实现类对象作为参数传递。
  3. 调用线程对象的start()方法开启线程,并调用Runnable实现类的run()方法。
    【示例】实现Runable接口实现多线程
/**
 * Runnable接口的实现类
 */
class TestRunnable implements Runnable {
  @Override
  public void run() {
    for(int i = 0; i < 10; i++) {
      System.out.println("线程名称:" + Thread.currentThread().getName());
    }
  }
}
/**
 * 测试类
 */
public class Test {
  public static void main(String[] args) {
    // 创建线程执行任务对象
    TestRunnable tr = new TestRunnable();
    // 创建线程对象,将tr作为参数传递给Thread类的构造函数
    Thread th1 = new Thread(tr, "线程A");
    Thread th2 = new Thread(tr, "线程B");
    // 启动线程
    th1.start();
    th2.start();
    for(int i = 0; i < 10; i++) {
      System.out.println("线程名称:" + Thread.currentThread().getName());
    }
  }
}

运行以上案例代码,输出结果如下:

思考一:开发中,使用继承Thread创建线程多,还是实现Runnable接口创建线程多呢?

答案:实现Runnable接口创建线程

思考二:开发中,使用Runnable接口来创建线程有什么好处呢???

  1. 实现Runnable接口来创建线程,则线程类就可以继承别的父类,那么就避免单继承的局限性。
  2. 实现Runnable接口来创建线程,实现了任务对象和线程对象相分离,实现了代码的解耦操作。
  3. 实现Runnable接口来创建线程,可以实现数据的共享,而使用Thread创建线程不方便数据的共享。

五、【扩展】模拟Thread类start方法

我们知道,通过继承Thread类创建线程,线程任务是封装在Thread子类的run()方法中;通过实现Runnable接口来创建线程,线程任务是封装在Runnable接口实现类的run()方法中,那么调用start()方法开启线程,在Thread内部是如何正确的执行线程任务的呢?

接下来我们就来模拟实现Thread类,明确调用start()方法开启线程调用之后是如何实现调用run方法来执行对应的线程任务。

【示例】模拟Thread类start方法的实现

class Thread {
  private Runnable target;
  public Thread() {}
  public Thread(Runnable r) {
    this.target = r;
  }
  public void start() {
    run(); // 注意此处是重点
  }
  public void run() {
    if(target != null)
      target.run(); 
  }
}

模拟Thread类start方法的实现的核心:

如果创建线程采用继承Thread类的方式,也就是通过Thread子类对象调用start()方法,那么调用的就是Thread子类对象的run()方法。

如果创建线程采用实现Runnable接口的方式,也就是通过Thread对象调用start()方法,那么调用的就是Thread的run()方法,然后再调用Runnable接口实现类的run()方法。

接下来,我们就基于两种创建线程的方式,来对我们模拟Thread类的start()方法进行测试,看一下我们模拟实现是否成功!

【示例】基于两种创建线程方式的测试

// 模拟Thread类的start方法实现
class Thread {
  private Runnable target;
  public Thread() {}
  public Thread(Runnable r) {
    this.target = r;
  }
  public void start() {
    run();
  }
  public void run() {
    if(target != null)
      target.run(); 
  }
}
// Runnable接口的实现类
class MyRunnable implements Runnable {
  @Override
  public void run() {
    System.out.println("MyRunnable run....");
  }
}
// 继承于Thread的子类
class MyThread extends Thread {
  public void run() {
    System.out.println("MyThread run....");
  }
}
// 测试类
public class Test01 {
  public static void main(String[] args) {
    // 创建多线程方式一:继承Thread类
    MyThread th1 = new MyThread();
    th1.start();
    // 创建多线程方式二:实现Runnable接口
    MyRunnable mr = new MyRunnable();
    Thread th2 = new Thread(mr);
    th2.start();
  }
}

运行以上案例代码,输出结果如下:

相关文章
|
7天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
3天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
20 9
|
6天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
3天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
6天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
20 3
|
5天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
6天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
16 1
|
6天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
7天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
31 1
|
10天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####