欢迎来到电脑知识学习网,专业的电脑知识大全学习平台!

手机版

什么是多线程技术(java多线程为什么可以直接创建)

网络知识 发布时间:2021-09-28 09:09:29

1 基本概括

什么是多线程技术(java多线程为什么可以直接创建)(1)

2 主要介绍

2.1 线程的概念

现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。

比如在Windows系统中,一个运行的exe就是一个进程。

线程是指进程中的一个执行流程,一个进程中可以运行多个线程。

我们来理解一下下面几个问题:

2.1.1 为啥有了进程还要有线程

有了线程以后,凡是程序涉及到多线任务时,都使用多线程来实现,使用多线程来实现时,线程间的切换和数据通信的开销非常低,正因为开销非常低

因此线程还有另一个名称,叫”轻量级的进程“。总结地讲,说白了线程就是为了多线任务而生的

2.1.2 线程为啥开销比较低

使用多进程来实现程序的多线任务,多线并发运行时,涉及到的是进程间的切换,我们前面就说过,进程间切换时开销非常大。

但是使用多线程来实现多线任务,由于线程本质上它只是程序(进程)的一个函数,只不过线程函数与普通函数的区别是,普通

函数是单线的运行关系,而线程函数被注册为线程后,是多线并发运行

2.1.3 为什么线程间数据通信的开销很低

函数间通信有两种方式:

(1)具有相互调用关系函数来说使用函数传参来通信。

(2)对于没有调用关系的函数来说使用全局变量来通信。

A函数一一>全变变量一一>B函数

进程内部的线程间进行数据共享非常容易,使用全局变量即可,根本不需要调用什么os提供的通信机制,所以线程间通信的开销自然就非常的低。

2.1.4 有了线程后,为啥还用进程

线程是不可能完全替代掉进程的,只有在多线任务时会替代进程,但是运行新程序时,还是必须通过创建子进程来实现

线程的本质是函数,函数运行需要内存空间,这个内存空间怎么来,事实上线程所需内存空间就是进程的内存空间,因此

线程的运行时依赖于进程,如果没有进程所提供的内存空间这个资源,线程根本无法运行

2.2 线程的分类以及实现方式

2.2.1 线程的分类

根据操作系统内核是否对线程可感知,可以把线程分为内核线程用户线程


名称

描述

用户级线程(User-LevelThread, ULT)

由应用程序所支持的线程实现, 内核意识不到用户级线程的实现

内核级线程(Kemel-LevelThread, KLT)

内核级线程又称为内核支持的线程


2.2.2 用户线程(多对一模型)

用户级线程优点

1 可以在不支持线程的操作系统中实现。

2 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多, 因为保存线程状态的过程和调用程序都只是本地过程

3 允许每个进程定制自己的调度算法,线程管理比较灵活。这就是必须自己写管理程序,与内核线程的区别

4 线程能够利用的表空间和堆栈空间比内核级线程多

5 不需要陷阱,不需要上下文切换,也不需要对内存高速缓存进行刷新,使得线程调用非常快捷

6 线程的调度不需要内核直接参与,控制简单。

用户线程的缺点

1 线程发生I/O或页面故障引起的阻塞时,如果调用阻塞系统调用则内核由于不知道有多线程的存在,而会阻塞整个进程从而阻塞所有线程, 因此同一进程中只能同时有一个线程在运行

2 页面失效也会产生类似的问题。

3 一个单独的进程内部,没有时钟中断,所以不可能用轮转调度的方式调度线程

4资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用

用户线程的特点

内核对线程包一无所知。从内核角度考虑,就是按正常的方式管理,即单线程进程(存在运行时系统)

用户线程的实现方式

有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在. 应用程序可以通过使用线程库设计成多线程程序。 通常,应用程序从单线

程起始,在该线程中开始运行,在其运行的任何时刻,可以通过调用线程库中的派生例程创建一个在相同进程中运行的新线程。

用户级线程仅存在于用户空间中,此类线程的创建、撤销、线程之间的同步与通信功能,都无须利用系统调用来实现。用户进程利用线程库来控制

用户线程。由于线程在进程内切换的规则远比进程调度和切换的规则简单,不需要用户态/核心态切换,所以切换速度快。由于这里的处理器时间片分配

是以进程为基本单位,所以每个线程执行的时间相对减少为了在操作系统中加入线程支持,采用了在用户空间增加运行库来实现线程,这些运行库被称

为“线程包”,用户线程是不能被操作系统所感知的。

什么是多线程技术(java多线程为什么可以直接创建)(2)

如图,操作系统调度器管理、调度并分派这些线程。运行时库为每个用户级线程请求一个内核级线程。操作系统的内存管理和调度子系统必须要

考虑到数量巨大的用户级线程。您必须了解每个进程允许的线程的最大数目是多少。操作系统为每个线程创建上下文。进程的每个线程在资源

可用时都可以被指派到处理器内核。

2.2.3 内核线程

内核线程的优点

1 多处理器系统中,内核能够并行执行同一进程内的多个线程

2 如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行(用户级线程的一个缺点)

3 所有能够阻塞线程的调用都以系统调用的形式实现,代价可观

4 当一个线程阻塞时,内核根据选择可以运行另一个进程的线程,而用户空间实现的线程中,运行时系统始终运行自己进程中的线程

5 信号是发给进程而不是线程的,当一个信号到达时,应该由哪一个线程处理它?线程可以“注册”它们感兴趣的信号

内核线程的特点

当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用

内核线程的实现

核线程建立和销毁都是由操作系统负责、通过系统调用完成的。在内核的支持下运行,无论是用户进程的线程,或者是系统进程的线程,他们的创建、撤销、切换都是依靠内核实现的。

线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口. 内核为进程及其内部的每个线程维护上下文信息,调度也是在内核基于线程架构的基础上完成。图2-2(b)说明了内核级线程的实现方式。

内核线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。这被称作”一对一”线程映射,

1 线程的创建、撤销和切换等,都需要内核直接实现,即内核了解每一个作为可调度实体的线程

2 这些线程可以在全系统内进行资源的竞争

3 内核空间内为每一个内核支持线程设置了一个线程控制块(TCB),内核根据该控制块,感知线程的存在,并进行控制

什么是多线程技术(java多线程为什么可以直接创建)(3)

操作系统调度器管理、调度并分派这些线程。运行时库为每个用户级线程请求一个内核级线程。操作系统的内存管理和调度子系统必须要考虑到数量巨大的用户级线程。

您必须了解每个进程允许的线程的最大数目是多少。操作系统为每个线程创建上下文。进程的每个线程在资源可用时都可以被指派到处理器内核。

2.2.4 组合方式

什么是多线程技术(java多线程为什么可以直接创建)(4)

线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行. 一个应用程序中的多个用户级线程被映射到一些(小于或等于用户级线程的数目)内核级线程上。

下图说明了用户级与内核级的组合实现方式, 在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合

2.2.5 用户级线程和内核级线程的区别

1 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。

2 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;

而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。

3 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。

4 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的

轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。

5 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

2.3 线程的状态

2.3.1 线程状态

1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。

2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的

start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。

同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5. 超时等待(TIMED_WAITING)

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

6. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

2.3.1 线程状态详解

1 新建(New)状态
    当程序使用new关键字创建了一个线程之后,该线程就处于 新建状态,此时的线程情况如下:
        此时JVM为其分配内存,并初始化其成员变量的值;
        此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体;
2 就绪(Runnable)状态
    当线程对象调用了start()方法之后,该线程处于 就绪状态。此时的线程情况如下:
    此时JVM会为其 创建方法调用栈和程序计数器;
    该状态的线程一直处于 线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为CPU的调度不一定是按照先进先出的顺序来调度的),线程并没有开始运行;
    此时线程 等待系统为其分配CPU时间片,并不是说执行了start()方法就立即执行;
    调用start()方法与run()方法,对比如下:

    调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,
    而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体;
    需要指出的是,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用
    start()方法,否则将引发IllegaIThreadStateExccption异常;
    如何让子线程调用start()方法之后立即执行而非"等待执行":
    
    程序可以使用Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,因为在这1
    毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行;

3 运行(Running)状态

    当CPU开始调度处于 就绪状态 的线程时,此时线程获得了CPU时间片才得以真正开始执行run()方法的线程执行体,则该线程处于 运行状态。
    
如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态;
    如果在一个多处理器的机器上,将会有多个线程并行执行,处于运行状态;
    当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象;
    处于运行状态的线程最为复杂,它 不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,
    目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。线程状态可能会变为 阻塞状态、就绪状态和死亡状态。比如:
        
对于采用 抢占式策略 的系统而言,系统会给每个可执行的线程分配一个时间片来处理任务;当该时间片用完后,系统就会剥夺该线程所占用
    的资源,让其他线程获得执行的机会。线程就会又 从运行状态变为就绪状态,重新等待系统分配资源;
        对于采用 协作式策略的系统而言,只有当一个线程调用了它的yield()方法后才会放弃所占用的资源—也就是必须由该线程主动放弃所占用的
     资源,线程就会又 从运行状态变为就绪状态。
4 阻塞(Blocked)状态

    处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入 阻塞状态。

    当发生如下情况时,线程将会进入阻塞状态:

    线程调用sleep()方法,主动放弃所占用的处理器资源,暂时进入中断状态(不会释放持有的对象锁),时间到后等待系统分配CPU继续执行;
    线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
    线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;
    程序调用了线程的suspend方法将线程挂起;
    线程调用wait,等待notify/notifyAll唤醒时(会释放持有的对象锁);
    阻塞状态分类:

    等待阻塞:运行状态中的 线程执行wait()方法,使本线程进入到等待阻塞状态;
    同步阻塞:线程在 获取synchronized同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态;
    其他阻塞:通过调用线程的 sleep()或join()或发出I/O请求 时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕 时,线程重新转入就绪状态;
    在阻塞状态的线程只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。

    但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。

4.1 等待(WAITING)状态

    线程处于 无限制等待状态,等待一个特殊的事件来重新唤醒,如:

    通过wait()方法进行等待的线程等待一个notify()或者notifyAll()方法;
    通过join()方法进行等待的线程等待目标线程运行结束而唤醒;
    以上两种一旦通过相关事件唤醒线程,线程就进入了 就绪(RUNNABLE)状态 继续运行。

4.2 时限等待(TIMED_WAITING)状态

    线程进入了一个 时限等待状态,如:

    sleep(3000),等待3秒后线程重新进行 就绪(RUNNABLE)状态 继续运行。

5 死亡(Dead)状态

线程会以如下3种方式结束,结束后就处于 死亡状态:

   1 run()或call()方法执行完成,线程正常结束;
   2 线程抛出一个未捕获的Exception或Error;
   3 直接调用该线程stop()方法来结束该线程—该方法容易导致死锁,通常不推荐使用;
    处于死亡状态的线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。
    如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

    所以,需要注意的是:

    一旦线程通过start()方法启动后就再也不能回到新建(NEW)状态,线程终止后也不能再回到就绪(RUNNABLE)状态。

5.1 终止(TERMINATED)状态
    
线程执行完毕后,进入终止(TERMINATED)状态。

2.4 线程的创建过程

下面这副图描述了线程从创建到消亡之间的状态

什么是多线程技术(java多线程为什么可以直接创建)(5)

2.4 Java中Thread类

2.4.1 属性

private long tid:线程的序号
private volatile char name[]:线程的名称
private int priority:线程的优先级
private boolean daemon = false:是否是守护线程
private Runnable target:该线程需要执行的方法

2.4.2 方法

1.start(): 启动当前线程: 调用当前线程的run()
2.run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread(): 静态方法,返回执行当前代码的线程
4.getName(): 获取当前线程的名字
5.setName(): 设置当前线程的名字
6.yield(): 释放当前CPU的执行权
7.join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态, 直到线程b完全执行完以后,线程a才结束阻塞状态
8.stop(): 已过时。当执行此方法时,强制结束当前线程。
9.sleep(long millitime):让当前线程"睡眠"指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
10.isAlive(): 判断当前线程是否存活
线程的优先级:MAX_PRIORITY: 10MIN_PRIORITY: 1NORM_PRIORITY: 5 -->默认优先级
获取线程的优先级和设置线程的优先级:
    getPriority();获取线程的优先级
    setPriority(int p):设置线程的优先级

Thread类中的方法调用到底会引起线程状态,如图

什么是多线程技术(java多线程为什么可以直接创建)(6)

2.4.3 sleep()方法和yield()方的区别

1 sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态;

2 sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常;

3 sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行;

2.4.4 sleep()方法和wait()方的相同与不同

1.相同点:

(1)这两个方法都能使线程进入阻塞状态

2.不同点:

(1)sleep()方法是Thread类中的静态方法;而wait()方法是Object类中的方法;

(2)sleep()方法可以在任何地方调用;而wait()方法只能在同步代码块或同步方法中使用(即使用synchronized关键字修饰的);

(3)这两个方法都在同步代码块或同步方法中使用时,sleep()方法不会释放同步监视器;而wait()方法则会释放同步监视器;

3 简单用例

3.1 操作Main线程

//通过currentThread()来操作Main线程
public class Test extends Thread {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println("Current thread: "   t.getName());
        t.setName("Geeks");
        System.out.println("After name change: "   t.getName());
        System.out.println("Main thread priority: "   t.getPriority());
        t.setPriority(MAX_PRIORITY);
        System.out.println("Main thread new priority: "   t.getPriority());
        for (int i = 0; i < 5; i  ) {
            System.out.println("Main thread");
        }
        ChildThread ct = new ChildThread();
        System.out.println("Child thread priority: "   ct.getPriority());
        ct.setPriority(MIN_PRIORITY);
        System.out.println("Child thread new priority: "   ct.getPriority());
        ct.start();
    }
}

class ChildThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i  ) {
            System.out.println("Child thread");
        }
    }
}

3.2 线程死锁

//Thread.currentThread().join()的声明就告诉Main线程要等待这个线程(例如等待自己)。
//因此Main线程在等待他自己挂掉,也就是死锁么。
public class Test {
    public static void main(String[] args) {
        try {
            System.out.println("Entering into Deadlock");
            Thread.currentThread().join();
            System.out.println("This statement will never execute");
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4 线程会出现的问题

1. 什么是Java线程转储(Thread Dump),如何得到它?
2. 什么是死锁(Deadlock)?如何分析和避免死锁?
3. sleep方法和wait方法的相同点和不同点? 
4. 用户线程与内核线程的区别
责任编辑:电脑知识学习网

网络知识