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

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

1.背景

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

1.1 进程和线程的区别

进程

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

线程

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

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

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

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

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号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后台还启动了很多守护线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。两者的区别是:当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出

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 方式,代码如下所示:

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()方法唤醒线程

        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方法实现线程的等待和唤醒

    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。

    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");
        }
    

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

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. 线程死锁

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

产生死锁的必要条件:

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

死锁的解决方法:

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

死锁案例如下:

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 进程号会看到如下信息:

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{
   
   mathJaxContainer[0]}1(JucExampleDemo.java:40)
        - waiting to lock <0x000000076adfdf60> (a java.lang.Object)
        - locked <0x000000076adfdf70> (a java.lang.Object)
        at com.shepherd.juc.JucExampleDemo{
   
   mathJaxContainer[1]}2/2094777811.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"A":
        at com.shepherd.juc.JucExampleDemo.lambda{
   
   mathJaxContainer[2]}0(JucExampleDemo.java:25)
        - waiting to lock <0x000000076adfdf70> (a java.lang.Object)
        - locked <0x000000076adfdf60> (a java.lang.Object)
        at com.shepherd.juc.JucExampleDemo{
   
   mathJaxContainer[3]}1/1879492184.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
目录
相关文章
|
5天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
1月前
|
Java Python
python知识点100篇系列(16)-python中如何获取线程的返回值
【10月更文挑战第3天】本文介绍了两种在Python中实现多线程并获取返回值的方法。第一种是通过自定义线程类继承`Thread`类,重写`run`和`join`方法来实现;第二种则是利用`concurrent.futures`库,通过`ThreadPoolExecutor`管理线程池,简化了线程管理和结果获取的过程,推荐使用。示例代码展示了这两种方法的具体实现方式。
python知识点100篇系列(16)-python中如何获取线程的返回值
|
2月前
|
存储 消息中间件 资源调度
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
该文章总结了操作系统基础知识中的十个关键知识点,涵盖了进程与线程的概念及区别、进程间通信方式、线程同步机制、死锁现象及其预防方法、进程状态等内容,并通过具体实例帮助理解这些概念。
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
|
1月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
30 3
|
1月前
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
33 0
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
60 3
|
2月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
2月前
|
并行计算 API 调度
探索Python中的并发编程:线程与进程的对比分析
【9月更文挑战第21天】本文深入探讨了Python中并发编程的核心概念,通过直观的代码示例和清晰的逻辑推理,引导读者理解线程与进程在解决并发问题时的不同应用场景。我们将从基础理论出发,逐步过渡到实际案例分析,旨在揭示Python并发模型的内在机制,并比较它们在执行效率、资源占用和适用场景方面的差异。文章不仅适合初学者构建并发编程的基础认识,同时也为有经验的开发者提供深度思考的视角。
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
41 1
C++ 多线程之初识多线程