进程
线程
进程与线程的区别
进程间通信的方式
线程的同步互斥
四种线程同步互斥的控制方法
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源。
上下文是CPU寄存器和程序计数器在任何时间点的内容。
寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。
程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。
上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。
上下文的切换是有时间损耗的,会对性能有影响。如果在程序中不断的创建线程,那么这些线程之间的上下文切换会对系统性能造成很大的影响,因此要考虑线程复用,也就是利用到线程池区创建线程。
上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本, 实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。
对于CPU密集型任务(计算密集型)和IO密集型任务的多线程创建,使用到线程池的参数是不一样,这个后续在分析。
通过命令查看CPU上下文切换情况
linux系统可以通过命令统计CPU上下文切换数据
#可以看到整个操作系统每1秒CPU上下文切换的统计
vmstat 1
其中cs列就是CPU上下文切换的统计。当然,CPU上下文切换不等价于线程切换。
查看某一个线程\进程的上下文切换
使用pidstat命令
常用的参数:
-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID
其中cswch表示主动切换,nvcswch表示被动切换。如果说该进程每秒主动切换次数很多的话,代码中可能存在大量的睡眠\唤醒操作。
从进程的状态信息中查看
通过命令 cat cat /proc/7059/status
查看进程的状态信息
这2项就是该进程从启动到当前总的上下文切换情况。
操作系统层面的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
这五种状态在不同的编程语言当中有所不同。这属于操作系统层面的线程状态。
Java 语言中线程共有六种状态,分别是:
new Thread的时候 出入NEW 状态,调用start()方法的时候处于RUNNABLE状态,BLOCKED是针对synchronized来说的,
当调用wait()、join()、LockSupport.part()没有参数的方法是状态是WAITING 。带有参数时是TIMED_WAITING状态。线程异常或者执行结束后处于TERMINATED。
在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态, 即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。
思考一下java中创建线程有几种方式
1.使用 Thread类或继承Thread类
Thread t = new Thread() {
public void run() {
System.out.println("通过Thread创建的线程执行");
}
};
t.start();
2.实现 Runnable 接口配合Thread
把【线程】和【任务】(要执行的代码)分开 Thread 代表线程 Runnable 可运行的任务(线程要执行的代码)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过Runnable接口实现的线程执行");
}
});
t2.start();
3:使用有返回值的 Callable
public class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
Future<Integer> future = fixedThreadPool.submit(new CallableTask());
try {
Integer integer = future.get();
System.out.println(integer);
}catch (Exception e){
e.printStackTrace();
}
}
}
4利用lambda创建线程
new Thread(() ‐ > System.out.println(Thread.currentThread().getName())).start();
看上去有多种创建线程的方法,其实本质上Java中实现线程只有一种方式。
对于使用lambda表达式来创建线程是因为Runnable接口上有@FunctionalInterface
基本上第四种和第一种是一样的只不过写法不同。再来看Thread方式创建线程。
首先Thread实现了Runnable接口。当我们继承Thread或者直接new Thread创建线程的时候,调用是它的run方法。它的run方法中有target的静态变量,这个target在Thread的定义就是一个Runnable。所以Thread的本质还是传入了一个Runnable和第二种通过构造器传入Runnable没有区别。
在来看第三种的通过实现Callable的方式创建线程。这种方式是通过线程池来创建线程的,我们来看看线程池怎么去创建线程。创建线程池有一个工具类Executors
。底层在创建线程池用到了一个ThreadFactory。
ThreadFactory是一个接口,在Executors的实现类是DefaultThreadFactory
。newThread的实现方法如下图
底层实现的方式是通过Thread来创建线程。
总结来说,线程的创建方式只有一种就是通过Thread来创建线程,但是执行任务的方式有多种,但是最后都是通过start()方法来启动线程。
思考下为什么线程需要通过start()方法来启动,而不是调用run()方法
根据上面的问题用个案例来看下
public class ThreadDemo2 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":通过Runnable方式执行任务");
}
};
new Thread(runnable).start();
new Thread(runnable).run();
runnable.run();
}
}
为什么要通过start()来启动线程。来看下执行结果
三种方式都能执行run()方法的任务。但是new Thread(runnable).run();runnable.run();
其实是主线程执行的结果,并没有创建一个线程,只是在main方法中进行对象的调用。那为什么start()方法就能创建一个线程呢。我们来看看这个start()的源码。start()的底层调了 start0()方法, start0()是一个本地方法,这涉及到jvm的层面。
Thread#start()jvm源码执行过程流程图
整个创建线程的流程是这样的:
首先start()方法调用start0()方法。start0()映射到jvm的代码是jvm_StartThread。在new Thread()的时候,如下图
Thread有一个静态块,静态块执行了registerNatives();也是一个本地方法,作用是把线程的方法映射到jvm层面如下图
就是说在这个时候start0()就映射到了jvm_StartThread。执行jvm_StartThread就创建了jvm层面javaThread对象。new javaThread()作用在于屏蔽不同操作系统之间的差异,对不同的操作系统执行不同的创建线程的方法。对于Linux系统来说,最终调了pthread_create方法。看下pthread_create命令的作用。Linux下输入man pthread_create
;
很明显就是创建一个线程。那么从这里就是用户态到内核态的切换了。创建了线程后设置为初始化状态,Linux会自旋的进行判断线程是不是初始化状态,如果是该线程就会调用操作系统层面的wait()
方法进行等待。那么这些过程就是jvm中new javaThread()
的过程,线程创建完成时就是new javaThread()
完成。
接下来把java的线程对象和jvm的线程对象绑定在一起。就是java的Thread和jvm的javaThread进行绑定。
为什么要进行绑定。之前jvm的new javaThread()
操作是在操作系统上创建了一个线程,这样javaThread
就和操作系统的线程建立了关联,但是操作系统的线程和java的线程没有关联,java的Thread和jvm的javaThread进行绑定以后,java线程就和操作系统的线程间接建立了关联。
绑定完成后设置java线程状态为RUNNABLE,然后去唤醒操作系统的线程。唤醒操作系统的线程后,会去调jvm线程的run方法,jvm最后调到java线程的run方法()。
Java线程属于内核级线程
JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本 都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线 程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调 度和抢占式线程调度
协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另 外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
Java线程调度就是抢占式调度。
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至 Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所 以线程调度最终还是取决于操作系统。
package demo;
public class BuyTicketDemo implements Runnable {
private Integer ticket;
public BuyTicketDemo() {
this.ticket = 1000;
}
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在执行操作,余票:" + ticket--);
}
}
Thread.yield();
}
}
public static void main(String[] args) {
BuyTicketDemo demo = new BuyTicketDemo();
Thread thread1 = new Thread(demo,"Thread1");
Thread thread2 = new Thread(demo,"Thread2");
Thread thread3 = new Thread(demo,"Thread3");
Thread thread4 = new Thread(demo,"Thread4");
//给2和4最高的优先级 1和3最低的优先级
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread3.setPriority(Thread.MIN_PRIORITY);
thread4.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
从执行结果来看,优先级有点用但又不完全有用,优先级高的24不代表第一次就是这两个线程,13优先级低也不代表没有机会执行。
sleep方法
调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志 睡眠结束后的线程未必会立刻得到执行 sleep当传入参数为0时,和yield相同
yield方法
yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高 (至少是相同)的线程获得执行机会,不会释放对象锁; 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程; 具体的实现依赖于操作系统的任务调度器
不会释放锁对象是什么意思,比如一个线程通过synchronized加锁以后,如果通过sleep阻塞了之后,不会释放锁,其他线程依然获取不到加锁的资源。如果通过wait方式阻塞的话,会释放锁,其他线程能够获取加锁的资源。这个后续在分析。
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之 后才能继续运行的场景。
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t begin");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t finished");
}
});
long start = System.currentTimeMillis();
t.start();
//main线程阻塞等待t线程完成
t.join();
System.out.println("执行时间:"+(System.currentTimeMillis() - start));
System.out.println("main 线程完成");
}
}
stop方法
stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。会释放锁对象
那么如何正确优雅的停止线程?
Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一 种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
优雅的停止线程和线程的中断机制有关。
java中断机制就是说,在线程当中添加了一个中断标志为true,但是具体什么时候判断中断标志,什么时候停止线程需要自己决定,这是比较安全的停止线程的方式。可能听起来不好理解。来看两个例子
API的使用
interrupt(): 将线程的中断标志位设置为true,不会停止线程
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
案例1:
package demo;
public class ThreadStopDemo {
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
i++;
System.out.println(i);
//判断当前线程的中断标志
if (Thread.currentThread().isInterrupted()){
//如果中断标志为true 打印=======
System.out.println("=========");
}
if(i==10){
break;
}
}
}
});
//线程开始执行
t1.start();
//给线程添加中断标志为true
t1.interrupt();
}
}
当我们给线程设置中断标志,那么在线程中需要编写代码判断这个中断标志。上述代码运行结果是打印====10次
把Thread.currentThread().isInterrupted() 换成Thread.interrupted()。上面说了Thread.interrupted()会清除中断标志,所以只打印一次
如果上面代码改成这样
运行结果:
所以用到中断机制的时候遇到sleep、wait
上述两个方法检测到线程中的中断标志为true就会抛出异常并且清除中断标志。因此需要在catch中补上中断标志
运行结果:
案例2:如何优雅中断线程
public class ThreadStopDemo2 implements Runnable{
@Override
public void run() {
int count = 0;
//中断标志为true或者 count 大于100 的时候跳出循环
while (!Thread.currentThread().isInterrupted() && count < 1000){
System.out.println("count = " + count++);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
//重新补上中断标志
Thread.currentThread().interrupt();
}
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadStopDemo2());
thread.start();
Thread.sleep(10);
thread.interrupt();
}
}
给线程添加中断标志,在线程判断中断标志如果为true跳出while循环。看下运行结果
如果没有在catch中重新补上中断标志为的话,在看下结果
会运行到999才会结束。
volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程 之间进行通信。
在之前并发编程分析过了,就不在举例了。
等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒
这个并不是那么好用,有局限性,必须要和synchronized使用,还有就是如果存在多个线程没法指定要唤醒那个线程,不过可以通过notifyAll()唤醒全部线程。
LockSupport
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用 unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
这个是用的比较多的。举个例子
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo1 {
static class AA implements Runnable{
@Override
public void run() {
System.out.println("线程开始执行");
//等待许可
LockSupport.park();
System.out.println("线程执行结束");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new AA());
thread.start();
System.out.println("唤醒AA");
//给AA线程一个许可
LockSupport.unpark(thread);
}
}
管道输入输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程 之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节, 而后两种面向字符。
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class PipedTest {
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException ex) {
}
}
}
}
Thread.join
join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等 待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但 是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串 行的,最后join的实现其实是基于等待通知机制的。
Join方法上面有案例了,就不在举案例了。