当前位置:网站首页 > 技术博客 > 正文

java的并发,多线程,线程模型




1、进程与线程

进程
  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器
二者对比
  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

概念

  • 单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是 同时运行的 。
  • 总结为一句话就是: 微观串行,宏观并行 , 一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent
  • 并发是同一时间应对多件事情的能力
  • 并行是同一时间动手做多件事情的能力

栈与栈帧

  • Java Virtual Machine Stacks (Java 虚拟机栈)
  • 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。
    • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

  • 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
    • 线程的 cpu 时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
  • 当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
    • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
    • Context Switch 频繁发生会影响性能
1.start与run
  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
  1. sleep与yield
2.sleep
  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
  5. 案例:

    在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield来让出cpu的使用权给其他程序

3.yield
  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器
线程优先级
  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
2.join方法

java中Thread中的join方法是一个用于同步线程的方法,它可以让调用它的线程等待被调用线程执行完毕后再继续执行。例如,如果在主线程中调用了t.join(),那么主线程会阻塞,直到线程t结束后才会恢复运行。join方法的作用是保证线程的执行顺序,或者等待某个线程完成某项任务后再进行下一步操作。

join方法有三种重载形式:

  • join():无参数,表示等待被调用线程执行完毕后再继续。
  • join(long millis):带一个long类型的参数,表示等待被调用线程最多millis毫秒后再继续。
  • join(long millis, int nanos):带两个参数,表示等待被调用线程最多millis毫秒加nanos纳秒后再继续。

join方法的原理是基于wait和notify机制实现的。当我们调用t.join()时,实际上是在t对象上调用了wait方法,让持有t对象锁的线程进入等待状态。当t线程结束时,会在t对象上调用notifyAll方法,唤醒所有等待在t对象上的线程。这些线程中只有原来调用t.join()的线程能够获取到t对象的锁,从而继续执行。

下面是一个简单的示例代码,演示了join方法的使用:

 
   

运行结果可能如下:

 
   

可以看到,主线程在调用了t1.join()和t2.join()后,等待了t1和t2两个子线程执行完毕后才结束。

3.interrupt方法

Java中的Thread类有一个interrupt方法,它可以用来中断一个正在运行的线程。interrupt方法的作用是设置线程的中断标志,表示该线程已经被中断。然而,这并不意味着线程会立即停止运行,线程还需要检查自己的中断标志,并根据情况做出相应的响应。

有两种情况可以使线程响应中断:

  • 如果线程处于阻塞状态,比如调用了sleep、wait、join等方法,那么它会抛出一个InterruptedException异常,并清除中断标志。
  • 如果线程处于非阻塞状态,那么它需要主动检查自己的中断标志,比如调用了isInterrupted或者interrupted方法,如果发现已经被中断,那么它可以选择停止运行或者继续运行。

interrupt方法的使用示例如下:

 
   
4.打断park线程

[打断线程park是指使用LockSupport类的park方法来阻塞当前线程,直到满足以下三种情况之一]

  • 其他线程调用了unpark方法,将当前线程的许可证设置为可用
  • 其他线程中断了当前线程,设置了中断标志
  • 发生了无法预测的偶然事件

打断线程park和使用interrupt方法来中断线程有一些区别。interrupt方法只是给线程设置一个中断标志,不会影响线程的继续执行。而park方法会让线程真正地停止运行,进入WAITING或TIMED_WAITING状态

如果线程被打断,park方法会直接返回,不会抛出InterruptedException异常。但是,线程的中断标志会被设置为true,所以可以通过isInterrupted或interrupted方法来判断线程是否被打断如果想让线程再次进入park状态,需要手动重置中断标志为false

5.不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名 static 功能说明 stop() 停止线程运行 suspend() 挂起(暂停)线程运行 resume() 恢复线程运行
6.守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。 例:

 
   

注意 垃圾回收器线程就是一种守护线程

Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

根据 Thread.State 枚举,分为六种状态

在这里插入图片描述

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束
代码演示
 
   
7.1 共享问题

举例:假设有一个共享资源,例如一个银行账户,多个线程(例如多个出纳员)都可以访问和修改该账户的余额。如果这些线程没有正确地同步,可能会导致数据不一致的问题。

  • 为了避免这种情况,可以使用Java中的synchronized关键字来确保同一时刻只有一个线程可以访问共享资源。例如,可以将修改账户余额的方法标记为synchronized,这样在任何时候只有一个线程可以进入该方法,从而确保数据的一致性。
  • 另外,Java中的volatile关键字也可以用来解决线程共享问题。volatile关键字可以确保多个线程能够正确地共享变量。当一个变量被声明为volatile时,它会保证该变量的读写操作是原子的,不会被编译器优化或者处理器重排,从而确保多个线程能够正确地共享该变量。
临界区 Critical Section
  • 一个程序运行多个线程本身是没有问题的 问题出在多个线程访问共享资源 多个线程读共享资源其实也没有问题
  • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区 例如,下面代码中的临界区
     
竞态条件

竞态条件是指两个或以上进程或线程在并发执行时,其最终结果依赖于进程或线程执行的精确时序,即一个在设备或系统试图同时执行两个操作的时候出现的不希望的状况12。

竞态条件会产生超出预期的情况,一般情况下我们都希望程序执行的结果是符合预期的,因此竞态条件是一种需要被避免的情形。竞态条件分为两类2:

  • Mutex(互斥):两个或多个进程彼此之间没有内在的制约关系,但是由于要抢占使用某个临界资源(不能被多个进程同时使用的资源,如打印机、变量)而产生制约关系。
  • Synchronization(同步):两个或多个进程彼此之间存在内在的制约关系(前一个进程执行完,其他的进程才能执行),如严格轮转法。

要阻止出现竞态条件的关键就是不能让多个进程或线程同时访问那块共享变量。

7.2 synchronized
  • synchronized是Java语言的关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程运行完这个方法后再运行此线程,没有的话,锁定调用者,然后直接运行
  • synchronized关键字可以用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  • synchronized关键字包括两种用法:synchronized方法和synchronized块

实例代码:

 
   

在上述代码中,、和方法都使用了关键字。这意味着在同一时刻,只有一个线程可以执行这些方法。如果其他线程试图同时执行这些方法,它们将会被阻塞,直到当前线程执行完这些方法。这样可以确保计数器的值在任何时候都是正确的,避免了竞态条件的发生。

”线程八锁“
  • 情况一:12或者21
     
  • 情况二:1s后12,或者2 1s后1
     
  • 情况三:3 1s后 12或23 1s后1或32 1s后1
     
  • 情况四:2 1s后1
     
  • 情况五:2 1s后1
     
  • 情况六:1s 后12,或2 1s后1
     
  • 情况七:2 1s后1
     
  • 情况八:1s后12,或2 1s后1
     
7.3 线程安全分析
  • 成员变量和静态变量是否线程安全
    • 如果它们没有共享,则线程安全
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
  • 局部变量是否线程安全
    • 局部变量是线程安全的
    • 但局部变量引用的对象则未必
      • 如果该对象没有逃离方法的作用访问,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全
  • 局部变量线程安全分析

    在Java中,局部变量通常是线程安全的,因为它们只存在于单个线程的栈内存中。这意味着,每个线程都有其自己的栈,其中包含该线程执行所需的所有局部变量。因此,同一时间只有一个线程可以访问或修改这些局部变量。

    然而,线程安全问题并非完全与局部变量无关。例如,你可能有一个多线程环境,其中一些线程试图修改共享数据结构,这可能涉及到线程安全问题。

    以下是一个示例,说明如何在Java中处理可能出现的线程安全问题:

     
         
    • 在这个例子中,我们有一个类SharedData,它有一个共享的count变量。这个类有两个方法:increment和getCount。这些方法通过synchronized关键字进行同步,这意味着在同一时间只有一个线程可以访问这些方法。
    • 然而,如果你有一个复杂的程序,并且不能保证每次访问count变量时都使用synchronized关键字,那么你可能需要更复杂的同步机制,例如java.util.concurrent包中的类。
    • 总的来说,局部变量是线程安全的,但如果你在多线程环境中使用共享数据结构,那么你需要考虑线程安全问题。
  • 常见的线程安全类
    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent 包下的类

      注意:这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

       
             
      • 它们的每个方法是原子的
      • 但注意它们多个方法的组合不是原子的,见后面分析
  • 不可变类线程安全性
    • String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
    • 那么,String 有 replace,substring 等方法可以改变值啊,那么这些方法又是如何保证线程安全的呢?
    • String类中的replace和substring等方法实际上会修改字符串。然而,这些修改并不会改变原始的String对象,而是创建一个新的修改过的String对象。
    • 请注意,虽然不可变对象本身是线程安全的,但是在多线程环境中使用它们可能仍然需要额外的同步或并发控制。例如,如果你有一个大的数据结构,如列表或映射,其中包含许多不可变对象,并且多个线程可能同时访问和修改这个数据结构,那么你可能需要使用额外的同步机制来保证这个数据结构的线程安全。
Monitor wait/notify
  • 在Java中,Monitor是一个抽象概念,是Java并发编程的基础。在Java的并发编程中,一个重要的概念是监视器(Monitor),它是一种同步机制,用于控制对共享资源的访问。
  • Monitor在Java中的具体实现通常与Object的锁机制一起使用。当一个线程进入一个对象的synchronized方法或synchronized代码块时,它会尝试获取这个对象的锁(或者叫监视器)。如果这个对象没有被其他线程持有,那么该线程就会获取到对象的锁,然后执行这个synchronized方法或synchronized代码块。其他想要进入这个synchronized方法或synchronized代码块的线程必须等待,直到持有锁的线程释放这个锁。

这个机制确保了对共享资源的互斥访问,防止多个线程同时修改同一资源,造成数据的不一致。

例如:

 
   
  • 在这个例子中,lock对象被用作同步标记。任何时候只能有一个线程能够进入synchronized(lock)块中的代码,因为其他任何尝试进入此代码块的线程都必须等待,直到锁被释放。
wait/notify
  • wait()和notify()是Java的内置方法,用于处理多线程中的同步问题。它们必须在同步上下文中使用,也就是说,这两个方法必须由synchronized关键字保护的代码块或方法调用。
  • wait()方法使当前线程进入等待状态,直到其他线程调用该对象的notify()方法或notifyAll()方法。
  • notify()方法唤醒等待该对象的等待线程。这些线程然后从就绪状态进入运行状态,并尝试获取对象的锁。

以下是一个简单的例子:

 
   
  • 在这个例子中,doSomething()方法中的代码在一个同步块中执行,然后调用wait()。这使当前线程进入等待状态,直到其他线程调用同一对象的notify()或notifyAll()方法。
  • doSomethingElse()方法中的代码也在一个同步块中执行,然后调用notifyAll()。这会唤醒所有等待该对象的线程,这些线程随后将从等待状态进入就绪状态,并争夺CPU的使用权。
线程状态转换 活跃性
  • 在Java中,线程的状态转换主要涉及到以下七种状态:
  • 新建(New):当线程被创建时,它处于新建状态。此时,它还没有开始运行。
  • 就绪(Runnable):当线程已经被启动并且没有任何阻止它立即运行的条件时,它就处于就绪状态。此时,线程正在等待CPU分配时间片。Java中,就绪状态的线程主要包括以下两种情况:
    • 线程的优先级大于0且线程的队列在执行过程中;
    • 线程的优先级等于0且当前没有其他优先级大于0的线程在执行,同时线程的队列在执行过程中。
  • 运行(Running):当线程获得CPU,并执行其运行方法时,它处于运行状态。注意,此时线程不一定是处于就绪状态。例如,一个线程可能正在等待某个锁释放,或者正在执行同步块或同步方法。
  • 阻塞(Blocked):当线程正在等待某些资源或满足某些条件(例如,等待I/O操作完成)时,它进入阻塞状态。在Java中,当线程试图获取一个内部的对象锁(而不是在方法或代码块上的监视器锁),而该锁被其他线程持有,则该线程进入阻塞状态。
  • 等待(Waiting):线程等待其他线程做出一些特定动作。例如,线程可以调用其他线程的join()方法或某些I/O操作的wait()方法进入等待状态。
  • 超时等待(Timed Waiting):这是等待状态的一个特殊形式,线程等待特定的时间。例如,Thread.sleep(int),Thread.await()或Thread.join(long, int)会进入这种状态。
  • 终止(Terminated):当线程已经执行完毕或者因为异常而结束时,它处于终止状态。
    • 活跃性是指线程的执行强度和频率。"活跃性"并不是Java或任何其他编程语言的一个标准术语,所以它的确切定义可能因上下文而异。然而,通常我们可以理解活跃性是指一个线程在一段时间内执行的工作量或者被调用的频繁程度。
    • 在Java中,可以通过各种方式来控制和管理线程的活跃性。例如,你可以通过Thread类的sleep()方法让线程进入暂停状态一段时间,从而降低其活跃性。另外,你可以通过合理的调度算法来平衡不同线程的执行顺序和时间长度,从而在一定程度上影响其活跃性。在一些高级的并发框架(如Quartz或Spring Task)中,也提供了更为复杂的控制线程活跃性的方法。
    • 然而,Java并没有直接提供获取线程活跃度的API。要获取线程的活跃度信息,可能需要通过自定义计数器或者使用特定的性能监控和分析工具(如VisualVM或JProfiler)来实现。
Lock
  • Java中的Lock接口是并发编程中的重要部分,它允许程序员在多线程环境中对共享资源进行精确的控制。Lock接口提供了一种机制,使得多个线程在访问共享资源时,能保持线程安全。
  • 这主要是通过lock()和unlock()方法实现的,前者用于获取锁,后者用于释放锁。当一个线程通过调用lock()获取了一个锁时,其他尝试通过lock()获取同一把锁的线程将会被阻塞,直到原线程调用unlock()释放锁。
  • 下面是一个简单的例子,展示了如何使用Lock接口:
 
   
  • 这个例子中,我们创建了一个Lock实例,并用它来保护对data变量的访问。increment()方法在增加data的值之前首先获取锁,然后在finally块中释放锁,以确保无论是否发生异常,锁都能被正确地释放。
  • 然后我们在主方法中创建了10个线程,每个线程都尝试调用increment()方法。由于我们对data变量的访问进行了正确的同步,所以无论这些线程如何竞争,data的值都不会出现不一致的结果。
  • 这是ReentrantLock的一个简单示例。Java还提供了其他几种Lock实现,如StampedLock,它提供了更高级的同步功能,比如“乐观读”和“写偏斜”的防止。
  • ReentrantLock类

类是接口的一个实现,它具有以下特点:

  • 它是可重入的,意味着一个线程可以多次获得同一个锁,只要它每次都通过调用来获取,然后通过调用来释放。
  • 等待可中断性:通过调用方法,线程可以中断等待锁的线程。
  • 公平性:通过构造函数可以指定锁是否是公平的。如果是公平的,那么等待时间最长的线程将首先获得锁。

下面是一个使用的简单例子:

 
   
  • 在这个例子中,我们创建了一个并使用它来保护对的访问。方法在修改之前先获取锁,然后在finally块中释放锁,以确保锁总是被释放。
  • 需要注意的是,使用的时候一定要配合语句或者使用Java 7引入的try-with-resources语句,以确保锁在修改完成后一定会被释放。

版权声明


相关文章:

  • c语言eof的用法!=eof2026-05-16 22:01:02
  • monkey测试结果怎么看2026-05-16 22:01:02
  • echarts官网教程2026-05-16 22:01:02
  • java模拟网站登录2026-05-16 22:01:02
  • es倒排索引原理2026-05-16 22:01:02
  • 数据库读写分离实现2026-05-16 22:01:02
  • 卷积神经网络发明人2026-05-16 22:01:02
  • vue开发页面样式怎么写2026-05-16 22:01:02
  • 黑客软件是什么软件2026-05-16 22:01:02
  • 终端模拟器app下载中文版2026-05-16 22:01:02