前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 多线程(1)---- 初识线程

Java 多线程(1)---- 初识线程

作者头像
指点
发布2019-01-18 15:21:46
3970
发布2019-01-18 15:21:46
举报
文章被收录于专栏:指点的专栏指点的专栏

前言

多线程想必大家都不会陌生。因为在日常使用和开发中,多线程的使用实在是太常见了。我们都知道,发明多线程的目的是为了更好的利用计算机的 CPU 资源。比如在一个进程中如果只有一个线程(也叫主线程),那么如果当这个线程因为某种原因阻塞(等待用户输入数据等情况)的时候,那么相对应的这个进程也让出了 CPU 资源并暂停执行了。试想一下,如果我们在一个进程中添加多个线程,那么当这个进程中某个线程阻塞的时候,其余线程还可以继续执行,做它们自己的工作,这样的话计算机的利用效率就提高了。这当然是一个最简单也是最常用的例子。下面来看一下 Java 中线程的基本概念

基本概念

在 Java 中,线程被封装在 Thread.java 类中,我们可以通过这个类提供的相关 API 对线程进行调控和操作。先来看看源码中对这个类的介绍(截取自 JDK_1.8 Thread.java 部分顶部注释):

代码语言:javascript
复制
/**
 * A <i>thread</i> is a thread of execution in a program. The Java
 * Virtual Machine allows an application to have multiple threads of
 * execution running concurrently.
 * <p>
 * Every thread has a priority. Threads with higher priority are
 * executed in preference to threads with lower priority. Each thread
 * may or may not also be marked as a daemon. When code running in
 * some thread creates a new <code>Thread</code> object, the new
 * thread has its priority initially set equal to the priority of the
 * creating thread, and is a daemon thread if and only if the
 * creating thread is a daemon.
 * <p>
 * When a Java Virtual Machine starts up, there is usually a single
 * non-daemon thread (which typically calls the method named
 * <code>main</code> of some designated class). The Java Virtual
 * Machine continues to execute threads until either of the following
 * occurs:
 * <ul>
 * <li>The <code>exit</code> method of class <code>Runtime</code> has been
 *     called and the security manager has permitted the exit operation
 *     to take place.
 * <li>All threads that are not daemon threads have died, either by
 *     returning from the call to the <code>run</code> method or by
 *     throwing an exception that propagates beyond the <code>run</code>
 *     method.
 * </ul>
 * <p>
 * 
 * ......
 * 
 * Every thread has a name for identification purposes. More than
 * one thread may have the same name. If a name is not specified when
 * a thread is created, a new name is generated for it.
 * <p>
 * ......
 * /

大致意思是: 在一个程序中线程是可以执行的对象,Java 虚拟机允许在一个程序中有多个线程并发执行(同时执行)。 每个线程都有一个优先级,优先级高的线程会优先于优先级低的线程执行。每个线程也可以被标记为守护线程(后面会介绍),当在一个线程执行过程中如果创建了另一个新的线程,那么初始时这个新的线程的优先级和创建它的线程的优先级相同,另外,在守护线程中只能创建守护线程。 当虚拟机启动的时候,它会创建一个非守护线程(就是主线程),并且虚拟机会一直执行这个线程直到发生下面几种情况之一: RunTime 类中的 exit 方法被调用并且安全管理器允许程序退出。 所有的非守护线程结束运行,这包括线程中 run 方法的返回或者在 run 方法执行过程中发生了一个异常。 每个线程都有一个名字用来标志这个线程,多个线程名字可以相同。如果一个线程在创建时没有为它指定一个名字,那么虚拟机会为它自动生成一个名字。

从上面那段话我们知道线程存在优先级的概念,每个线程都会有一个名字,并且线程可以分为守护线程和非守护线程,同时一个 Java 程序运行时会创建并执行一个主线程(运行 main 方法的线程)。

下面来看一下怎么创建一个线程:

代码语言:javascript
复制
public Thread(Runnable target) {
    // 如果我们不指定线程的名字,那么虚拟机会根据当前已经创建的线程的数量来默认指定一个线程名
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
代码语言:javascript
复制
public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

Thread.java 类源码中截取了两个常用的创建线程的方法,很明显,参数 target 就是创建的线程在运行执行的 Runnable 对象,而参数 name 就是我们为线程指定的名字。 在创建完线程之后,我们可以调用线程对象的start() 方法来开启一个线程(这个方法只能被调用一次),也可以通过 Thread 类提供的 setPriority(int newPriority) 方法来设置线程的优先级,系统已经给我们提供了几个线程的优先级:

代码语言:javascript
复制
/**
 * The minimum priority that a thread can have.
 */
public final static int MIN_PRIORITY = 1;

/**
 * The default priority that is assigned to a thread.
 */
public final static int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public final static int MAX_PRIORITY = 10;

如果你不指定线程的优先级,那么其默认的优先级为 NORM_PRIORITY线程的优先级越高,其越容易得到 CPU 资源。 请注意:优先级越高的线程只是越容易得到 CPU 资源,也就是说优先级可以理解成线程获取 CPU 资源的概率,优先级越高的线程获取 CPU 资源的概率越大,但不一定说线程优先级越高,线程就一定能得到 CPU 资源。

下面看个具体的例子来看一下线程基本用法,新建一个 Java 工程并且新建一个 Java 类:

代码语言:javascript
复制
import java.util.Scanner;

public class ThreadTest {

    /**
     * 第一种实现线程的方法:通过自定义类继承 Thread 类并且重写 run 方法来实现线程
     */
    public static class FirstThread extends Thread {
        public FirstThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始执行");
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "打印: " + i);
            }
            System.out.println(Thread.currentThread().getName() + "结束执行");
        }
    }

    // 新建并执行子线程同时模拟用户输入数据
    public static void threadTest1() {
        System.out.println("主线程开始执行");
        // 新建线程
        FirstThread firstThread = new FirstThread("线程1");
        firstThread.start(); // 启动线程
        threadTest2();
        Scanner scanner = new Scanner(System.in);
        // 等待用户输入一个数字
        int x = scanner.nextInt();
        System.out.println("您输入的数字为: " + x);
        System.out.println("主线程结束执行");  
    }

    /**
     * 第二种实现线程的方法:通过创建 Runnable 对象作为参数来创建线程并执行
     */
    public static void threadTest2() {
        // 新建 Runnable 对象并重写其 run 方法并将其作为参数来新建一个线程
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始执行");
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "打印: " + i);
                }
                System.out.println(Thread.currentThread().getName() + "结束执行");
            }
        };
        // 传入定义的 Runnable 对象
        Thread thread = new Thread(r, "线程2");
        thread.start(); // 开启线程
    }

    public static void main(String[] args) {
        threadTest1();
    }
}

我们自定义了一个类 FirstThread,使其继承 Thread 类,之后在 threadTest1 方法中新建了一个 FirstThread 类的对象并且调用了它的 start() 方法,此时 FirstThread 线程对象在后台执行,然后我又调用了 threadTest2() 方法并在里面用 Runnable 的方式新建了一个线程 2 并执行它,最后在主线程中调用 threadTest1 这个方法,然后我新建了一个 Scanner 对象并且利用这个对象来获取用户输入的一个数字。那么此时主线程应该陷入等待状态,让出CPU,直到用户的输入将其唤醒。但是子线程还是可以继续执行。 来看看结果:

这里写图片描述
这里写图片描述

大多数情况下,你可能会得到上面这种运行结果,即线程 1 先执行完成,之后线程 2 才执行,这其实是一种错觉,相同状态下相同优先级的线程获得 CPU 资源并执行的几率是相同的,这里为什么会导致这种结果呢?其一是因为在这里线程 1 是先调用start() 方法,之后才创建线程 2 并且调用其 start() 方法,其二是因为这里循环只执行了 10 次,次数太少了,因此线程执行实现非常短,有可能在线程 2 还没调用 start() 方法线程 1 就执行完毕了,所以大多情况下线程 1 会先执行完毕,但是当你反复执行多次的时候,你也能得到下面的结果:

这里写图片描述
这里写图片描述

即线程 1 在执行的过程中线程 2 穿插入线程 1 中执行,也就是两个线程交替执行(你也可以试着增加循环次数来更容易的达到这个执行结果)。根据执行结果我们知道,一个程序中某个主线程陷入等待状态时,其余非等待状态线程依然可以得到 CPU 资源并执行。

我们注意到上面的代码中我用两种方式新建线程: 第一种是自定义一个类 FirstThread 继承于 Thread 类并且重写其 run 方法; 第二种是通过新建一个 Runnable 对象并将其作为一个参数传入 Thread 类的构造方法中。这两种方法有什么区别呢?这其实是一个涉及到参数传递的基础问题。我们来看一张图:

这里写图片描述
这里写图片描述

从图中很容易可以看出:第一种方法创建的线程中,每个线程都执行自己的 run 方法,而第二种方法创建的线程中,对于使用同一个 Runnable 作为参数传递创建的线程,多个线程执行同一个 Runnable 对象的 run 方法。

线程的生命周期

我们再来看看一个线程的在其生命周期中会有哪些状态: 1、新建:指我们使用关键字 new 新建了一个线程对应上面代码中 FirstThread firstThread = new FirstThread("线程1");

2、运行状态:这个状态可以分成两个部分:可运行状态(就绪状态)、正在运行状态。当线程对象调用了 start() 方法时,其就处于可运行状态(注意这里是可运行状态),当线程处于可运行状态并且得到了 CPU 资源时,线程才进入正在运行状态。也就是说你对一个线程调用 start() 方法只是提醒线程调度器这个线程可以被执行,但是到底执不执行还得看线程调度器的调度结果。

3、等待状态:在等待状态的线程不会占用 CPU,这个状态也可以分成两种:永久等待状态、期限等待状态。永久等待状态需要被显式的唤醒(对应上面代码中等待用户输入数字),期限等待状态在过了一定时间之后会被系统自动唤醒(调用 Thread.sleep(long millis); 方法等)。

4、阻塞状态:线程因为在等待获取一个互斥资源(互斥锁)而陷入的等待状态,一般在程序进入临界区(同一时刻只有一个线程能进入并执行代码)的时候,会导致没有获得互斥资源的线程陷入阻塞状态。

5、结束状态:线程已经结束(Thread 对象的 run() 方法执行完成或者在其 run() 方法执行过程中发生异常)。

我们可以用一张图来看一下这 5 个状态的对应转换关系:

这里写图片描述
这里写图片描述

在这里我将运行状态分成了两个子状态:就绪状态和正在执行状态,因此会有 6 个状态。 介绍一下图中涉及到的相关方法:

代码语言:javascript
复制
Thread.sleep(long millis) // 让调用这个方法的线程让出 CPU,休眠参数指定的毫秒数

Thread.join() // 在线程执行过程中插入另外一个线程,并且直到这个插入的线程执行完成之后再继续执行原来的线程

Object.wait() // 让调用这个方法的线程陷入等待状态,可以通过参数设置等待时间,
              // 如果不设置参数将使得线程一直等待。
              // 注意这个方法只能在 synchronized 关键字修饰的代码块中调用,
              // 这个我们会在后面的文章中细讲。

Object.notify() // 唤醒一个因调用当前对象的 wait() 方法而陷入等待状态的线程,具体哪个线程未知。
                // 这个方法也只能在 synchronized 关键字修饰的代码块中执行

Object.notifyAll() // 唤醒所有因调用当前对象的 wait() 方法而陷入等待状态的线程。
                   // 同样,这个方法也只能在 synchronized 关键字修饰的代码块中执行。

对于这些方法,在此系列之后的文章中会具体介绍。

守护线程

到目前为止我们创建的所有线程都是非守护线程,我们在文章开头还提到过一个守护线程的概念,顾明思议,守护线程就是在后台默默的守护的线程(这么说其实有点绕,因为线程本身就是在后台运行),我们可以把守护线程理解为非守护线程的守护者,只不过这个守护者又是一个线程。 要创建一个守护线程也很简单,只需要在调用线程的start() 方法之前调用该线程对象的 setDaemon(true) 即可,请注意,务必在调用 start() 方法之前调用 setDaemon(true) 方法,否则会报异常。守护线程和普通线程的区别也很简单:当所有的非守护线程都结束(处于结束状态)之后,所有的守护线程也会结束,不管它有没有执行完成。换句话说,当要守护的线程都结束了,那么就没有线程需要守护了,守护线程也就没有存在的意义了。由于守护线程的这个特点,我们不应该将重要的任务放在守护线程中完成。想像一下,假设我们有一个重要的文件要下载到本地,那如果我们在守护执行这个任务,当所有的非守护线程都执行结束了,所有的守护线程也会强制结束。如果此时文件还未下载完成,然而执行下载任务的线程却结束了,那么我们就只能得到不完整的文件了。这种情况是绝对不能容忍的。所以我们不应该将重要的任务放在守护线程中完成。我们来看一个具体的例子,我们在类中添加方法并且修改main 方法:

代码语言:javascript
复制
/**
 * 守护线程的测试
 */
public static void daemonThreadTest() {
    Thread daemonThread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "打印: " + i);
                try {
                    Thread.sleep(1000); // 守护线程休眠 1 秒
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } 
            }
        }}, "守护线程");
    daemonThread.setDaemon(true); // 设置当前线程为守护线程
    daemonThread.start(); // 开启守护线程
}

public static void main(String[] args) {
    System.out.println("主线程启动");
    daemonThreadTest();
    try {
        Thread.sleep(5000); // 主线程休眠 5 秒
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    System.out.println("主线程结束");
}

main 方法中我先调用方法启动守护线程,这个守护线程的任务是每隔 1 秒打印一次 i 的值,循环一共需要执行 10 次,也就是打印 10 次 i 的值。之后我让主线程休眠 5 秒后打印一句结束语后结束。来看看结果:

这里写图片描述
这里写图片描述

当然你也可能的到下面这个结果:

这里写图片描述
这里写图片描述

你还可能得到下面的结果:

这里写图片描述
这里写图片描述

可能有小伙伴会问了,为什么下面两个运行结果守护线程会多打印一次i 的值?这其实是线程之间的调度导致的。我们知道在守护线程中是先打印 i 的值然后再进行休眠。那么当主线程休眠完 5 秒的时候守护线程也正好休眠完成并且在准备下一次的打印。如果在此时或者 当主线程打印主线程结束 这句话之后并且在 main 方法结束之前 CPU 执行了守护线程的话,那么守护线程就会执行第 6 次打印。即出现 守护线程打印: 5 这一行。这个当然也是有概率的。所以会出现上面三种情况,多线程之间的运转就是这么奇妙(哈哈)。

话说回来,不管执行结果是上面 3 种情况中的哪种,守护线程都是没有执行完成的(任务是打印 10 次,但是结果只打印了 5~6 次)。因为在这个程序中主线程是唯一的非守护线程,如果主线程结束了,也就意味着程序中不存在非守护线程了。那么此时所有的守护线程都会被强制结束。所以一些重要的任务不应该放在守护线程中完成。

好了。Java 多线程第一篇就到这里了,相信你对 Java 中多线程已经有了一个初步的了解。如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。

谢谢观看。。。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年03月09日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 基本概念
  • 线程的生命周期
  • 守护线程
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档