进程是系统运行的基本单位,通俗的理解我们计算机启动的每一个应用程序都是一个进程。如下图所示,在Windows中这一个个exe文件,都是一个进程。而在JVM下,每一个启动的Main方法都可以看作一个进程。
线程是比进程更小的单位,所以在进行线程切换时的开销会远远小于进程,所以线程也常常被称为轻量级进程。每一个进程中都会有一个或者多个线程,在JVM中每一个Java线程都会共享他们的进程的堆区和方法区。但是每一个进程都会有自己的程序计数器、虚拟机栈和本地方法栈。
Java天生就是一个多线程的程序,我们完全可以运行下面这段代码看看一段main方法中会有那些线程在运行
public class MultiThread { public static void main(String[] args) { // 获取 Java 线程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍历线程信息,仅打印线程 ID 和线程名称信息 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } }
输出结果如下,所以Java程序在main函数运行时,还有其他的线程再跑。
[6] Monitor Ctrl-Break //这个线程是IDEA用来监控Ctrl-Break中断信号的线程 [5] Attach Listener //添加事件 [4] Signal Dispatcher // 方法处理Jvm信号的线程 [3] Finalizer //清除finalize 方法的线程 [2] Reference Handler // 清除引用的线程 [1] main // main入口
如下图所示,可以看出线程是比进程更小的单位,进程是独立的,彼此之间不会干扰,但是线程在同一个进程中共享堆区和方法区,虽然开销较小,但是资源之间管理和分配处理相对于进程之间要更加小心。
所以为了保证局部变量不被别的线程访问到,虚拟机栈和本地方法栈都是私有的,这就是我们解决某些线程安全问题时,常会用到一个叫栈封闭技术。
关于栈封闭技术如下所示,将变量放在局部,每个线程都有自己的虚拟机栈,线程安全
public class StackConfinement implements Runnable { //全部变量 多线操作会有现场问题 int globalVariable = 0; public void inThread() { //栈封闭技术,将变量放在局部,每个线程都有自己的虚拟机栈 线程安全 int neverGoOut = 0; synchronized (this) { for (int i = 0; i < 10000; i++) { neverGoOut++; } } System.out.println("栈内保护的数字是线程安全的:" + neverGoOut);//栈内保护的数字是线程安全的:10000 } @Override public void run() { for (int i = 0; i < 10000; i++) { globalVariable++; } inThread(); } public static void main(String[] args) throws InterruptedException { StackConfinement r1 = new StackConfinement(); Thread thread1 = new Thread(r1); Thread thread2 = new Thread(r1); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(r1.globalVariable); //13257 } }
从宏观角度来看:线程可以理解为轻量级进程,切换开销远远小于进程,所以在多核CPU的计算机下,使用多线程可以更好的利用计算机资源从而提高计算机利用率和效率来应对现如今的高并发网络环境。
从微观场景下来说: 单核场景,在单核CPU情况下,假如一个线程需要进行IO才能执行业务逻辑,若只有单线程,这就意味着IO期间发生阻塞线程却只能干等。假如我们使用多线程的话,在当前线程IO期间,我们可以将其挂起,让出CPU时间片让其他线程工作。
多核场景下,假如我们有一个很复杂的任务需要进程各种IO和业务计算,假如只有一个线程的话,无论我们有多少个CPU核心,因为单线程的缘故他永远只能利用一个CPU核心,假如我们使用多线程,那么这些线程就会映射到不同的CPU核心上,做到最好的利用计算机资源,提高执行效率,执行事件约为单线程执行事件/CPU核心数。
//继承Thread 然后start public class Task extends Thread { public void run() { for (int x = 0; x < 60; x++) System.out.println("Task run----" + x); } public static void main(String[] args) throws InterruptedException { Task d = new Task();//创建好一个线程。 d.start();//开启线程并执行该线程的run方法。 d.join(); } }
//实现Runnable 方法 public class Ticket implements Runnable { private int tick = 100; public void run() { synchronized (Ticket.class) { while (true) { if (tick > 0) { System.out.println(Thread.currentThread().getName() + "....sale : " + tick--); } } } } } public class Ticket implements Runnable { private int tick = 100; public void run() { synchronized (Ticket.class) { while (true) { if (tick > 0) { System.out.println(Thread.currentThread().getName() + "....sale : " + tick--); }else{ break; } } } } }
FutureTaskfutureTask=new FutureTask<>(()-> "123"); new Thread(futureTask).start(); try { System.out.println(futureTask.get()); } catch (Exception e) { e.printStackTrace(); }
由于Java的类只能单继承,当一个类已有继承类时,某个函数需要扩展为多线程这时候,Runnable接口就是最好的解决方案。
新建(NEW):新创建的了一个线程对象,该对象并没有调用start()。
可运行(RUNNABLE):线程对象创建后,并调用了start方法,等待分配CPU时间执行代码逻辑。
阻塞(BLOCKED):阻塞状态,等待锁的释放。当线程在synchronized 中被wait,然后再被唤醒时,若synchronized 有其他线程在执行,那么它就会进入BLOCKED状态。
等待(WAITING):因为某些原因被挂起,等待其他线程通知或者唤醒。
超时等待(TIME_WAITING):等待时间后自行返回,而不像WAITING那样没有通知就一直等待。
终止(TERMINATED):该线程执行完毕,终止状态了。
如下图所示,实际上操作系统层面可将RUNNABLE分为Running以及Ready,Java设计者之所以没有区分那么细是因为现代计算机执行效率非常高,这两个状态在宏观角度几乎无法感知。现代操作系统对多线程采用时间分片的抢占式调度算法,使得每个线程得到CPU在10-20ms 处于运行状态,然后在让出CPU时间片,在不久后又会被调度执行,所以对于这种微观状态区别,Java设计者认为没有必要为了这么一瞬间进行这么多的状态划分。
线程在执行过程中都会有自己的运行条件和状态,这些运行条件和状态我们就称之为线程上下文,这些信息例如程序计数器、虚拟机栈、本地方法栈等信息。当出现以下几种情况的时候就会从占用CPU状态中退出:
上述的前三种情况都会发生上下文切换。为了保证线程被切换在恢复时能够继续执行,所以上下文切换都需要保存线程当前执行的信息,并恢复下一个要执行线程的现场。这种操作就会占用CPU和内存资源,频繁的进行上下文切换就会导致整体效率低下。
如下图所示,两个线程各自持有一把锁,必须拿到对方手中那把锁才能释放自己的锁,正是这样一种双方僵持的状态就会导致线程死锁问题。
翻译称代码就如下图所示
public class DeadLockDemo { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock1){ System.out.println("线程1获得锁1,准备获取锁2"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ System.out.println("线程1获得锁2"); } } }).start(); new Thread(() -> { synchronized (lock2){ System.out.println("线程2获得锁2,准备获取锁1"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1){ System.out.println("线程2获得锁1"); } } }).start(); } }
输出结果
线程1获得锁1,准备获取锁2 线程2获得锁2,准备获取锁1
符合以下4个条件的场景就会发生死锁问题:
预防死锁的3种方式
因为sleep要做的仅仅是让线程休眠,所以不涉及任何锁释放等逻辑,放在Thread上最合适。
我们都知道使用wait时就会释放锁,并让对象进入WAITING 状态,会涉及到资源释放等问题,所以我们需要将wait放在Object 类上。
若我们编写run方法,然后调用Thread 的start方法,线程就会从用户态转内核态创建线程,并在获取CPU时间片的时候开始运行,然后运行run方法。
若直接调用run方法,那么该方法和普通方法没有任何差别,它仅仅是一个名字为run的普通方法。
找到线程对应线程组即可定位到线程,然后调用interrupt将其打断即可。但如果想精确定位线程,我们还是建议使用ThreadLocal对线程做个标记。
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); if (threadGroup != null) { Thread[] threads = new Thread[(int) (threadGroup.activeCount() * 1.2)]; int count = threadGroup.enumerate(threads, true); for (int i = 0; i < count; i++) { if (threads[i].getId() == threadId) { return threads[i]; } } } return null;
由于讲述问题的篇幅比较大,笔者专门写了一篇文章来讨论这两个问题,感兴趣的朋友可以看看:来聊聊IO阻塞与CPU任务调度
Java并发编程:volatile关键字解析:https://www.cnblogs.com/dolphin0520/p/3920373.html
图解 | 你管这破玩意叫线程池?: https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247491549&idx=1&sn=1d5728754e8c06a621bbdca336d85452&chksm=c2c66570f5b1ec66df623e5300084257bd943b134d34e16abaacdb58834702dbbc4599868b89&scene=178&cur_album_id=1703494881072955395#rd
我是一个线程:https://mp.weixin.qq.com/s/IkNfuE541Mqqbv2iLIhMRQ
Java 并发常见面试题总结(上):https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html#什么是线程和进程
创建线程几种方式_线程创建的四种方式及其区别:https://cloud.tencent.com/developer/article/2135189#:~:text=创建线程的几种方式: 方式1:通过继承Thread类创建线程 步骤:1.定义Thread类的子类,并重写该类的run方法,该方法的方法体就是线程需要执行的任务,因此run,()方法也被称为线程执行体 2.创建Thread子类的实例,也就是创建了线程对象 3.启动线程,即调用线程的start ()方法