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.
目录
相关文章
|
18天前
|
Java 程序员 调度
【JAVA 并发秘籍】进程、线程、协程:揭秘并发编程的终极武器!
【8月更文挑战第25天】本文以问答形式深入探讨了并发编程中的核心概念——进程、线程与协程,并详细介绍了它们在Java中的应用。文章不仅解释了每个概念的基本原理及其差异,还提供了实用的示例代码,帮助读者理解如何在Java环境中实现这些并发机制。无论你是希望提高编程技能的专业开发者,还是准备技术面试的求职者,都能从本文获得有价值的见解。
32 1
|
9天前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
66 6
【Java学习】多线程&JUC万字超详解
|
2天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
11天前
|
Java 数据库连接 微服务
揭秘微服务架构下的数据魔方:Hibernate如何玩转分布式持久化,实现秒级响应的秘密武器?
【8月更文挑战第31天】微服务架构通过将系统拆分成独立服务,提升了可维护性和扩展性,但也带来了数据一致性和事务管理等挑战。Hibernate 作为强大的 ORM 工具,在微服务中发挥关键作用,通过二级缓存和分布式事务支持,简化了对象关系映射,并提供了有效的持久化策略。其二级缓存机制减少数据库访问,提升性能;支持 JTA 保证跨服务事务一致性;乐观锁机制解决并发数据冲突。合理配置 Hibernate 可助力构建高效稳定的分布式系统。
25 0
|
12天前
|
程序员 调度 C++
解锁Ruby并发编程新境界!Fiber与线程:轻量级VS重量级,你选哪一派引领未来?
【8月更文挑战第31天】Ruby提供了多种并发编程方案,其中Fiber与线程是关键机制。Fiber是自1.9版起引入的轻量级并发模型,无需独立堆栈和上下文切换,由程序员控制调度。线程则为操作系统级别,具备独立堆栈和上下文,能利用多核处理器并行执行。通过示例代码展示了Fiber和线程的应用场景,如任务调度和多URL数据下载,帮助开发者根据需求选择合适的并发模型,提升程序性能与响应速度。
20 0
|
13天前
|
Java API 调度
JUC线程池: FutureTask详解
总而言之,FutureTask是Java并发编程中一个非常实用的类,它在异步任务执行及结果处理方面提供了优雅的解决方案。在实现细节方面可以搭配线程池的使用,以及与Callable接口的配合使用,来完成高效的并发任务执行和结果处理。
21 0
|
20天前
|
Java C语言 C++
并发编程进阶:线程同步与互斥
并发编程进阶:线程同步与互斥
25 0
|
20天前
|
存储 安全 Unix
并发编程基础:使用POSIX线程(pthread)进行多线程编程。
并发编程基础:使用POSIX线程(pthread)进行多线程编程。
49 0
|
16天前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
43 1
|
8天前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。