从0开始深入理解并发、线程与等待通知机制(上)进程和线程 进程 : 是操作系统进行资源分配的最小单位 。一个进程是一个程序的一次执行过程。每启动一个进程,操作系统就会为它分配一块独立的内存空间,用于存储PCB、数据段、程序段等资源。每个进程占有一块独立的内存空间。 在操作系统没有引入进程之前,由于CPU一次只能执行一个程序,所以多个程序只能顺序执行,而CPU的速度很快,磁盘、网路等IO的速度很慢,造成CPU会有大量空闲的时间,此时CPU的利用率很低,为了解决CPU的利用率低的问题,操作系统引入了进程以及中断处理,实现了在同一时间段内,多个程序的并发执行,这个程序执行一点,那个程序执行一点,这样并发交替的执行大大提高了CPU的利用率。 线程 :线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 面试题:进程之间的通信? 1:管道,有匿名管道和命名管道。 当一个进程fork出一个子进程的时候,这时候双方都知道对方存在,具有亲缘关系的两个进程的通信,可以用匿名管道。 当两个进程没有亲缘关系,就应该用命名管道。 2:信号,信号分发,用于通知进程有某事发生。 3:消息队列,就和我们平常用mq差不多,创建一个内存队列,其他进程往这个内存队列发送消息。 4:共享内存,多个进程访问同一块内存空间。 这种方式需要依赖某种操作,比互斥和信号等。 5:信号量,为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。 6:套接字:socket通信。 CPU核心数和线程数的关系 同一时刻,一个cpu只能运行一个线程。cpu内核和线程同时运行的关系是1:1,inter引入 超线程技术 后,内核和cpu的关系是1:2,那么在设置最优线程数的时候应该是cpu*2。避免cpu不停的上下文切换,也就是我们平常用到最多的设置方式:Runtime.getRuntime().availableProcessors()*2 上下文切换 操作系统将线程或者进程从cpu调度出去的时候,会把当前线程、进程的cache数据保存出去,就比如说CPU寄存器和程序计数器。这个过程叫做上下文切换。它的代价是相对比较大的。引发上下文切换有线程切换,进程切换,系统调用… …。对一个简单的命令cpu处理,需要大概几个或者几十个时钟周期。上下文切换大概需要5000 - 20000个始终周期。synchronized是有锁操作,cas是无锁操作。cas一般来说是比synchronized性能好点,但是不停的cas上下文切换也会浪费性能,导致比synchronized更差。 这里的CPU寄存器包括: 程序记数寄存器:跟踪程序执行的准确位置 堆栈指针寄存器:指示操作栈项 框架寄存器:指向当前执行的环境 变量寄存器:指向当前执行环境中第一个本地变量 并行和并发 并发 :指应用能够交替执行不同的任务,比如单个cpu下多线程执行并不是同时执行,而是不停的上下文切换这两个任务,以达到‘同时‘执行的效果,只是切换的较快肉眼感觉不到。 并行 :指应用能同时执行不同的任务。比如说两个线程分别由两个不桶的cpu去执行,就叫做并行。 创建线程的几种方式 创建线程: 从底层代码上看,只有一种,当只有调用Thread类的native start0方法内核创建线程然后内核返回来调用run方法。start()->JVM_StartThread -> new JavaThread->os::start_thread -> run() 从java源码注释上说有两种,一种是new Thread另一种是实现Runnable方法(Runnable底层也是Thread start0创建的线程) 从应用程序,根据不同的需求派生出大概有五种(也有可能多种)方式如下 方式一:继承于Thread类 方式二:实现Runnable接口 方式三:实现Callable接口,Future,RunnableFuture 方式四:使用线程池 方式五:使用匿名类 Thread和Runnable的区别 : Thread才是Java里对线程的唯一抽象。Runnable是对任务的。Thread可以接受任意一个Runnable实例并执行 方式一 :继承Thread类 image-20230227165501497 static class ThreadTest extends Thread{ @Override public void run(){ System.out.println("继承Thread。"+currentThread().getName()+":正在运行。"); } } 方式二:实现Runnable接口 static class RunnableTest implements Runnable{ @Override public void run() { System.out.println("实现Runnable。"+Thread.currentThread().getName()+":正在运行。"); } } 方式一和方式二都是没有返回值的。callable是有返回值的 方式三:实现callable接口 static class CallableTest implements Callable { @Override public Object call() throws Exception { System.out.println("实现callable。" + Thread.currentThread().getName() + ":正在运行"); return "随便返回了"; } } public static void main(String[] args) throws Exception { CallableTest callableTest = new CallableTest(); FutureTask futureTask = new FutureTask(callableTest); new Thread(futureTask).start(); Object o = futureTask.get(); } 其实callable底层也是实现了Runnable接口,只不过它callable写了具体的逻辑方法是call(),将call()方法传递给了FutureTask类中,FutureTask类中保存着call方法和实现了Runnable的run方法,还有线程的状态和返回值变量。 当调用futureTask.get()的时候判断线程是否执行完成并且返回它自己保存的返回值变量。 如果没有执行完成就调用park。线程的执行是调用了FutureTask实现的run方法,run方法里边调用了call方法,在finally里边调用unpark,也就是把调用get方法的线程unpark掉,并返回值。 方式四: ExecutorService service = Executors.newFixedThreadPool(10); service.execute(new ThreadTest()); service.shutdown(); 方式五: 其实就是8的特性 Thread thread = new Thread(()->{ System.out.println(111); }); 综上案例真正创建线程还是Thread.run方法。其他的都是衍生品。 如何安全的终止线程 1:代码执行完成 2:抛出异常 3:stop(废弃的,但是他不会释放任何资源,比如IO,网卡,锁… 这些资源不会被释放,占着茅坑不拉屎,当我们在写文件时候,正确打开了io,调用stop后没有调用IO的结束符,导致文件损坏), suspend挂起线程(cpu不在执行,只有当有人唤醒它才会继续执行,和stop一样都不会释放资源) 4:最好的方式是中断,中断信号。调用线程的interrupt,interrupt是jvm中线程类的一个变量。 当其他线程调用线程A的interrupt的时候只是将线程A这个变量改为true,然后线程A判断isInterrupt判断是否有中断信号,线程A也可以不理会,看心情。 Thread类中interrupt()、interrupted()和isInterrupted()方法介绍 interrupt();将线程状态设置成true /** * 中断此线程。 *线程可以中断自身,这是允许的。在这种情况下,不用进行安全性验证({@link #checkAccess() checkAccess} 方法检测) *
若当前线程由于 wait() 方法阻塞,或者由于join()、sleep()方法,然后线程的中断状态将被清除,并且将收到 {@link InterruptedException}。 *
如果线程由于 IO操作({@link java.nio.channels.InterruptibleChannel InterruptibleChannel})阻塞,那么通道 channel 将会关闭, * 并且线程的中断状态将被设置,线程将收到一个 {@link java.nio.channels.ClosedByInterruptException} 异常。 *
如果线程由于在 {@link java.nio.channels.Selector} 中而阻塞,那么线程的中断状态将会被设置,它将立即从选择操作中返回。 *该值可能是一个非零值,就像调用选择器的{@link java.nio.channels.Selector#wakeupakeup}方法一样。 * *
如果上述条件均不成立,则将设置该线程的中断状态。 *
中断未运行的线程不必产生任何作用。 * @throws SecurityException 如果当前线程无法修改此线程 */ public void interrupt() { //如果调用中断的是线程自身,则不需要进行安全性判断 if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // 只是设置中断标志 b.interrupt(this); return; } } interrupt0(); } interrupted(); //将线程归为,设置为false isInterrupted()//返回档期那线程的状态 优雅的通过线程信号中断线程 : 就是线程A不断的while判断isInterupted()然后做出相应。最后还得interrupted(),将线程归为。 那么我可以用自定义的变量来代替Interrupt ? 不建议! 1:Interrupt是jvm源码里边Thread类里的一个变量。是每个线程独有的。 2:当我们调用Thread.sleep,阻塞队列中take,pull,future.get… …这些方法是会阻塞的,是被cpu给挂起来了的。那我们自定义的变量是无法中断这些阻塞的。就比如这些方法都会抛出一个中断异常,InterruptExcetion所有的阻塞类的都会抛出这个异常,当阻塞的时候,被别的线程调用interrupt的时候会抛出这个异常,可以快速的相应。自定义的不能快速相应只有当不阻塞的时候才会相应。 需要补充sleep的时候抛出Interrupt 当执行两次start的时候会怎样? 在调用start方法的时候线程的状态会变化,在执行start的时候会判断这个状态。 当我们new Thread的时候只是在JVM堆里边创建一个内存变量。只有在调用start()->start0()方法的时候才会真正的和操作系统的线程挂上钩,才会真正的创建和运行线程,然后这个线程才会执行我们的run方法,start0()->JVM_StartThread -> new JavaThread->os::start_thread -> run()。