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.
目录
相关文章
|
14天前
|
SQL 开发框架 .NET
高级主题:Visual Basic 中的多线程和并发编程
【4月更文挑战第27天】本文深入探讨了Visual Basic中的多线程和并发编程,阐述了其基本概念,如何使用`System.Threading.Thread`类创建线程,以及借助`ThreadPool`、`Monitor`和`SyncLock`进行同步管理。文章还提到了多线程编程面临的挑战如竞态条件、死锁和资源竞争,并介绍了VB的异步编程、TPL和并发集合等高级技术。通过实例展示了多线程在文件处理、网络通信和图像处理中的应用,并给出了多线程编程的最佳实践。总之,理解并掌握VB的多线程和并发编程能有效提升应用程序的性能和响应能力。
|
2天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
11 0
|
3天前
|
消息中间件 程序员 调度
Python并发编程:利用多线程提升程序性能
本文探讨了Python中的并发编程技术,重点介绍了如何利用多线程提升程序性能。通过分析多线程的原理和实现方式,以及线程间的通信和同步方法,读者可以了解如何在Python中编写高效的并发程序,提升程序的执行效率和响应速度。
|
5天前
|
安全 Java
Java中的并发编程:理解并发性与线程安全
Java作为一种广泛应用的编程语言,在并发编程方面具有显著的优势和特点。本文将探讨Java中的并发编程概念,重点关注并发性与线程安全,并提供一些实用的技巧和建议,帮助开发人员更好地理解和应用Java中的并发机制。
|
11天前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
|
11天前
|
安全 调度 Swift
【Swift开发专栏】Swift中的多线程与并发编程
【4月更文挑战第30天】本文探讨Swift中的多线程与并发编程,分为三个部分:基本概念、并发编程模型和最佳实践。介绍了线程、进程、并发与并行、同步与异步的区别。Swift的并发模型包括GCD、OperationQueue及新引入的结构体Task和Actor。编写高效并发代码需注意任务粒度、避免死锁、使用线程安全集合等。Swift 5.5的并发模型简化了异步编程。理解并掌握这些知识能帮助开发者编写高效、安全的并发代码。
|
12天前
|
安全 Java 开发者
构建高效微服务架构:后端开发的新范式Java中的多线程并发编程实践
【4月更文挑战第29天】在数字化转型的浪潮中,微服务架构已成为软件开发的一大趋势。它通过解耦复杂系统、提升可伸缩性和促进敏捷开发来满足现代企业不断变化的业务需求。本文将深入探讨微服务的核心概念、设计原则以及如何利用最新的后端技术栈构建和部署高效的微服务架构。我们将分析微服务带来的挑战,包括服务治理、数据一致性和网络延迟问题,并讨论相应的解决方案。通过实际案例分析和最佳实践的分享,旨在为后端开发者提供一套实施微服务的全面指导。 【4月更文挑战第29天】在现代软件开发中,多线程技术是提高程序性能和响应能力的重要手段。本文通过介绍Java语言的多线程机制,探讨了如何有效地实现线程同步和通信,以及如
|
12天前
|
并行计算 数据处理 开发者
Python并发编程:解析异步IO与多线程
本文探讨了Python中的并发编程技术,着重比较了异步IO和多线程两种常见的并发模型。通过详细分析它们的特点、优劣势以及适用场景,帮助读者更好地理解并选择适合自己项目需求的并发编程方式。
|
17天前
|
Java
[并发编程基础] Java线程的创建方式
[并发编程基础] Java线程的创建方式
|
17天前
|
安全 Java API
Java从入门到精通:3.2.1分布式与并发编程——深入Java并发包,精通多线程高级用法
Java从入门到精通:3.2.1分布式与并发编程——深入Java并发包,精通多线程高级用法