【Java基础系列教程】第十章 Java多线程(上)_线程创建与使用、线程生命周期

一、程序、进程、线程

 

1.1 基本概念

1.1.1 程序概述

        程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

        我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。

1.1.2 进程概述

        进程(process)是程序的一次执行过程,或是一个正在执行的程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。
                如:运行中的QQ,运行中的音乐播放器、视频播放器等;
                程序是静态的,进程是动态的;
                进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域;

        进程是一个具有一定独立功能的应用程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。

        进程一般由程序,数据集合和进程控制块三部分组成。
                程序用于描述进程要完成的功能,是控制进程执行的指令集;
                数据集合是程序在执行时所需要的数据和工作区;
                程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志。
                        程序控制块:https://blog.csdn.net/mxrrr_sunshine/article/details/79538827

        进程具有的特征:
                动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
                并发性:任何进程都可以同其他进程一起并发执行;
                独立性:进程是系统进行资源分配和调度的一个独立单位,不同进程的工作互相不影响;
                结构性:进程由程序,数据和进程控制块三部分组成;
                制约性:因访问共享数据/资源或进程间同步而产生制约; 

        简而言之:进程是一个应用程序的运行实例,是一个动态概念,竞争计算机系统资源的基本单位。在操作系统中运行的程序就是进程,比如:QQ、游戏、IDEA、钉钉等;

进程与程序联系与区别:

  • 两者联系

        进程是操作系统处于执行状态程序的抽象。
                程序 = 文件(静态的可执行文件);
                进程 = 执行中的程序 = 程序 + 执行状态;

        同一个程序的多次执行过程对应为不同进程。
                例如:多次使用命令ls的执行对应多个进程。

        进程执行需要的资源。
                内存:保存代码和数据;
                cpu:执行指令;

  • 两者区别

        进程是动态的,程序是静态的。
                程序是有序代码的集合;
                进程是程序的执行;

        进程是暂时的,程序是永久的。
                进程时一个状态变化的过程;
                程序可长久保存;

        进程与程序的组成不同。
                进程的组成包括程序,数据,和进程控制块。

        简单来说:我们写好的一个代码,要想让它运行起来,首先需要将这个程序加载到内存中,程序一旦运行那么就是一个进程,就需要给进程分配资源建立PCB等动作。说白了,程序只是一个存储在我们硬盘上的文件而已断电不会消失,但是进程是在动态运行的一个实体,进程执行结束相关信息就会被释放了。

1.1.3 线程概述

        在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。

        后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

        线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
                若一个进程同一时间并行执行多个线程,就是支持多线程的(360)。
                线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
                一个进程中的多个线程共享相同的内存单元/内存地址空间 -> 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

        简而言之:线程是进程的一个执行单元。比进程更小的独立运行的基本单位。

        注:一个程序至少一个进程,一个进程至少一个线程。比如视频中同时有声音、图像、弹幕等;

1.1.4 单核CPU和多核CPU的理解

        单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。

        如果是多核的话,才能更好的发挥多线程的效率(现在的服务器都是多核的)。

        多核CPU 和 多个CPU有何区别?
                多核和多CPU分别代表什么意思吧?例如,你需要搬很多砖,你现在有一百只手。当你将这一百只手全安装到一个人身上,这模式就是多核。当你将这一百之手安装到50个人身上工作,这模式就是多CPU。
                https://www.zhihu.com/question/20998226  

        “多个单核CPU”与“单个多核CPU”哪种方式性能较强?
                多个单核CPU: 成本更高,因为每个CPU都需要一定的线路电路支持,这样对主板上布局布线极为不便。并且当运行多线程任务时,多线程间通信协同合作也是一个问题。依赖总线的传输,速度较慢,且每一个线程因为运行在不同的CPU上。导致不同线程间各开一个Cache,会造成资源的浪费,同时如果线程间协作就会有冗余数据的产生,更加大了内存的开销。
                单个多核CPU:    可以很好地规避基本上多个单核CPU提到的所有缺点。它不需要考虑硬件上的开销以及复杂性问题,同时也可以很好地解决多线程间协同工作的问题,减少内存的开销,因为多线程程序在多核CPU中运行是共用一块内存区的,数据的传输速度比总线来的要快同时不会有冗余数据的产生。单个多核CPU的问题也是显而易见的,当多个较大程序共同运行时,内存就显得极为匮乏了,不光是Cache占用的的问题,同时还有程序的指令以及数据的替换问题。
                https://www.cnblogs.com/zackary/p/8666485.html

1.2 进程和线程的图文理解

        进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。最近,我读到一篇材料,发现有一个很好的类比,可以把它们解释地清晰易懂。

        1、计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

         2、假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

         3、进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

         4、一个车间里,可以有很多工人。他们协同完成一个任务。

         5、线程就好比车间里的工人。一个进程可以包括多个线程。

        6、车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

         7、可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

         8、一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

        9、还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

         10、这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

         不难看出,mutex(互斥锁)是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

        11、操作系统的设计,因此可以归结为三点:
                1、以多进程形式,允许多个任务同时运行;
                2、以多线程形式,允许单个任务分成不同的部分运行;
                3、提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

二、进程与线程的区别

        1、线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
        2、一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
        3、进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
        4、调度和切换:线程上下文切换比进程上下文切换要快得多;

2.1 线程和进程关系示意图

        进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

        一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。

        总之,线程和进程都是一种抽象的概念,线程是一种比进程还小的抽象,线程和进程都可用于实现并发。

        在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位,它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程。

        后来,随着计算机的发展,对多个任务之间上下文切换的效率要求越来越高,就抽象出一个更小的概念-线程,一般一个进程会有多个(也可以是一个)线程。

2.2 任务调度方式

        大部分操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态,等待下一个属于它的时间片的到来。
    
        这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。

2.3 为何不使用多进程而是使用多线程

2.3.1 为什么要使用多线程?

         原本一条道路,因为车辆越来越多,会造成道路堵塞,效率低下。为了提高使用的效率,能够充分利用道路,于是加了多个车道,从此,道路通行效率就高了。

多线程的优点:

        背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

        多线程程序的优点:
                1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
                2、提高计算机系统CPU的利用率。
                3、改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

何时需要多线程:

        程序需要同时执行两个或多个任务。
        程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
        需要一些后台运行的程序时。

2.3.2 为什么不使用多进程?

        线程廉价,线程启动比较快,退出比较快,对系统资源的冲击也比较小。而且线程彼此分享了大部分核心对象(File Handle)的拥有权;如果使用多重进程,但是不可预期,且测试困难.

2.4 并行和并发

        并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)

        并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。

        比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。

        如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。

为什么操作系统上可以同时运行多个程序而用户感觉不出来?

        这是因为无论是单CPU还是多CPU,操作系统都营造出了可以同时运行多个程序的假象。实际的过程操作系统对进程的调度以及CPU的快速上下文切换实现的:每个进程执行一会就先停下来,然后CPU切换到下个被操作系统调度到的进程上使之运行。因为切换的很快,使得用户认为操作系统一直在服务自己的程序。

        并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象。

        并发描述的是多进程同时运行的现象。但实际上,对于单核心CPU来说,同一时刻只能运行一个进程。所以,这里的"同时运行"表示的不是真的同一时刻有多个进程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占CPU的,而是执行一会停一会。

        所以,并发和并行的区别就很明显了。它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个进程在运行(处于running),并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象。

        实际上,当程序中写下多进程或多线程代码时,这意味着的是并发而不是并行。并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。也就是说,并行与否程序员无法控制,只能让操作系统决定。

        再次注明,并发是一种现象,之所以能有这种现象的存在,和CPU的多少无关,而是和进程调度以及上下文切换有关的。

        现实生活中,有很多这样同时做多件事情的例子;但是看起来是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情;

2.5 多线程的应用场景

        红蜘蛛同时共享屏幕给多个电脑;

        百度网盘同时开启多个任务一起下载;

        QQ同时和多个人一起视频;
    
        服务器同时处理多个客户端请求;

        一个游戏可以多人同时玩;

        注:很多多线程是模拟出来的,真正的多线程是指多个CPU,也就是我们所说的多核,如服务器;如果是模拟出来的多线程,即在一个cpu的情况下,在同一时间点,cpu只能执行一个代码,因为切换的比较快,所以就有了同时执行的错觉;

2.6 多线程面试题

2.6.1 Java程序运行原理

        通过java命令会启动 java虚拟机。启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程”,然后主线程去调用某个类的 main方法。所以 main方法运行在主线程中。

        在此之前的所有程序都是单线程的。

方法调用和多线程示例图:

2.6.2 JVM的启动是多线程的吗?

        多线程的。原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。

        注:JVM启动其实至少有三个线程:main主线程,gc垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

public class ThreadTest1 {
    public static void main(String[] args) {
        System.out.println("hello");

        new Object(); // 造对象
        new Object(); // 造对象
        new Object(); // 造对象
        new Object(); // 造对象
        //...造很多很多对象后,如果垃圾回收线程不启动的话,内存就会溢出!

        System.out.println("world");
    }
}

2.7 总结

        线程是程序执行的最小单位,是独立的执行路径;

        在程序运行时,即使我们没有创建线程,后台也会有多个线程,如主线程,gc线程;

        main()称之为主线程,是程序的执行入口,用来执行整个应用程序;

        在一个进程中,如果开辟了多条线程,线程的运行由调度器安排调度,调度器与操作系统紧密联系,先后顺序是不能人为干预的;

        对同一资源操作时,会存在资源抢夺的问题,需要加入并发控制;

        线程会带来额外的开销,如cpu调度,并发控制开销;

        每个线程在自己的工作内存交互,内存控制不当会造成数据不一致;如果多线程操作同一条数据,会把数据放到自己的工作内存操作,操作完成之后再写入到共享内存;比如:火车票售票负数问题。

三、Java多线程的创建(重点)

注意:下面的程序不是多线程!

public class Sample {
    public void method1(String str) {
        System.out.println(str);
    }
    public void method2(String str) {
        method1(str);
    }
    public static void main(String[] args) {
        Sample s = new Sample();
        s.method2("hello!");
    }
}

        Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。

        Thread类的特性:
                每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
                通过该Thread对象的start()方法来启动这个线程,而非直接调用run()。

        JDK1.5之前创建新执行线程有两种方法:
                继承Thread类的方式;
                实现Runnable接口的方式;

3.1 方式一:继承Thread类

        1、定义子类继承Thread类。
        2、子类中重写Thread类中的run方法,把新线程要做的事写在run方法中。
        3、创建Thread子类对象,即创建线程对象。
        4、调用线程对象start方法:启动线程,调用run方法。

主线程和分支线程并发执行:

public class ThreadTest2 {
    public static void main(String[] args) {
        // 3、创建Thread子类对象,即创建线程对象。
        MyThread mt = new MyThread(); 
        // 4、调用线程对象start方法:启动线程,调用run方法。
        mt.start(); 

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

}

// 1、定义子类继承Thread类。
class MyThread extends Thread {
    // 2、子类中重写Thread类中的run方法,把新线程要做的事写在run方法中。
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("aaaaaaaaaaaa");
        }
    }
}

两个分支线程并发执行:

public class ThreadTest3 {

	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				// 创建一个新的线程 myThread1 此线程进入新建状态
				Thread myThread1 = new MyThread();

				// 创建一个新的线程 myThread2 此线程进入新建状态
				Thread myThread2 = new MyThread();

				// 调用start()方法使得线程进入就绪状态
				myThread1.start();

				// 调用start()方法使得线程进入就绪状态
				myThread2.start();
			}
		}
	}
}

class MyThread extends Thread {

	private int i = 0;

	@Override
	public void run() {
		for (i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
		}
	}
}

        如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。

        当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。

        注:不建议使用该方式,避免OOP单继承局限性;

运行示意图:

 注意点:

        1、如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。

        2、run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。

        3、想要启动多线程,必须调用start方法。

        4、一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常 “IllegalThreadStateException”。

使用方式一完成售票案例(存在线程安全问题):

// 存在线程的安全问题,待解决。
class Window extends Thread{
	// 总票数
    private static int ticket = 100;
    
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}


public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

3.2 方式二:实现Runnable接口

        实现Runnable接口并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
    
        Thread类构造器:
                Thread():创建新的Thread对象;
                Thread(String threadname):创建线程并指定线程实例名;
                Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法;
                Thread(Runnable target, String name):创建新的Thread对象;

        1、定义子类,实现Runnable接口。
        2、子类中重写Runnable接口中的run方法,把新线程要做的事写在run方法中。
        3、创建自定义的Runnable的子类对象。
        4、通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
        5、调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。

public class ThreadTest4 {
	public static void main(String[] args) {
		// 3、创建自定义的Runnable的子类对象
		MyRunnable mr = new MyRunnable();

		// 4、通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
		Thread t = new Thread(mr);
        
		// 5、调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
		t.start();

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

}

// 1、定义子类,实现Runnable接口。
class MyRunnable implements Runnable {

	// 2、子类中重写Runnable接口中的run方法,把新线程要做的事写在run方法中
	@Override
	public void run() {
		for (int i = 0; i < 1000; i++) {
			System.out.println("aaaaaaaaaaaa");
		}
	}
}

使用方式二完成售票案例(存在线程安全问题):

class Window implements Runnable{
	// 总票数
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}


public class WindowTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}

        相信以上两种创建新线程的方式大家都很熟悉了,那么Thread和Runnable之间到底是什么关系呢?我们首先来看一下下面这个例子。

public class ThreadTest5 {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable();
                Thread thread = new MyThread(myRunnable);
                thread.start();
            }
        }
    }
}

//实现Runnable接口实现的
class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        System.out.println("in MyRunnable run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class MyThread extends Thread {

    private int i = 0;
    
    public MyThread(Runnable runnable){
        super(runnable);
    }

    @Override
    public void run() {
        System.out.println("in MyThread run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

同样的,与实现Runnable接口创建线程方式相似,不同的地方在于:

        Thread thread = new MyThread(myRunnable);

        那么这种方式可以顺利创建出一个新的线程么?答案是肯定的。至于此时的线程执行体到底是MyRunnable接口中的run()方法还是MyThread类中的run()方法呢?通过输出我们知道线程执行体是MyThread类中的run()方法。其实原因很简单,因为Thread类本身也是实现了Runnable接口,而run()方法最先是在Runnable接口中定义的方法。

public interface Runnable {

    public abstract void run();

}

我们看一下Thread类中对Runnable接口中run()方法的实现:

private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

        也就是说,当执行到Thread类中的run()方法时,会首先判断target是否存在,存在则执行target中的run()方法,也就是实现了Runnable接口并重写了run()方法的类中的run()方法。但是上述给到的列子中,由于多态的存在,根本就没有执行到Thread类中的run()方法,而是直接先执行了运行时类型即MyThread类中的run()方法。
                1、看Thread类的构造函数,传递了Runnable接口的引用;
                2、通过init()方法找到传递的target给成员变量的target赋值;
                3、查看run方法,发现run方法中有判断,如果target不为null就会调用Runnable接口子类对象的run方法。

        实现Runnable接口比继承Thread类所具有的优势:
                1、从形式上来说,可以避免Java中的单继承局限;
                2、使用Runnable实现的多线程的程序类可以更好的描述出程序共享的概念(方便同一个对象被多个线程使用)。

3.3 两种创建方式的对比

        查看源码的区别:
                继承Thread:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run()方法。
                实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法(也就是实现了Runnable接口并重写了run()方法的类中的run()方法)。

        继承Thread:
                好处是:可以直接使用Thread类中的方法,代码简单。
                弊端是:如果已经有了父类,就不能用这种方法。

        实现Runnable接口(推荐):
                好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的,避免了单继承的局限性。多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
                弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂。

3.4 匿名内部类实现线程

public class ThreadTest6 {

    public static void main(String[] args) {
        // 1.继承Thread类
        new Thread() {
            // 2.重写run方法
            public void run() {
                // 3.将要执行的代码写在run方法中
                for (int i = 0; i < 1000; i++) {
                    System.out.println("aaaaaaaaaaaaaa");
                }
            }
        }.start(); // 4.开启线程

        // 1.将Runnable的子类对象传递给Thread的构造方法
        new Thread(new Runnable() {
            // 2.重写run方法
            public void run() {
                // 3.将要执行的代码写在run方法中
                for (int i = 0; i < 1000; i++) {
                    System.out.println("bb");
                }
            }
        }).start(); // 4.开启线程
    }

}

练习题:

        创建两个线程,让其中一个线程输出1-100之间的偶数,另一个线程输出1-100之间的奇数。

public class ThreadDemo {
    public static void main(String[] args) {
        // MyThread1 m1 = new MyThread1();
        // MyThread2 m2 = new MyThread2();
        //
        // m1.start();
        // m2.start();

        //创建Thread类的匿名子类的方式
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i % 2 == 0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);

                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i % 2 != 0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);

                    }
                }
            }
        }.start();
    }
}

class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);

            }
        }
    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);

            }
        }
    }
}

四、Java多线程的使用(重点)

4.1 获取名称和设置名称

        1、获取名称
                通过 getName()方法获取线程对象的名字

        2、设置名称
                通过构造函数可以传入String类型的名字
                Thread(String name)     分配新的 Thread 对象。name - 新线程的名称。

                setName(String name)     改变线程名称,使之与参数 name 相同。

4.1.1 成员方法实现设置线程名称

public class ThreadNameTest {
    public static void main(String[] args) {
        // demo1();
        Thread t1 = new Thread() {
            public void run() {
                // this.setName("张三");
                System.out.println(this.getName() + "....aaaaaaaaaaaaa");
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                // this.setName("李四");
                System.out.println(this.getName() + "....bb");
            }
        };

        //成员方法方式给线程赋值
        t1.setName("张三");
        t2.setName("李四");
        t1.start();
        t2.start();
    }
}

4.1.2 构造函数实现设置线程名称

//构造函数方式给线程赋值
public static void demo1() {
    new Thread("芙蓉姐姐") { // 通过构造方法给name赋值
        public void run() {
            System.out.println(this.getName() + "....aaaaaaaaa");
        }
    }.start();

    new Thread("凤姐") {
        public void run() {
            System.out.println(this.getName() + "....bb");
        }
    }.start();
}

4.2 获取当前线程的对象

        static Thread currentThread()     返回对当前正在执行的线程对象的引用,在Thread子类中就是this,通常用于主线程和Runnable实现类。 

        注:主线程也可以获取;

public class CurrentThreadTest {
    public static void main(String[] args) {
        new Thread() {
            public void run() {
                System.out.println(getName() + "....aaaaaa");
            }
        }.start();


        new Thread(new Runnable() {
            public void run() {
                //Thread.currentThread()获取当前正在执行的线程
                System.out.println(Thread.currentThread().getName() + "...bb");
            }
        }).start();

        Thread.currentThread().setName("我是主线程");
        System.out.println(Thread.currentThread().getName());
    }

}

4.3 休眠线程

        static void sleep(long millis)    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 

        static void sleep(long millis, int nanos)    在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 

                1秒=1000毫秒
                1毫秒=1000微秒
                1微秒=1000纳秒
                1纳秒=1000皮秒
                1皮秒=1000飞秒

        sleep:让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,处于 sleep()中的线程也不会执行。因此sleep()方法常用来暂停线程执行。

        前面有讲到,当调用了新建的线程的 start()方法后,线程进入到就绪状态,可能会在接下来的某个时间获取CPU时间片得以执行,如果希望这个新线程必然性的立即暂停执行,直接调用线程的sleep()即可。

        注:每个对象都有一把锁,sleep不会释放锁。

public class SleepTest {

    public static void main(String[] args) throws InterruptedException {
        // demo1();
        new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...aaaaaaaaaa");
                }
            }
        }.start();

        new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...bb");
                }
            }
        }.start();
    }

    public static void demo1() throws InterruptedException {
        for (int i = 20; i >= 0; i--) {
            Thread.sleep(1000);
            System.out.println("倒计时第" + i + "秒");
        }
    }
}

4.4 守护线程

        Java线程中有两种线程,一种是用户线程(非守护线程),一种是守护线程。

        守护线程是一种特殊的线程,它具有陪伴的含义。当进程中不存在非守护线程了,则守护线程自动销毁。典型的就是垃圾回收线程。当进程中没有非守护线程了,则垃圾回收线程没有存在的必要,自动销毁。

        任何一个守护线程都是整个JVM中所有非守护线程的保姆。只要当前 JVM 中有非守护线程没有结束,守护线程就在工作。只有当最后一个非守护线程不工作的时候,守护进程才随着JVM一同结束工作。 

        Dammon(守护线程)的作用是为非守护线程的运行提供便利服务。守护进程最典型的应用是GC(垃圾回收器),它是很称职的守护者。

        void setDaemon(boolean on)    将该线程标记为守护线程或用户线程。
                on - 如果为 true,则将该线程标记为守护线程。如果为false,则将该线程标记为用户线程;

        boolean isDaemon()    测试该线程是否为守护线程

public class DaemonTest {

	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for (int i = 0; i < 2; i++) {
					System.out.println(getName() + "...aaaaaaaaaaaaaaaaaaaa");
				}
			}
		};

		Thread t2 = new Thread() {
			public void run() {
				for (int i = 0; i < 50; i++) {
					System.out.println(getName() + "...bb");
				}
			}
		};

		t2.setDaemon(true); // 设置为守护线程

		t1.start();
		t2.start();
	}
}

4.5 加入线程

        join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续;

        join(long millis), 可以等待指定的毫秒之后继续;

        join(long millis, int nanos)     等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

        join —— 让一个线程等待另一个线程完成才继续执行。如A线程线程执行体中调用B线程的 join()方法,则A线程被阻塞,直到B线程执行完为止,A才能得以继续执行。

        join()没有时间参数,那么就得等加入的线程执行完毕才能执行;如果有时间参数,那么就相当于等待指定时间之后,继续执行;

public class JoinTest {
    public static void main(String[] args) {
        final Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(getName() + "...aaaaaaaaaaaaa");
                }
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if (i == 2) {
                        try {
                            // t1.join();
                            t1.join(100); // 插队指定的时间,过了指定时间后,两条线程交替执行
                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                    }
                    System.out.println(getName() + "...bb");
                }
            }
        };

        t1.start();
        t2.start();
    }

}

4.6 礼让线程

        static void yield()    暂停当前正在执行的线程对象,并执行其他线程。 

        礼让线程的方法,并没有指定暂停时间,从我们之后的图例可以看出来,礼让线程并没有阻塞;直接从运行状态转换为就绪状态;不像之前的sleep()、join()方法,会阻塞;

        那么礼让线程直接从运行状态切换为就绪状态,那么就存在一种可能性,立马被调用;

public class YieldTest {

    /**
	 * yield让出cpu礼让线程
	 */
    public static void main(String[] args) {
        new MyThread1().start();
        new MyThread2().start();
    }

}

class MyThread1 extends Thread {
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            if (i % 10 == 0) {
                Thread.yield(); // 让出CPU
            }
            System.out.println(getName() + "..." + i);
        }
    }
}

class MyThread2 extends Thread {
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            System.out.println(getName() + "..." + i);
        }
    }
}

        可以看到有的让了,有的没有让,这是为什么嘞,我们来看一下yield()方法的源码注释,第一行就给了答案:

        A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

        提示调度程序,当前线程愿意放弃当前对处理器的使用。调度器可以忽略这个提示。

4.7 设置线程的优先级

        void setPriority(int newPriority)     更改线程的优先级。 

        int getPriority()     返回线程的优先级。 

        IllegalArgumentException - 如果优先级不在 MIN_PRIORITY 到 MAX_PRIORITY 范围内。

        每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。main线程默认具有普通优先级。
        /**
        * 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;

        注:
                同优先级线程组成先进先出队列(先到先服务),使用时间片策略;
                对高优先级,使用优先调度的抢占式策略:高优先级的线程抢占CPU;
                线程创建时继承父线程的优先级;
                低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用;

public class PriorityTest {

    /**
	 * @param args
	 */
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName() + "...aaaaaaaaa");
                }
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName() + "...bb");
                }
            }
        };

        // t1.setPriority(10); 设置最大优先级
        // t2.setPriority(1);

        t1.setPriority(Thread.MIN_PRIORITY); // 设置最小的线程优先级
        t2.setPriority(Thread.MAX_PRIORITY); // 设置最大的线程优先级

        t1.start();
        t2.start();
    }

}

4.8 线程终止(了解)

        由于实际的业务需要,常常会遇到需要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的做法是设置一boolean型的变量,当条件满足时,使线程执行体快速执行完毕。如:

        线程终止的三种方式:https://www.cnblogs.com/liyutian/p/10196044.html

        现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。

        需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。

        也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。

public class InterruptThread1 extends Thread{

    public static void main(String[] args) {
        try {
            InterruptThread1 t = new InterruptThread1();
            t.start();
            Thread.sleep(200);
            t.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        super.run();
        for(int i = 0; i <= 200000; i++) {
            System.out.println("i=" + i);
        }
    }

}

        从输出的结果我们会发现 interrupt 方法并没有停止线程 t 中的处理逻辑,也就是说即使 t 线程被设置为了中断状态,但是这个中断并不会起作用,那么该如何停止线程呢?

        这就需要使用到另外两个与线程中断有关的方法了:
                public boolean Thread.isInterrupted() //判断是否被中断
                public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

        这两个方法使得当前线程能够感知到是否被中断了(通过检查标志位)。

        所以如果希望线程 t 在中断后停止,就必须先判断是否被中断,并为它增加相应的中断处理代码:

@Override
public void run() {
    super.run();
    for(int i = 0; i <= 200000; i++) {
        //判断是否被中断
        if(Thread.currentThread().isInterrupted()){
            //处理中断逻辑
            break;
        }
        System.out.println("i=" + i);
    }
}

五、线程的生命周期

5.1 图示状态和生命周期

        关于Java中线程的生命周期,首先看一下下面这张较为经典的图:

         上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。

5.2 Java线程的五种基本状态

        JDK中用Thread.State类定义了线程的几种状态。
    
        要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
    
        新建状态(New):当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
                如:Thread t = new MyThread();
    
        就绪状态(Runnable):处于新建状态的线程对象被start()后(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,已经加入了操作系统的任务调度队列,等待被操作系统调度执行,并不是说执行了t.start()此线程立即就会执行;
    
        运行状态(Running):当就绪状态的线程被操作系统的任务调度机制调度到,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;run()方法定义了线程的操作和功能;
    
        阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其再次进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
                1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
                2.同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
                3.其他阻塞: 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    
        死亡状态(Dead):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

状态转换:

        就绪状态转换为运行状态:当此线程得到处理器资源。

        运行状态转换为就绪状态:当此线程主动调用 yield()方法或在运行过程中失去处理器资源。

        运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

        此处需要特别注意的是:当调用线程的 yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

六、面试题

1、理解程序、进程、线程?
    程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
    进程(process)是程序的一次执行过程,或是一个正在执行的程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。
    线程(thread)是进程中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

2、线程的深入理解?
    若一个进程同一时间并发执行多个线程,就是支持多线程的。
    线程作为处理器调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
    一个进程中的多个线程共享相同的内存单元/内存地址空间。
        它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

3、进程和线程的区别(***)?
    1、线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
    2、一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
    3、进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
    4、调度和切换:线程上下文切换比进程上下文切换要快得多;

4、进程的特性?
    动态性: 进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
    并发性: 任何进程都可以同其他进程一起并发执行;
    独立性: 进程是系统进行资源分配和调度的一个独立单位;
    结构性: 进程由程序,数据集和进程控制块三部分组成;

5、当我们实现Runnable接口实现多线程的时候,开启线程调用的Runnable接口实现类的run()还是Thread类的run()?
    调用的是Thread类的run(),在Thread的run()内部判断target是否为null,当不为null的时候,调用Runnable接口实现类的run();

6、线程/操作系统的任务调度方式(***)?
    大部分操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。
    任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态,等待下一个属于它的时间片的到来。
    任务调度方式只提到了两种状态:
        运行状态: 任务(线程)正在执行时的状态。
        就绪状态: 任务等待被执行的状态。
        只有处于就绪状态的线程才可能被任务调用机制调度到。

7、为何不使用多进程而是使用多线程?
    多线程程序的优点:
        1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
        2、提高计算机系统CPU的利用率。
        3、改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
        4、线程廉价,线程启动比较快,退出比较快,对系统资源的冲击也比较小。
    多进程的缺点:
        多重进程,启动慢,开销大,共享数据麻烦,运行过程不可预期,且测试困难。

8、并行和并发?
    并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。
    并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。

9、Java程序运行原理(从JVM角度)?
    通过java命令会启动 java虚拟机。启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
    该进程会自动启动一个 “主线程”,然后主线程去调用某个类的 main方法。所以 main方法运行在主线程中。

10、JVM的启动是多线程的吗(***)?
    多线程的。JVM启动其实至少有三个线程:main主线程,gc垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

11、多线程的创建方式?
    JDK5之前:
        继承Thread类
            1、定义子类继承Thread类
            2、重写从Thread类中继承而来的run(),把线程要做的事情定义在run()里面,run()我们又称之为线程体;
            3、创建子类对象 -> 创建线程对象
            4、通过线程对象调用start(): ① 启动线程 ② 调用run()。
            执行机制:
                子类没有重写start(),调用的父类的start(),父类的start()会启动线程并调用run();
                但是run()在子类中重写了,那么调用的就是子类的run();
        实现Runnable接口
            1、定义实现类实现Runnable接口
            2、实现接口中定义的run(),把线程要做的事情定义在run()里面,run()我们又称之为线程体;
            3、创建实现类的对象
            4、调用Thread类的有参构造器,把实现类对象作为参数传递进去,得到一个线程对象;
            5、通过线程对象调用start(): ① 启动线程 ② 调用run()。
            执行机制:
                Thread类的对象调用start(),会启动线程并调用run();
                这个run()只会调用Thread类的run(),但是Thread的run()里面有个判定,如果target不为null,则调用target的run();
                target就是作为构造器传递进来的Runnable接口类的参数;
    JDK5之后:
        使用Callable接口创建线程
            1、定义实现类实现Callable接口
            2、实现接口中定义的call(),把线程要做的事情定义在call()里面;
            3、创建Callable接口的实现类对象
            4、使用FutureTask类来包装Callable实现类的对象;
            5、此FutureTask对象作为Thread对象的target来创建线程。
            6、通过线程对象调用start(): ① 启动线程 ② 调用call()。
            7、我们可以通过FutureTask对象来获取返回值
通过线程池创建线程

12、继承Thread类 和 实现Runnable接口的对比?
    查看源码的区别:
        a、继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法。
        b、实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法(也就是实现了Runnable接口并重写了run()方法的类中的run()方法)。

    继承Thread:
        好处是:可以直接使用Thread类中的方法,代码简单。
        弊端是:如果已经有了父类,就不能用这种方法。

    实现Runnable接口(推荐):
        好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的,避免了单继承的局限性。多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
        弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂。

13、Thread类和Runnable接口的关系?
    Thread类实现了Runnable接口,并重写了run();

14、什么是守护线程(***)?
    java线程中有两种线程,一种是用户线程(非守护线程),一种是守护线程。
    守护线程是一种特殊的线程,它具有陪伴的含义。当进程中不存在非守护线程了,则守护线程自动销毁。
        典型的就是垃圾回收线程。当进程中没有非守护线程了,则垃圾回收线程没有存在的必要,自动销毁。

15、线程的生命周期(***)?
    新建状态(New):当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
    就绪状态(Runnable):处于新建状态的线程对象被start()后,线程即进入就绪状态。
        处于就绪状态的线程,只是说明此线程已经做好了准备,已经加入了操作系统的任务调度队列,等待被操作系统调度执行,并不是说执行了t.start()此线程立即就会执行;
    运行状态(Running):当就绪状态的线程被操作系统的任务调度机制调度到,此时线程才得以真正执行,即进入到运行状态。
        就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;run()方法定义了线程的操作和功能;
    阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其再次进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
    死亡状态(Dead):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

    新建状态 -> 就绪状态:
        处于新建状态的线程对象被start()后,线程即进入就绪状态。

    就绪状态 -> 运行状态:
        获取CPU的使用权;

    运行状态 -> 就绪状态:
        调用yield()方法;
        失去CPU的使用权;

    运行状态 -> 死亡状态:
        当此线程线程执行体执行完毕 或 发生了异常。

    运行状态 -> 阻塞状态:
        join(): 把指定线程a加入到当前线程,当前线程阻塞;
        sleep(): 当前线程睡眠指定时间,线程阻塞;
        wait(): 当前线程处于等待状态,线程阻塞;
        同步: 同步阻塞

    阻塞状态 -> 就绪状态:
        join(): 加入的线程执行完毕,当前线程从阻塞状态变为就绪状态;
        sleep(): 睡眠时间结束,当前线程从阻塞状态变为就绪状态;
        wait(): 等到另外一条线程唤醒,当前线程从阻塞状态变为就绪状态;
        同步: 同步的代码执行完毕,当前线程从阻塞状态变为就绪状态;

16、阻塞状态的分类(***)?
    根据阻塞产生的原因不同,阻塞状态又可以分为三种:
    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    2.同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    3.其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值