线程基础知识笔记

概述

现代操作系统调度CPU的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

现在我们了解一下系统的用户空间和内核空间两个概念:

虚拟内存被操作系统划分成两块:内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态,为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。说起这个概念就是因为线程上下文切换的概念。虽然线程上下文切换比进程切换成本要低但是,线程切换也是很影响性能的。线程上下文切换就涉及用户态到内核态的转换。

线程的实现可以分为两类:1、用户级线程(User-Level Thread)   2、内核线线程(Kernel-Level Thread);而java线程就是内核级线程。下面我们着重说一下这两个概念。

用户线程

不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少

优点:

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

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

创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。

允许每个进程定制自己的调度算法,线程管理比较灵活。

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

同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。

缺点:

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

内核线程

由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。

优点:当有多个处理机时候,一个进程的多线程可以同时执行。

缺点:由内核进行调度。

ULT和KLT之间的关系

原理如下图所示:

程序执⾏的时候实际上分为两种状态,这个状态会被⼀条线划分,上⾯称之为⽤户态,下⾯称之为系统/内核态。⽤户态执⾏的都是我们⾃⼰写的代码,⽐如我们做的登录、⽤户CPU时间⽚分配⽅式。但是这些都是由操作系统做⽀持的,操作系统⽀持的时候就得进⼊系统态。举个例⼦调⽤⽂件读写操作,实际上是调⽤类似 open 的 API,这个API最终是由操作系统实现的,操作系统实际上会把API翻译成具体的系统调⽤ syscall,然后在操作系统⾥⾯执⾏⼀些代码,所以说这个代码实际上分为⽤户态代码和系统
态代码。当从⽤户态代码进⼊系统态代码调⽤的时候会涉及到上下⽂切换,这是要付出⼀定的代价的。很显然系统线程去创建去调度是要付出这些代价的,所以很多时候系统线程成本会⾮常的⾼,当我们频繁的去创建系统线程销掉系统线程这种代价实在太⼤了。以上这些都是选择性摘抄和记录,仅凭参考。

JAVA线程

 现在咱们来说一下JAVA线程。

下面是创建线程的几种方式:

 1 import java.util.concurrent.*;
 2 
 3 /**
 4  * @program: ConCurrentHashMap_TEST1.8
 5  * @description: ${description}
 6  * @author: Mr.Wang
 7  * @create: 2016-12-29 12:00
 8  **/
 9 
10 public class TestThread {
11     public static void main(String[] args) throws ExecutionException, InterruptedException {
12         Thread thread = new SampleThread();
13         thread.start();
14         final SampleThread1 thread1= new SampleThread1();
15 
16         SampleThread2 thread2 = new SampleThread2();
17         FutureTask oneTask = new FutureTask(thread2);
18         Thread oneThread = new Thread(oneTask);
19         oneThread.start();
20         System.out.println(oneTask.get());
21 
22         //lambda 抛异常
23         FutureTask<Integer> task = new FutureTask<Integer>(()->{
24            throw new Exception("");
25         });
26         new Thread(task).start();
27         System.out.println(task.get());
28 
29         ExecutorService es = Executors.newCachedThreadPool();
30 
31         es.execute(new Runnable() {
32             @Override
33             public void run() {
34                 //
35             }
36         });
37     }
38 
39 }
40 //集成Thread方法,实现start方法
41 class SampleThread extends Thread{
42     public void run(){
43         System.out.println("This is a thread");
44     }
45 }
46 //实现ruannable方法
47 class SampleThread1 implements Runnable {
48     @Override
49     public void run() {
50         System.out.println("This is a thread1");
51     }
52 }
53 //实现 Callable 接口的 call 方法,用 FutureTask 类包裹 Callable 对象。
54 // 然后再用 Thread 类包裹 FutureTask 类,并调用 start 方法。
55 // call() 方法可以有返回值
56 class SampleThread2 implements Callable{
57 
58     @Override
59     public String call() throws Exception {
60         return "This is a thread2";
61     }
62 }

Java线程的生命周期:

休眠状态(BLOCKED、WAITING、TIMED_WAITING)与RUNNING状态的转换

1、RUNNING状态与BLOCKED状态的转换

线程等待 synchronized 的隐式锁,RUNNING —> BLOCKED

线程获得 synchronized 的隐式锁,BLOCKED —> RUNNING

2、RUNNING状态与WAITING状态的转换

获得 synchronized 隐式锁的线程,调用无参数的Object.wait()方法

调用无参数Thread.join()方法

调用LockSupport.park()方法,线程阻塞切换到WAITING状态,

调用LockSupport.unpark()方法,可唤醒线程,从WAITING状态切换到RUNNING状态

3、RUNNING状态与TIMED_WAITING状态的转换

调用带超时参数的 Thread..sleep(long millis)方法

获得 synchronized 隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法

调用带超时参数的Thread.join(long millis)方法

调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)方法

调用带超时参数的LockSupport.parkUntil(long deadline)方法

上下文切换

切换过程:

在理解这个之前,要理解程序计数器的作用(以下摘抄来自《深入理解java虚拟机》):

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。且由于java虚拟机的多线程是通过线程轮流切换并分配器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器是一个内核)都只会执行一条线程中的指令,因为为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,我们称这类区域为“线程私有”的内存。

线程切换时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。

总结

感谢网络大神的分享,

https://www.zhihu.com/question/20998226

https://blog.csdn.net/qq_38998213/article/details/87688929

https://www.cnblogs.com/winclpt/articles/10873024.html

猜你喜欢

转载自www.cnblogs.com/boanxin/p/12114368.html