juc(一) 线程与进程的基础概念

43 阅读7分钟

一、进程

  1. 进程可以看作是一个程序的实例,比如 启动某个应用就相当于开多了一个进程。
  2. 进程是线程的容器。

二、 线程

  1. 线程是操作系统调度的最小单位
  2. 是进程实际在工作的单位。
  3. 一条线程指的是进程中单一的顺序控制流。
  4. 进程中是有多个线程的,并且共享着进程的全部共享资源。

三、并发与并行

通俗来讲,并发就是操作系统同时应付多条线程,给你用下,给他用下。(表面好像一起走,但实际上是串行,装的);并行就是大家一起同时走。

四、线程的创建方式

1. 直接使用thread

  Thread t1 = new Thread("way1") {
            @Override
            public void run() {
            System.out.println(Thread.currentThread().getName() + " : " +"我是直接使用thread对象创建出来滴!");
            }
        };
        t1.start();

image-20230307150606661.png

这种方式把线程和任务内容糅杂在一起,重写了Thread类下的方法

2. 使用Runnable接口配合Thread()

  Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 我是runnable接口方式");
            }
        };
​
        Thread t1 = new Thread(runnable);
​
        t1.start();
        System.out.println(Thread.currentThread().getName() + " 我是main下的");

image-20230307151144738

image-20230307151144738.png

0. 这种方式是初始化了Runnable接口,再把Runnable作为变量传到thread对象中, 这种方式把线程与任务分开,

  1. 可以使用lamdba表达式简化代码

     Runnable runnable = () -> {
                    System.out.println(Thread.currentThread().getName() + " 我是runnable接口方式");
                };
            };
    

image-20230307151733409.png

3. FutureTask配合Thread

当我们需要从某个线程中获取它的执行接口时,就可以使用这个FutureTask,一般用于线程通信

// integer表示返回结果的类型
FutureTask<Integer> futureTask = new FutureTask<Integer>(() -> {
    System.out.println("我是futureTask方式");
    return 100 * 2;
});
​
Thread t1 = new Thread(futureTask);
t1.start();
System.out.println("t1当前线程的返回结果是" + futureTask.get());

image-20230307152808035.png

五、 查看进程线程的方法

1. windows

1. 查看某个端口下的进程: netstat -ano | findstr 80
2. 查看进程:tasklist
3. 杀死某个进程:taskkill -f -pid 80

2. linux

1. 查看所有进程:ps -fe (如果你要筛选出某个名称的进程:ps -fe | grep java)
2. 杀死进程: kill  pid
3. 查看某个进程下的所有线程信息:ps -ft -p pid
4. 动态显示所有进程:top
5. 动态显示某个进程下的线程信息:top -h -p pid 

3. Java命令

1. 查看所有java进程:jps
2. 查看某个Java进程下的所有线程状态:jstack pid
3. 图形化界面显示某个Java进程下线程的状态:jconsole

六、 线程运行的原理

补充知识
  1. 我们都知道jvm中由堆、栈、方法区组成,其中栈内存就是给线程用,如果我们新开了一条线程,那么jvm就会为其分配一个栈内存。
  2. 其中,栈中有很多个栈帧,一个方法其实对应着一块栈帧,但是只会有一个活动栈帧,就是正在运行的方法。方法执行结束后,栈帧也会消失

1. 线程的上下文切换

线程的上下文切换实际上就是从用cpu到不用cpu的过程。

主要分为四种情况

主动:

1) 线程的cpu时间片用完

2 ) 垃圾回收

3) 有更高优先级的线程需要拿到cpu的使用权

被动:

4) 线程自己调用了sleep、yield、wait、join、park、synchronized、lock 等方法

在线程的上下文切换中,程序计数器发挥着重要的作用,它记录着当前线程执行到哪一行代码,方便后续当前线程恢复运行状态时才可以继续执行下去,因此,程序计数器是线程私有的。其中,频繁的上下文切换会很影响性能。

⭐面试题:

什么是上下⽂切换?

多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使 ⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀ 个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切 换。

概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次 再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。 上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换 中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的 CPU 时间,事 实上,可能是操作系统中时间消耗最⼤的操作。 Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下⽂切换 和模式切换的时间消耗⾮常少

七、线程常见的方法

1. start() && run()

   Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "在执行我");
            }
        };
​
        Thread t1 = new Thread(runnable,"t1");
​
        t1.run();

image-20230308105801329.png

 public static void main(String[] args)  {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "在执行我");
            }
        };

        Thread t1 = new Thread(runnable,"t1");

        t1.start();
    }

image-20230308105833074.png

  • 只有调用了start方法,才会启动新的线程。
  • 如果直接调用run方法,其实是相当于普通方法的调用而已,还是在主线程中,并非新建一个线程。

2. sleep () && yield()

2.1 sleep()

  1. sleep可以认为是有时间的睡觉,会让线程从running状态变成Timed Waiting 状态(阻塞)

  2. 如果其他线程打断当前正在睡觉的线程,就会抛出异常InterruptedException

  3. 睡眠结束的线程会从阻塞状态变成就绪,但未必会马上得到执行,还是得看cpu的心情

  4. 建议用TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

      TimeUnit.SECONDS.sleep(2);
    

2.2 yield()

  1. 这个方法你可以理解为谦让,当前cpu已经被他握在手里了,结果他突然大方地让出来
  2. 这个方法会使自身从running变成就绪,但是也有可能交出使用权结果还是轮到它执行,因为具体还是得看操作系统的任务调度器

我们还可以对线程设置优先级,但仅仅只是一个提示,在cpu比较闲的时候,优先级没啥用处,但在比较忙的时候,它会变成一个参考。t1.setPriority(Thread.MAX_PRIORITY);

3. join方法

image-20230308112526491.png

image-20230308112600132.png

  1. 作用:等待线程结束
  2. 一定要在start方法调用后使用,不然不起作用
  3. 可以同时设置等待时间 t1.join(100); 如果设置的时间过长,线程结束了,也不会继续等待下去的

4. interrupt() 方法 - 唤醒

  1. 如果是正在运行的线程,被interrupt后,打断标记为true
  2. 如果是sleep、wait、join的线程被打断,打断标记为false

5. park方法

// 可以让当前线程停下来,但是被打断后又会继续运行
LockSupport.park();

但如果你原先被打断过,打断标记为true后,再使用park方法停止线程运行,就会失效。

八、 线程的六种状态

1. 从操作系统层面

image-20230308114655280.png

  1. 初始状态:指从代码层面创造了线程,但还没有跟操作系统的线程相关脸上
  2. 可运行状态:线程已经被创建好了,可以随时被cpu调度
  3. 运行状态:指线程拿到cpu的使用权,当cpu的时间片用完后,会切换回可运行状态,导致上下文切换
  4. 阻塞状态:如读写文件,该线程不会实际用到cpu,但会导致上下文切换,线程状态变为阻塞状态。如果没有被唤醒,cpu不会考虑调度它
  5. 终止状态:表示执行完毕。

2. 从Java层面

image-20230310102228729.png

  1. new:表示线程刚被创建,还没调用start方法
  2. runnable:表示线程已经被启动,不管是启动完在运行中,还是就绪中,还是阻塞了 都属于runnable状态
  3. terminated:终止,线程运行结束
  4. blocked、waiting、timed_waiting 只是对阻塞状态进行细分

举个栗子:我们读取文件,从操作系统层面讲是阻塞的,但是从java层面讲它是可运行的