JUC并发编程(二):线程相关知识点

1.背景

实现编发编程的主要手段就是多线程。线程是操作系统里的一个概念。接下来先说说两者的定义、联系与区别。

1.1 进程和线程的区别

进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。如下所示就是mac电脑后台的程序进程:

线程

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区 资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

用window系统的用户都喜欢下载电脑管家,如下所示:

这里电脑管家就是一个进程 ,你可以同时进行病毒查杀、垃圾清理、电脑加速 等操作,这些操作就是由一个个线程 去执行完成的。谈到多线程就不能说一下什么是上下文切换

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址github.com/plasticene/...

Gitee地址gitee.com/plasticene3...

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

1.2 什么是上下文切换

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁上下文切换就会影响多线程的执行速度,这也是多线程不一定就快的原因。

Thread.Sleep(0)的作用,就是"触发操作系统立刻重新进行一次CPU竞争"。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权

1.3 用户线程和守护线程

Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)

线程的daemon属性为true表示是守护线程,false表示是用户线程。JVM启动会调用 main 函数, main函数所在的线程就是一个用户线程,其实在 JVM后台还启动了很多守护线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。两者的区别是:当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出

csharp 复制代码
public class DaemonDemo {
    public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+" 开始运行,"+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
                while (true) {
​
                }
            }, "t1");
            //线程的daemon属性为true表示是守护线程,false表示是用户线程
            t1.setDaemon(true);
            t1.start();
            //3秒钟后主线程再运行
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("----------main线程运行完毕");
    }
}

2.线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start()
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换

由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态,线程的创建主要有这几种方式:实现Runnable接口和继承Thread类,使用 FutureTask 方式,代码如下所示:

csharp 复制代码
public class ThreadTest {
​
​
    public static void main(String[] args) throws Exception {
      System.out.println("main......start.....");
      // 方式1
      Thread thread = new Thread01();
      thread.start();
      System.out.println("main......end.....");
​
      // 方式2
      Runnable01 runnable01 = new Runnable01();
      new Thread(runnable01).start();
​
      // 放松3
      FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
      new Thread(futureTask).start();
      System.out.println(futureTask.get());
      System.out.println("main......end.....");
    }
    
     public static class Thread01 extends Thread {
        @Override
        public void run() {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
        }
    }
​
​
    public static class Runnable01 implements Runnable {
        @Override
        public void run() {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
        }
    }
​
​
    public static class Callable01 implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }
    }
  
  
 }

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,底层会调用Java本地方法```start0()``, 当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

线程等待和唤醒的方式大概有如下三种:

  • 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

    scss 复制代码
        public static void main(String[] args) {
            Object objectLock = new Object();  //同一把锁,类似资源类
    ​
            new Thread(() -> {
                synchronized (objectLock) {
                    try {
                        objectLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了");
            }, "t1").start();
    ​
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    ​
            new Thread(() -> {
                synchronized (objectLock) {
                    objectLock.notify();
                }
            }, "t2").start();
        }

    Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)

    先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒。

  • Condition接口中的await后signal方法实现线程的等待和唤醒

    csharp 复制代码
    public static void main(String[] args) {
            Lock lock = new ReentrantLock();
            Condition condition = lock.newCondition();
    ​
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t" + "start");
                    condition.await();
                    System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
    ​
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    ​
            new Thread(() -> {
                lock.lock();
                try {
                    condition.signal();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "通知了");
            }, "t2").start();
    ​
        }

    调用condition中线程等待和唤醒的方法的前提是,要在lock和unlock方法中,要有锁才能调用。必须先await()后signal,否则线程无法被唤醒。Condition 精准的通知和唤醒线程,而object的wait和notify机制做不到

  • LockSupport类中的park等待和unpark唤醒

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。

    scss 复制代码
    public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis());
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis() + "---被叫醒");
            }, "t1");
            t1.start();
    ​
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    ​
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis() + "---unpark over");
        }
    ​

如何使用中断标识停止线程?

csharp 复制代码
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("-----t1    线程被中断了,break,程序结束");
                break;
            }
            System.out.println("-----hello");
        }
    }, "t1");
    t1.start();
​
    System.out.println("**************" + t1.isInterrupted());
    //暂停5毫秒
    try {
        TimeUnit.MILLISECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
    System.out.println("**************" + t1.isInterrupted());
}

当对一个线程,调用 interrupt() 时:

① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。 被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法, 那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

3. 线程死锁

死锁,是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的解决方法:

  • 撤消陷于死锁的全部进程。
  • 逐个撤消陷于死锁的进程,直到死锁不存在。
  • 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
  • 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。

死锁案例如下:

java 复制代码
public static void main(String[] args) {
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();
​
        new Thread(() -> {
            synchronized (objectLockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + "自己持有A,希望获得B");
                //暂停几秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectLockB) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "A-------已经获得B");
                }
            }
        }, "A").start();
​
        new Thread(() -> {
            synchronized (objectLockB) {
                System.out.println(Thread.currentThread().getName() + "\t" + "自己持有B,希望获得A");
                //暂停几秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectLockA) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "B-------已经获得A");
                }
            }
        }, "B").start();
​
    }

使用jps -l查看当前案例的进程号,再使用`jstack 进程号会看到如下信息:

vbnet 复制代码
Found one Java-level deadlock:
=============================
"B":
  waiting to lock monitor 0x00007fe86e02f208 (object 0x000000076adfdf60, a java.lang.Object),
  which is held by "A"
"A":
  waiting to lock monitor 0x00007fe86e0332a8 (object 0x000000076adfdf70, a java.lang.Object),
  which is held by "B"
​
Java stack information for the threads listed above:
===================================================
"B":
        at com.shepherd.juc.JucExampleDemo.lambda$main$1(JucExampleDemo.java:40)
        - waiting to lock <0x000000076adfdf60> (a java.lang.Object)
        - locked <0x000000076adfdf70> (a java.lang.Object)
        at com.shepherd.juc.JucExampleDemo$$Lambda$2/2094777811.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"A":
        at com.shepherd.juc.JucExampleDemo.lambda$main$0(JucExampleDemo.java:25)
        - waiting to lock <0x000000076adfdf70> (a java.lang.Object)
        - locked <0x000000076adfdf60> (a java.lang.Object)
        at com.shepherd.juc.JucExampleDemo$$Lambda$1/1879492184.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
​
Found 1 deadlock.
​
相关推荐
程序员爱钓鱼9 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
小小菜鸡ing25 分钟前
pymysql
java·服务器·数据库
getapi28 分钟前
shareId 的产生与传递链路
java
桦说编程38 分钟前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
我没想到原来他们都是一堆坏人1 小时前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
沙二原住民2 小时前
提升数据库性能的秘密武器:深入解析慢查询、连接池与Druid监控
java·数据库·oracle
Jerry&Grj2 小时前
SpringBoot埋点功能技术实现方案深度解析:架构设计、性能优化与扩展性实践
java·微服务·性能优化·springboot·架构设计·埋点技术
没有bug.的程序员2 小时前
Redis Stream:轻量级消息队列深度解析
java·数据库·chrome·redis·消息队列