一、线程和进程
1、进程
分配资源最基本的单位,内存,硬盘,cpu时间片等都是根据进程来分配的。
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。
进程可以理解为一个程序,例如idea,QQ等应用程序,进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)。
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
2、线程
轻量级的进程,线程可以共享进程中的资源,内存资源,cpu时间片。
线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
3、进程与线程的区别
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
进程拥有共享的资源,如内存空间等,供其内部的线程共享。
进程间通信较为复杂:
同一台计算机的进程通信称为 IPC(Inter-process communication)
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
4、进程间通信的方式
1)管道(pipe)及有名管道(named pipe)
管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2)信号(signal)
信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
3)消息队列(message queue)
消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
4)共享内存(shared memory)
可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
5)信号量(semaphore)
主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
6)套接字(socket)
这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
5、 线程的同步与互斥
线程同步:是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。 例如一个请求发送到服务端,这个请求的线程就需要等待数据库的数据查询返回。
线程互斥:是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源,再被唤醒。线程互斥可以看成是一种特殊的线程同步。
四种线程同步互斥的控制方法:
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
互斥量:为协调共同对一个共享资源的单独访问而设计的。
信号量:为控制一个具有有限数量用户资源而设计。
事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
6、 上下文切换(Context switch)
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
每个任务会被分配给处理器10ms-100ms的时间片,用来执行任务,处理器会在多个任务之间进行切换,在切换的过程中,就需要上下文切换,用来保存执行完的线程数据,指令执行位置和恢复将要执行的线程数据。
多个处理器可以并行执行多个任务,就单个处理器而言,当不需要处理器处理的任务时(例如i/o操作),处理器可以去执行其他任务,成为并行。
进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源。 上下文是CPU寄存器和程序计数器在任何时间点的内容。 寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值 的快速访问来加快计算机程序的执行。 程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。
上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
1)暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
2)从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
3)返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程
如果一个程序中频繁的出现上下文切换,是非常影响性能的,因为上下文切换需要进入到内核态。
上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。
上下文切换是多任务操作系统的一个基本特性。在多任务操作系统中,多个进程似乎同时在一个CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次 或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃它们在CPU中的时间,或者是调度器在进程耗尽其CPU时间片时进行切换的结果。
上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本, 实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。
二、用户态和内核态
1、内核态
内核态是用来保护操作系统的,如果应用软件可以随意使用和篡改系统程序和硬件,那么系统会很容易被攻击或者崩溃,这是非常危险的。
在内核模式下,执行代码可以完全且不受限制地访问底层硬件,它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别,最受信息的功能保留,内核模式下的崩溃是灾难性的,会让整个电脑瘫痪。
2、用户态
在用户模式下,执行代码不能直接访问硬件或者引用内存,在用户模式下运行的代码必须委托给系统api来访问硬件或内存,由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的,在计算机上运行的大多数代码都运行在用户模式下。
在X86架构中,内核运行在Ring0,用户运行在Ring3。如果用户态的程序需要创建线程,那就需要调用内核态的库函数,来让操作系统创建线程。
应用程序一般会在以下几种情况切换到内核模式:
1)系统调用:创建线程,加锁解锁,上线文切换
2)异常事件:当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
3)设备中断:在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件,如果原来在用户态,则自然会切换到内核态。
3、CAS
比较与交换,例如一个原始值V = 5,如果想把这个原始值做修改,那么就需要先做比较,用预期值E与原始值V做比较,如果相同 E = V = 5,那么可以修改原始值为最新值,如果不同则修改失败。
这个过程中包括一个读操作,一个写操作,本身这两个操作加在一起不是原子的,CAS会保证这两个操作的原子性。
三、线程生命周期
操作系统层面的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
1、初始状态
指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层 面,真正的线程还没有创建,即Thread对象在java中被new 出来了,但是还没有调用start方法。
2、可运行状态
指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
3、运行状态
当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
4、休眠状态
运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件 (例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态,线程会被唤醒。
5、终止状态
线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了,所以线程是否已经被调度运行对于jvm来说是感知不到的,在java的thread类中,统一称为runnable状态。
6、 Java线程的生命周期

在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态, 即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。
new状态仅仅是java创建出了一个线程对象,操作系统并没有创建线程。
runnable状态,在线程对象调用了start方法之后的状态。
休眠状态分为 blocked,waiting,timed_waiting。
blocked状态针对于synchronized,多个线程请求共享资源,如果线程没有获取到资源或者线程先被阻塞后再被唤醒时再去获取锁也没有获取到时,都是bolcked状态。
waiting状态,调用wait(),join(),park()方法会进入等待状态,无时限。
timed_waiting状态,有时间限制的等待。
terminated状态,线程的终止状态。
四、JAVA线程
1、Java线程的实现方式
方式1:使用 Thread类或继承Thread类
Thread t = new Thread(){public void run(){// TODO}};t.start();
方式2:实现 Runnable 接口配合Thread,任务和线程解耦,没有返回值,也不会抛异常
Runnable runnable = new Runnable(){public void run(){// TODO}};Thread t = new Thread(runnable);t.start();
以上两种方式本质是相同的,都是调用传入的runnable的run方法。
方式3:使用有返回值的 Callable,有返回结果,可以抛出异常
class CallableTask implements Callable<Integer>{@Overridepublic Integer call() throws Exception{return 1;}}ExecutorService service = Executors.newFixedThreadPool(10);Future<Integer> future = service.submit(new CallableTask());log.info("=================================================");FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call throws Exception{return "";}});new Thread(task).start();
线程池也是用线程池工厂创建thread对象。
方式4:使用 lambda,因为Runable 标注了 @FunctionalInterface 注解,是一个函数接口
new Thread(() ‐> System.out.println(Thread.currentThread().getName())).start();
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法,执行任务的方式有很多种。
2、Java线程实现原理
1)run方法和start方法
在线程对象创建后,会调用线程的start()方法来执行任务的run()方法,如果直接执行线程对象的run()方法或者任务的run()方法,则实际上是对象调用,因为start()方法是调用底层系统创建一个线程,而不调用start()方法之前,仅仅是一个java对象。
在调用Thread对象的start()方法后,调用了start0(),这是一个本地方法,会去调jvm的创建thread方法创建JavaThread对象,然后调用线程库,jvm根据操作系统调用不同的库,liunx系统中,是让系统来创建内核线程,调用pthread_create来创建一个真正的线程,在系统创建完线程后会初始化,初始化完成后会阻塞该线程。
初始化完成后,目前操作系统的线程和jvm级别的JavaThread有创建关系,这时需要把java级别的Thread对象和jvm级别的对象进行绑定,这样java级别的对象就和操作系统的线程间接绑定。
然后把阻塞的线程唤醒,调用java对象的run方法。
创建线程存在系统调用,需要jvm调用系统的库函数,所以存在用户态和内核态的转换,成本很高。
2) Java线程属于内核级线程
JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本 都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线 程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。
3)协程
协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
线程内可以开多个协程来处理程序,多个协程可以同时执行,可以用来处理i/o任务来提高效率。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用 了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。而协程的调用和子程序不同。协程 在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。
def A():print '1'print '2'print '3'def B():print 'x'print 'Y'print 'z'
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断 再去执行A,结果可能是:1 2 x y 3 z。
线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。
3、Java线程的调度机制
1) 协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
2) 抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
在JAVA中就是使用这种方式来调度的,可以通过设置线程优先级来完成。优先级分十级,但是这种配置并不明显,因为是java级别的配置,就算优先级比较低的线程也是有机会执行的,避免饥饿问题,优先级较高的线程会有大概率执行。(Thread.MIN_PRIORITY至 Thread.MAX_PRIORITY)
Thread thread1 = new Thread(demo,"thread1");Thread thread2 = new Thread(demo,"thread2");//priority优先级默认是5,最低1,最高10thread1.setPriority(Thread.MAX_PRIORITY);thread2.setPriority(Thread.MIN_PRIORITY);
4、Thread常用方法
1)sleep方法
调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁。
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志。
睡眠结束后的线程未必会立刻得到执行。
sleep当传入参数为0时,和yield相同。
2)yield方法
yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高 (至少是相同)的线程获得执行机会,不会释放对象锁。
假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程。
具体的实现依赖于操作系统的任务调度器。
3)join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
4)stop方法
stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。stop方法会让线程释放锁,如果一个线程在同步块中,此时被stop方法停止,那么其他线程可以获取锁,然而被终止的线程并没有正常的执行完同步块中的逻辑释放锁资源,所以容易导致数据不一致的问题。
5、 Java线程的中断机制
Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
interrupt(): 将线程的中断标志位设置为true,不会停止线程。
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位。
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志 位,重置为fasle。
Thread thread = new Thread(new Runable(){@Overridepublic void run(){//Thread.interrupted() 清除中断标志位//Thread.currentThread().isInterrupted() 不会清除中断标志位// Thread.sleep(1000) 如果标记了中断标志位会被唤醒,并清空中断标志位if (Thread.currentThread().isInterrupted()){// 结束线程}}});thread.start();//不会停止线程,只会设置一个中断标志位 flag=truethread.interrupt();
sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
如果在循环中有以上两个方法的调用,会清空中断标志位,所以需要捕获异常并把中断状态改回来。
可以使用判断中断标志位来优雅的结束线程。
6、 Java线程间通信
1)volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
2)等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒。
wait和notify是基于monitor来实现的,所以必须在synchronized同步块中使用,而且如果有多个线程在等待,而notify并没有指定唤醒哪一个等待的线程,所以有可能导致想要唤醒的线程没有被成功唤醒,一般使用notifyAll方法来唤醒等待的线程。
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
3)管道输入输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节, 而后两种面向字符。
4)Thread.join
join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等 待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但 是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串 行的,最后join的实现其实是基于等待通知机制的。
