深入解析多线程编程:线程状态、对象方法与生产者消费者模型全攻略

引

深入解析多线程编程:线程状态、对象方法与生产者消费者模型全攻略

如果对于线程是什么以及进程是什么仍有疑惑,那么请首先通过 Google 去了解。因为这两个概念并不在本文所涉及的范围之内。

用多线程的目的仅有一个,那就是充分利用 cpu 的资源。因为所有的多线程代码都能够用单线程来实现。然而,说这种话其实只对了一半。因为对于反映“多角色”的程序代码而言,至少每个角色都需要给它分配一个线程,不然就连实际场景都无法进行模拟,自然也就不能说能用单线程来实现了,比如最为常见的“生产者,消费者模型”。

很多人对于其中的一些概念不够清晰,像同步以及并发等。那么,让我们首先来建立一个数据字典,这样可以避免出现误会。

多线程:指的是这个程序(一个进程)运行时产生了不止一个线程

并行与并发:

多个 cpu 实例能够同时执行一段处理逻辑,多台机器也可以同时执行一段处理逻辑,这就是真正的同时。

并发是通过 cpu 调度算法,使得用户看起来像是同时在执行。但实际上,从 cpu 操作层面来看,并不是真正的同时进行。并发通常在一些场景中会有公用的资源,针对这个公用的资源往往会产生瓶颈。我们会用 TPS 或者 QPS 来反映这个系统的处理能力。

线程安全是经常用来描述一段代码的特性。在并发的情况下,当这段代码被多线程使用时,线程的调度顺序不会对任何结果产生影响。在这种情况下使用多线程,我们只需关注系统的内存和 CPU 是否够用就行。而线程不安全则意味着线程的调度顺序会对最终结果产生影响,例如不加事务的转账代码。

函数 transferMoney 接收两个用户对象 from 和 to 以及一个浮点数 amount,用于进行转账操作。 它会将 from 用户的账户余额减去 amount,同时将 amount 加到 to 用户的账户余额上。 这个函数主要用于在不同用户之间转移资金。
设置金额为获取余额加上指定金额;获取余额后加上指定金额并进行设置;将获取到的余额与指定金额相加后进行设置金额的操作;通过获取余额并加上指定金额来进行设置金额的动作;以获取余额并加上指定金额的方式来设置金额;利用获取到的余额加上指定金额来完成设置金额的操作;获取余额后把它与指定金额相加从而进行设置金额的行为;把获取到的余额加上指定金额以此来设置金额;以获取余额后加上指定金额的途径来设置金额;通过获取余额并与指定金额相加来实现设置金额的动作
从获取余额(from.getBalance())中减去金额(amount),然后从该结果中设置金额(from.setMoney())。
}

Java 中的同步意味着通过人为的控制与调度,以确保共享资源在多线程访问时能够保持线程安全,进而保障结果的准确性。例如,在上述代码中简单地加入相关内容。@synchronized关键字。要在保证结果准确的同时提高性能,这样的程序才是优秀的。线程安全的优先级比性能更高。

让我们开始吧。我准备把涉及到多线程的内容分成几部分来总结。

扎好马步:线程的状态

内功心法:每个对象都有的方法(机制)

太祖长拳:基本线程类

九阴真经:高级多线程控制类

扎好马步:线程的状态

先来两张图:

各种状态清晰可见。值得一提的是,“Blocked”状态与“Waiting”状态存在区别:

线程在进行 Running 操作期间可能会遭遇到阻塞性的情况。

对处于 Running 状态的线程加上同步锁(Synchronized),这样它就会进入到(lock blocked pool)。当同步锁被释放后,该线程就会进入可运行状态(Runnable)。从 jdk 源码注释来看,blocked 所指的是对 monitor 的等待(可参考下文的图),也就是说该线程处于等待区。

线程在处于 Running 状态的过程中或许会遭遇到等待(Waiting)的情形

线程能够主动去调用 object.wait 这一操作,也能够调用 sleep 这一操作,还能够调用 join(因为 join 内部调用的是 sleep,所以可以将其看成是 sleep 的一种)从而进入相应状态。从 jdk 源码注释去看,waiting 意味着等待另一个线程去完成某一个操作。比如 join 会等待另一个线程完成执行,而 object.wait()则会等待 object.notify()方法执行。

Waiting 状态和 Blocked 状态有些难以理解。我个人的看法是:Blocked 实际上也是一种等待,它等待的是 monitor。然而,它与 Waiting 状态不同。例如,有三个线程进入了同步块,其中两个线程调用了 object.wait(),从而进入了 waiting 状态。此时,第三个线程调用了 object.notifyAll()。这时,前两个线程中一个转移到了 Runnable 状态,另一个转移到了 Blocked 状态。

从下文的 monitor 结构图进行区分:在某个特定时刻,每个 Monitor 仅能被一个线程所拥有,这个线程就是“Active Thread”,而其他的线程则都是“Waiting Thread”,它们分别处于两个队列“Entry Set”和“Wait Set”中等待。在“Entry Set”中等待的线程处于 Blocked 状态,从 jstack 的 dump 来看是“Waiting for monitor entry”。在“Wait Set”中等待的线程状态为 Waiting,在 jstack 的 dump 中表现为“in Object.wait()”。

此外,处于 runnable 状态的线程是正在被调度的线程。此时,其调度顺序并不一定。Thread 类中的 yield 方法能够使一个处于 running 状态的线程转变为 runnable 状态。

内功心法:每个对象都有的方法(机制)

synchronized 是一种同步工具,wait 也是一种同步工具,notify 同样是一种同步工具,它们是任何对象都具有的。让我们先来了解它们。

他们是用于同步问题的人工线程调度工具。本质上,首先要明确 monitor 的概念。在 Java 中,每个对象都有一个监视器,用于监测并发代码的重入。在非多线程编码时,这个监视器不发挥作用。而如果是在 synchronized 范围内,监视器就会发挥作用。

wait 和 notify 必须处于 synchronized 块内。同时,这三个关键字所针对的是同一个监视器,也就是某对象的监视器。这就表明,在 wait 之后,其他线程能够进入同步块并执行。

当某代码处于未持有监视器使用权的状态(就像图中 5 的那种状态,也就是脱离了同步块),然后去进行 wait 操作或者 notify 操作,这种情况下就会抛出相应的异常。

java.lang.IllegalMonitorStateException 包括在 synchronized 块中调用另一个对象的 wait/notify 这种情况。因为不同对象的监视器是不同的,所以这样做同样会抛出此异常。

再讲用法:

synchronized单独使用:

在多线程的环境当中,synchronized 块里的方法获取了 lock 实例的 monitor 。如果实例是相同的,那么就只有一个线程能够去执行该块的内容。

公共类 Thread1 实现了 Runnable 接口。
        Object lock;
        public void run() {  
在锁定(lock)的情况下进行同步操作。
              ..do something
            }
        }
}

直接用于方法时,其效果与上面代码中用 lock 来锁定相同,实际获取的是 Thread1 类的 monitor。并且,如果修饰的是 static 方法,那么就会锁定该类的所有实例。

public class Thread1 implements Runnable {
public 方法 synchronized 修饰,用于执行 run 操作。 该方法用于在多线程环境下确保线程安全地执行特定的任务。 它会按照一定的逻辑和流程进行运行,以实现特定的功能或处理特定的情况。
             ..do something
        }
}

synchronized 与 wait、notify 相结合,其典型场景为生产者消费者问题。

/**
     * 生产者生产出来的产品交给店员
     */
公共的且同步的方法用于生产。
    {
如果这个产品大于等于最大产品量
        {
            try
            {
                wait();  
System 会打印“产品已满,请稍候再生产”。
            }
捕获(InterruptedException 异常 e)
            {
输出异常的堆栈跟踪信息。具体来说,就是将异常 e 的相关信息以堆栈跟踪的形式打印出来,以便开发者能够更清楚地了解异常发生的位置和过程。
            }
            return;
        }
        this.product++;
System.out.println("生产者生产出了第" + this.product + "个产品。");
告知所有处于等待状态的线程可以继续执行。
    }
    /**
     * 消费者从店员取产品
     */
公共的且同步的方法 consume() ,在执行时需要获取锁,以确保在同一时间只有一个线程能够进入该方法进行消费操作,避免数据不一致等问题的发生。
    {
        if(this.product <= MIN_PRODUCT)
        {
            try 
            {
                wait(); 
                System.out.println("缺货,稍候再取");
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
            return;
        }
        System.out.println("消费者取走了第" + this.product + "个产品.");
        this.product--;
        notifyAll();   //通知等待去的生产者可以生产产品了
    }

volatile

多线程有其内存模型,包括主存和线程栈。在处理数据过程中,线程会把值从主存加载到本地栈,完成操作后再保存回去。而 volatile 关键词的作用在于,每次针对该变量的操作都会激发一次加载和保存的动作。

对于多线程使用的变量,如果未被 volatile 或者 final 修饰,就很可能出现不可预知的结果。也就是说,另一个线程修改了该值,但在某个线程中看到的却是修改之前的值。从道理上讲,同一实例的同一属性本身只有一个副本。然而,多线程会缓存值,而 volatile 的本质是不缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。

太祖长拳:基本线程类

基本线程类包括 Thread 类,还有 Runnable 接口以及 Callable 接口。

Thread 类具备实现 Runnable 接口的能力,而启动一个线程的具体方法如下:

创建了一个 MyThread 的实例 my,即 MyThread my = new MyThread();
  my.start();

Thread类相关方法:

当前线程能够转让 CPU 的控制权,使其他处于就绪状态的线程得以运行(进行切换)。
public static Thread.yield() 
//暂停一段时间
让当前正在执行的线程暂停(暂时停止执行)一段指定的时间,这段时间以毫秒为单位。通过调用 Thread.sleep() 方法可以实现线程的暂停效果。在暂停期间,其他线程可以继续执行,而当前线程处于阻塞状态,不会占用 CPU 资源。只有当指定的暂停时间结束后,当前线程才会继续执行后续的代码。
在一个线程中调用 other.join() 这个操作,会使得本线程处于等待状态,直到 other 执行完毕后,本线程才会继续执行。
public join()
//后两个函数皆可以被打断
public interrupte()

关于中断:它与 stop 方法不同,不会中断正在运行的线程。线程会时常去检测中断标识位,以此来确定线程是否应该被中断,也就是判断中断标识值是否为 true。终端只会对 wait 状态、sleep 状态和 join 状态产生影响。被打断的线程会抛出 InterruptedException 。

Thread.interrupted()用于检查当前线程是否被中断,它会返回一个 boolean 值。

synchronized在获锁的过程中是不能被中断的。

中断是一种状态。interrupt()方法的作用仅仅是将这种状态设置为 true 。这意味着如果正常运行的程序不去检测这个状态,那么程序就不会终止。而 wait 等阻塞方法会去检查这个状态并抛出异常。倘若在正常运行的程序中添加 while(!Thread.interrupted()) ,那么在中断发生后,程序就能够离开代码体。

Thread类最佳实践:

写的时候最好要设置线程名称Thread.name

设置线程组 ThreadGroup,这样便于进行管理。当出现问题时,通过打印线程栈(jstack -pid),能够一眼看出是哪个线程出现了问题,以及该线程的用途是什么。

如何获取线程中的异常

Runnable

与Thread类似

Callable

Future 模式是并发模式的一种。它有两种形式,分别是无阻塞和阻塞。无阻塞形式是 isDone,阻塞形式是 get。Future 对象用于存放该线程的返回值以及状态。

创建了一个线程池执行服务 e,通过 Executors 的 newFixedThreadPool 方法创建了一个固定大小为 3 的线程池。
submit 方法存在多重参数的版本,它既支持 callable 类型,又能够支持 runnable 接口类型。
future 执行完成了。它返回 true 或 false,且不会阻塞程序的执行。
future.get(),此操作会返回一个返回值,并且会一直阻塞,直到该线程运行结束。

九阴真经:高级多线程控制类

以上属于内功心法。接下来便是实际项目中常使用的工具了。Java1.5 提供了一个多线程包,此包非常高效且实用。

java.util.concurrent 提供了很多高级工具,这些工具能够帮助开发者去编写高效的 Java 多线程程序,也能帮助开发者编写易维护的 Java 多线程程序,还能帮助开发者编写结构清晰的 Java 多线程程序。

1.ThreadLocal类

用途是保存线程的独立变量。对于一个继承自 Thread 的线程类而言。

使用 ThreadLocal 维护变量时,ThreadLocal 会为每个使用该变量的线程提供独立的变量副本。这样一来,每一个线程都能够独立地改变自己的副本,并且不会对其他线程所对应的副本产生影响。这种方式常用于用户登录控制,例如记录 session 信息。

实现:

每个 Thread 都拥有一个变量,此变量的类型是 ThreadLocalMap。ThreadLocalMap 是一种轻量级的 Map,它的功能和普通的 map 相同,区别在于其桶中存放的是 entry 而不是 entry 的链表,本质上它依然是一个 map。这个变量以 Thread 本身为 key,以目标为 value。

主要的方法有 get()以及 set(T a)。在 set 操作之后,会在 map 中维护一个 threadLocal 与 a 的关联。而在 get 操作时,会将 a 返回。ThreadLocal 是一种特殊的容器。

原子类包含 AtomicInteger、AtomicBoolean 等。

如果使用atomic wrapper class

如果使用 atomicInteger,或者采用能够保证原子性的操作,那么这就等同于 synchronized 。

//返回值为boolean
AtomicInteger 执行比较并设置操作,比较的预期值为 int 类型的 expect,要更新的值为 int 类型的 update

该方法能够用来实现乐观锁。文中最初提到了这样一个场景:a 向 b 付款 10 元,a 扣除了 10 元,b 需要增加 10 元。在这个时候,c 给 b 2 元,然而 b 的增加 10 元的代码大致如下:

如果 b 的值通过 compareAndSet 方法成功地将旧值 old 替换为新值 value ,那么
   return ;
}else{
   //try again
如果那样失败了,就进行回滚并记录
}

AtomicReference

对于 AtomicReference 而言,有可能会出现对象的情况,并且存在属性丢失的现象。具体表现为 oldObject 等于 current,但 oldObject 的 getPropertyA 不等于 current 的 getPropertyA。

这时候, AtomicStampedReference 开始发挥作用了。这是一个较为常用的思路,也就是添加版本号。

3.Lock类

lock 在 java.util.concurrent 包内,它共有三个实现。

主要目的是如同 synchronized 一般。synchronized 是为了解决同步问题,处理资源争端而产生的技术,而它也是为了解决同步问题,处理资源争端而产生的技术。两者功能类似但存在一些区别。

区别如下:

lock 更加灵活,它能够自由地定义多把锁的枷锁解锁顺序,而 synchronized 则需要按照先加锁后解锁的顺序进行。

提供多种加锁的方案,有 lock 这种阻塞式的,有 trylock 这种无阻塞式的,有 lockInterruptily 这种可打断式的,还有 trylock 的带超时时间的版本。

本质上和监视器锁(即synchronized是一样的)

能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。

和Condition类的结合。

性能更高,对比如下图:

ReentrantLock

可重入的意义在于:持有锁的线程能够继续持有该锁,并且需要释放对等的次数之后,才能够真正地释放该锁。

使用方法是:

1.先new一个实例

定义了一个名为 r 的静态 ReentrantLock 对象,即创建了一个可重入锁。

2.加锁

使用 r.lock() 或者 r.lockInterruptibly() 。

此处存在一个差异,后者能够被打断。当 a 线程进行锁定之后,b 线程会处于阻塞状态。

此时若为 lockInterruptibly,那么在执行 b.interrupt()之后,b 线程会退出阻塞状态,并且会放弃对资源的争抢,进而进入 catch 块。

如果使用后者,就必须抛出可中断异常或者进行捕获。

3.释放锁

r.unlock()

必须要做这件事。什么是必须做的呢?就是要放在 finally 里面。这样做是为了防止异常跳出正常流程而引发灾难。这里补充一个小知识点,finally 是可以被信任的:经过测试,即便发生了 OutofMemoryError 这种情况,finally 块中的语句也能够确保被执行。

ReentrantReadWriteLock

可重入读写锁(读写锁的一个实现)

创建了一个可重入读写锁,其对象名为 lock,通过 new ReentrantReadWriteLock() 进行初始化。
获取锁的读锁,将其赋值给 ReadLock r 。
获取锁 lock 的写锁,将其赋值给 WriteLock w 。

两者均具备 lock 和 unlock 方法。在进行写操作和读操作时会相互排斥;而进行读操作时则不会相互排斥。这样便能够实现并发读的高效且线程安全的代码。

4.容器类

这里就讨论比较常用的两个:

BlockingQueue

ConcurrentHashMap

BlockingQueue

阻塞队列。它是 java.util.concurrent 包下的重要类。通过对 Queue 的学习我们可以知道,这个 queue 是单向队列,能够在队列头添加元素,也能在队尾删除或取出元素。它类似于一个管道,特别适合用于先进先出策略的一些应用场景。普通的 queue 接口的主要实现形式有 PriorityQueue(优先队列),若有兴趣可以对其进行研究。

BlockingQueue 具备在队列基础上添加多线程协作功能这一特点:

除了具备传统的 queue 功能(也就是表格左边的两列)之外,还提供了阻塞接口 put 和 take ,以及带超时功能的阻塞接口 offer 和 poll 。当队列满时,put 会进入阻塞状态,直至有空间时才会被唤醒;而当队列空时,take 会处于阻塞状态,只有在有东西可拿时才会被唤醒。将其用于生产者 - 消费者模型时,效果特别好,简直堪称神器。

常见的阻塞队列有:

ArrayListBlockingQueue

LinkedListBlockingQueue

DelayQueue

SynchronousQueue

ConcurrentHashMap

高效的线程安全哈希map。

对比 HashMap 。

5.管理类

管理类的概念较为宽泛。它可用于管理线程,自身并非多线程的。不过,它提供了一些机制,能够利用上述的工具进行一些封装。

值得一提的管理类有:ThreadPoolExecutor 以及 JMX 框架下的系统级管理类 ThreadMXBean 。

ThreadPoolExecutor

如果对这个类不了解,那么就应该去了解前面提到过的 ExecutorService 。开一个属于自己的线程池是非常方便的。

创建了一个线程池对象 e,该线程池是通过 Executors 的 newCachedThreadPool 方法创建的。
创建一个执行服务对象 e,该对象通过 Executors 的 newSingleThreadExecutor 方法创建,即创建了一个单线程的线程池执行服务。
创建一个线程池执行服务 e,通过 Executors 的 newFixedThreadPool 方法创建,该线程池的大小为 3。
    // 第一种是可变大小线程池,按照任务数来分配线程,
第二种是单线程池,它的作用相当于 FixedThreadPool(1) 。
    // 第三种是固定大小线程池。
    // 然后运行
e 执行了一个新的 MyRunnableImpl 对象。

这类内部是由 ThreadPoolExecutor 来实现的。掌握这类能够有助于对线程池的管理进行理解。本质上,它们都是 ThreadPoolExecutor 类的不同实现版本。请参考 javadoc 。

翻译一下:

池内线程的初始值与最小值是 corePoolSize,即便处于空闲状态,也会一直保持这个数量的线程。

线程最大值为 maximumPoolSize,线程的增长始终不会超过这个值。

当池内线程数高于 corePoolSize 时,多余的空闲线程会在经过一定时间后被回收,这个时间就是 keepAliveTime,并且在回收之前这些线程处于 wait 状态。

unit:

时间单位,能够运用 TimeUnit 的实例,例如 TimeUnit.MILLISECONDS 。

workQueue 是待入任务(Runnable)的等待场所,此参数主要对调度策略产生影响,比如影响调度是否公平,是否会导致饿死(starving)的情况发生。

线程工厂类为 threadFactory,它有默认的实现。如果需要自定义线程工厂类,就需要自己去实现 ThreadFactory 接口,并且将实现后的接口作为参数传入。

请注意:该类十分常用,作者80%的多线程问题靠他。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

28阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

25阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读