Jacky's blog
首页
  • Android
  • Web
  • Server
  • Python
  • iOS
  • Java
  • Vue
  • 计算机基础
  • AI
  • 全部工具
  • CLI 工具
  • 编辑器
  • 开发工具
  • 构建工具
  • 版本控制
  • 调试工具
  • 网络工具
  • 个人成长
  • 博客
  • 分类
  • 标签
  • 归档
收藏
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • Android
  • Web
  • Server
  • Python
  • iOS
  • Java
  • Vue
  • 计算机基础
  • AI
  • 全部工具
  • CLI 工具
  • 编辑器
  • 开发工具
  • 构建工具
  • 版本控制
  • 调试工具
  • 网络工具
  • 个人成长
  • 博客
  • 分类
  • 标签
  • 归档
收藏
GitHub (opens new window)
  • java base
  • Java 面试高频问题指南
  • 其他

  • throwable
  • thread
    • state
    • jvm 内存模型
      • JMM 的关键概念
      • Happens-Before 规则
      • JMM 中的常见问题
      • JMM 与 volatile 关键字
      • JMM 与 synchronized
    • common method
      • interrupt
      • wait
      • notify/notifyAll
      • join
      • wait 和 yield(或 sleep)的区别?
      • 线程死锁, 如何避免线程死锁?
    • 同步/异步
      • 1.Object 的 wait 和 notify/notifyAll/synchronized 如何实现线程同步?
      • HandlerThread
      • CountDownLatch
      • Semaphore
      • ReentrantLock
    • other
      • 线程池
      • 开启线程
      • 并发容器
      • CopyOnWrite 容器
    • link
  • weakreference
  • UnSafe
  • collections
  • Class
  • classloader
  • java
Jacky
2024-08-13
目录

thread

# state

https://juejin.im/post/5b31b510e51d4558a426f7e9 (opens new window)

  • 新状态: 线程对象已经创建, 还没有在其上调用 start() 方法
  • 就绪: 当线程有资格运行, 但调度程序还没有把它选定为运行线程, 这时候线程所处的状态就做就绪状态。当 start()方法调用时, 线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后, 也返回到可运行状态。另外 yield 可以让线程由"运行状态"进入到"就绪状态"
  • 运行: 线程调度程序从可运行池中选择一个线程来执行。这也是线程进入运行状态的唯一一种方式
  • 等待/阻塞/睡眠: 这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种, 其共同点是: 线程仍旧是活的, 但是当前没有条件运行。换句话说, 它是可运行的, 但是如果某件事件出现, 他可能返回到可运行状态。另外 wait 可以让线程由"运行状态"进入到"等待(阻塞)状态"
  • 死亡态: 当线程的 run()方法完成时就认为它死去。这个线程对象也许是活的, 但是, 它已经不是一个单独执行的线程。线程一旦死亡, 就不能复生。 如果在一个死去的线程上调用 start()方法, 会抛出 java.lang.IllegalThreadStateException 异常

# jvm 内存模型

Java 内存模型(JMM)定义线程间共享变量的读写和同步语义,不等同于 JVM 的堆、栈等内存区域划分。它提供可见性、原子性及顺序约束的规则;只有代码正确使用这些规则,才能获得相应保证,不能说 JMM 自动保证所有并发程序线程安全。

# JMM 的关键概念

“主内存与工作内存”可以作为理解共享变量的抽象模型,但不能把它直接解释为 CPU 缓存或物理内存,也不能认为每次 volatile 访问都必须直接绕过缓存访问内存。

概念 保证及边界
可见性 同步动作建立跨线程的可见性关系;没有正确同步时,读取结果不能按直觉推断
原子性 单次操作不可被观察为部分完成;不意味着执行期间线程不能被调度或抢占
有序性 实现可以重排序,但必须遵守单线程语义和 JMM 的跨线程约束
volatile 对同一 volatile 变量的写入先行发生于后续读取;不能把 i++ 等复合操作变成原子操作
synchronized 持有同一 monitor 的线程互斥访问;解锁与后续获取同一 monitor 建立可见性关系

引用和多数基本类型的单次读写具有原子性;规范允许非 volatile 的 long、double 读写分成两个 32 位操作。即使某个平台实现了它们的原子读写,也不能据此忽略可见性和复合操作同步。

# Happens-Before 规则

happens-before 描述可见性与顺序保证,不要求机器执行顺序与源码完全一致。常用规则如下:

规则 必要条件
程序顺序 同一线程中,前面的动作先行发生于后面的动作
monitor 解锁与加锁 对同一 monitor 的解锁先行发生于后续加锁
volatile 写与读 必须针对同一个 volatile 变量,写入先行发生于后续读取
线程启动 调用 start() 前的动作对被启动线程可见
线程终止与等待 成功通过 join() 检测到终止后,可见该线程之前的动作
传递性 A 先行发生于 B,B 先行发生于 C,则 A 先行发生于 C

“对象完成初始化”本身不是对其他线程任意读取引用的安全发布保证。共享普通引用时,必须考虑发布路径,例如通过 volatile 引用、同一把锁或类初始化建立保证。final 字段还有特殊初始化语义,不能把它扩大为所有对象字段的通用规则。

# JMM 中的常见问题

问题 处理方式
用普通共享变量作为结束标志 根据场景采用 volatile、锁或其他同步机制
用 volatile 计数器执行 i++ 改用锁或 AtomicInteger 等提供原子更新的 API
把“看到对象引用”当作“所有字段初始化可见” 检查安全发布路径,避免构造期间泄漏 this
认为 synchronized 块内部绝不重排序 以互斥及 happens-before 判断可观察行为,而不是假定指令顺序固定

# JMM 与 volatile 关键字

volatile 用于建立特定的内存顺序与可见性保证,并不禁止所有指令重排序。写入同一 volatile 变量之前的普通写入,可以通过后续读取该 volatile 变量的线程获得可见性。

例如,初始化对象后写入 volatile 引用;读取方先读取该引用,若非空,再使用对象字段:

class PublishedConfig {
    static class Config {
        int value;

        Config(int value) {
            this.value = value;
        }
    }

    private volatile Config config;

    void publish() {
        config = new Config(42);
    }

    int read() {
        Config local = config;
        return local == null ? -1 : local.value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

示例只说明发布时的初始化可见性。发布后若继续修改 Config 的普通字段,仍需要为这些读写建立同步,不能因引用是 volatile 就认为对象内部所有操作都线程安全。

# JMM 与 synchronized

对同一 monitor 的互斥访问,以及解锁先行发生于后续加锁,是 synchronized 的关键保证。同步块内部仍可进行不破坏这些保证的优化或重排序。使用不同锁,或者有一方不按相同同步协议访问共享变量,不能自动获得这些保证。

来源:Java 17 语言规范:JMM 与 happens-before (opens new window)、final 字段语义 (opens new window)、long 与 double 的读写 (opens new window)。

# common method

# interrupt

Java 并发编程 之二: 线程中断 (opens new window)

注意一下: 待决中断

通常, 我们通过"标记"方式终止处于"运行状态"的线程。其中, 包括"中断标记"和"额外添加标记"

说明: isInterrupted()是判断线程的中断标记是不是为 true。当线程处于运行状态, 并且我们需要终止它时;可以调用线程的 interrupt()方法, 使用线程的中断标记为 true, 即 isInterrupted()会返回 true。此时, 就会退出 while 循环 注意: interrupt() 并不会终止处于"运行状态"的线程!它会将线程的中断标记设为 true

比较通用的终止线程的形式:

@Override
public void run() {
    try {
        // 1. isInterrupted()保证, 只要中断标记为true就终止线程
        while (!isInterrupted()) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {
        // 2. InterruptedException异常保证, 当InterruptedException异常产生时, 线程被终止
    }
}
1
2
3
4
5
6
7
8
9
10
11

# wait

*  The current thread must own this object's monitor. The thread
* releases ownership of this monitor and waits until another thread
* notifies threads waiting on this object's monitor to wake up
* either through a call to the {@code notify} method or the
* {@code notifyAll} method. The thread then waits until it can
* re-obtain ownership of the monitor and resumes execution.
1
2
3
4
5
6

wait() 的作用是让当前线程进入等待状态, 同时,让当前线程释放它所持有 object 对象锁

# notify/notifyAll

Wakes up all threads that are waiting on this object's monitor. A thread waits on an object's monitor by calling one of the {@code wait} methods. This method should only be called by a thread that is the owner of this object's monitor.

  • 唤醒此对象监视器上所有的等待的线程。一旦一个对象调用了 wait 方法, 必须要采用 notify()和 notifyAll()方法唤醒该线程
  • notify()和 notifyAll()的作用, 则是唤醒在此对象监视器上等待的单个/全部线程

# join (opens new window)

join() 方法的作用, 是等待这个线程结束; t.join() 方法阻塞调用此方法的线程(calling thread), 直到线程 t 完成, 此线程再继续;通常用于在 main()主线程内, 等待其它线程完成再结束 main()主线程

源码中:

Join 方法实现是通过 wait(小提示: Object 提供的方法)。 当 main 线程调用 t.join 时候, main 线程会获得线程对象 t 的锁(wait 意味着拿到该对象的锁),调用该对象的 wait(等待时间), 直到该对象唤醒 main 线程 , 比如退出后。这就意味着 main 线程调用 t.join 时, 必须能够拿到线程 t 对象的锁

# wait 和 yield(或 sleep)的区别?

wait()是让线程由"运行状态"进入到"等待(阻塞)状态" yield()是让线程由"运行状态"进入到"就绪状态", 从而让其它具有相同优先级的等待线程有机会获取执行权;但是, 并不能保证在当前线程调用 yield()之后, 其它具有相同优先级的线程就一定能获得执行权。 > wait()是会在线程释放它所持有对象的同步锁, 而 yield()方法不会释放锁。 Thread.sleep(long millis)和 Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠.当线程睡眠时, 它入睡在某个地方, 在苏醒之前不会返回到可运行状态。当睡眠时间到期, 则返回到可运行状态(就绪)

# 线程死锁, 如何避免线程死锁?

  • 什么是死锁: 多个线程因竞争资源而造成的一种僵局(互相等待), 若无外力作用, 这些进程都将无法向前推进

# 同步/异步

# 1.Object 的 wait 和 notify/notifyAll/synchronized 如何实现线程同步?

以 android 中的 HandlerThread (Handy class for starting a new thread that has a looper) 的源码为例说明

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        /**
         * 获取到当前Thread的Looper对象
         * 然后通知 getLooper 方法, 这个时候可以返回(同步)
         */
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

public Looper getLooper() {
    if (!isAlive()) {
        return null;
    }

    // If the thread has been started, wait until the looper has been created.
    synchronized (this) {
        while (isAlive() && mLooper == null) {
            try {
                /**
                 * 等待run方法的Looper对象初始化完成
                 */
                wait();
            } catch (InterruptedException e) {
            }
        }
    }
    return mLooper;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

再比如换肤框架 Android-skin-support (opens new window)中 loadSkin 使用了的同步示例:

private final Object mLock = new Object();

public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
    return new SkinLoadTask(listener, mStrategyMap.get(strategy)).execute(skinName);
}

private class SkinLoadTask extends AsyncTask<String, Void, String> {
    private final SkinLoaderListener mListener;
    private final SkinLoaderStrategy mStrategy;

    SkinLoadTask(@Nullable SkinLoaderListener listener, @NonNull SkinLoaderStrategy strategy) {
        mListener = listener;
        mStrategy = strategy;
    }

    protected void onPreExecute() {
        if (mListener != null) {
            mListener.onStart();
        }
    }

    @Override
    protected String doInBackground(String... params) {
        synchronized (mLock) {
            while (mLoading) {
                try {
                    mLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mLoading = true;
        }
        try {
            if (params.length == 1) {
                if (TextUtils.isEmpty(params[0])) {
                    SkinCompatResources.getInstance().reset();
                    return params[0];
                }
                if (!TextUtils.isEmpty(
                        mStrategy.loadSkinInBackground(mAppContext, params[0]))) {
                    return params[0];
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        SkinCompatResources.getInstance().reset();
        return null;
    }

    protected void onPostExecute(String skinName) {
        SkinLog.e("skinName = " + skinName);
        synchronized (mLock) {
            // skinName 为""时, 恢复默认皮肤
            if (skinName != null) {
                SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor();
                notifyUpdateSkin();
                if (mListener != null) mListener.onSuccess();
            } else {
                SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor();
                if (mListener != null) mListener.onFailed("皮肤资源获取失败");
            }
            mLoading = false;
            mLock.notifyAll();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# HandlerThread

HandlerThread thread = newHandlerThread("handler_thread");
thread.start();//必须要调用start方法
final Handlerhandler = newHandler(thread.getLooper()){}
1
2
3

main api

  • getLooper() : 返回与该线程相关联的 Looper 对象

# CountDownLatch

CountDownLatch 是一种闭锁实现,计数器初始化为非负整数,表示需要等待的事件数量。countDown() 在计数大于 0 时将其递减,await() 等待计数到达 0;初始计数为 0 时,await() 立即返回。计数归零后不能重置。在完成一组其他线程中的操作之前,它允许一个或多个线程等待。参考 (opens new window)

使用场景

  1. 例如我们上例中所有人都到达饭店然后吃饭
  2. 某个操作需要的资源初始化完毕
  3. 某个服务依赖的线程全部开启等等...

main api

//构造方法参数指定了计数的次数
public CountDownLatch(int count);

//countDown方法, 当前线程调用此方法, 则计数减一
public void countDown();

//awaint方法, 调用此方法会一直阻塞当前线程, 直到计时器的值为0
public void await() throws InterruptedException
1
2
3
4
5
6
7
8

# Semaphore

// 允许最多0个线程获取许可
final Semaphore semaphore = new Semaphore(0);
final Boolean[] result = new Boolean[1];
fileDecodingQueue.postRunnable(new Runnable() {
    @Override
    public void run() {
        result[0] = openOpusFile(messageObject.location.path) != 0;
        semaphore.release();
    }
});
semaphore.acquire();
1
2
3
4
5
6
7
8
9
10
11

main api

  • <init>(count): 初始许可个数
  • acquire(): 请求许可, 如果没有许可, 那么 acquire 方法将会一直阻塞直到有许可(或者直到被终端或者操作超时)
  • release(): 释放许可

case: 使用信号量机制, 实现互斥访问打印机

public class MutexPrint {
    private final int PrintCount = 2;    //打印机的数量

    private final Semaphore semaphore = new Semaphore(PrintCount);

    public void print(String str) throws InterruptedException {
        semaphore.acquire();

        System.out.println(Thread.currentThread().getName() + " enter ...");
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + " 正在打印 ..." + str);
        System.out.println(Thread.currentThread().getName() + " out ...");
        semaphore.release();
    }

    public static void main(String[] args) {
        final MutexPrint print = new MutexPrint();

        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    try {
                        print.print("helloworld");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# ReentrantLock

可重入锁, 互斥锁 (opens new window), 用法参照 LinkedBlockingQueue 源码实现

  • 重入性: 指的是同一个线程多次试图获取它所占有的锁, 请求会成功。当释放锁的时候, 直到重入次数清零, 锁才释放完毕

ReentrantLock 将由最近成功获得锁, 并且还没有释放该锁的线程所拥有。如果当前线程已经拥有该锁, 此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生 此类的构造方法接受一个可选的公平 参数。当设置为 true 时, 在多个线程的争用下, 这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比, 使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢, 常常极其慢), 但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是, 公平锁不能保证线程调度的公平性。因此, 使用公平锁的众多线程中的一员可能获得多倍的成功机会, 这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是, 未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待, 只要该锁是可用的, 此方法就可以获得成功

main api

ReentrantLock()        // 创建一个自由竞争的可重入锁
ReentrantLock(boolean fair)

lock
unlock
isHeldByCurrentThread
getHoldCount
lockInterruptibly
1
2
3
4
5
6
7
8

ReentrantLock vs synchronized

**不同: ** 1.ReentrantLock 功能性方面更全面, 比如时间锁等候, 可中断锁等候, 锁投票等, 因此更有扩展性。在多个条件变量和高度竞争锁的地方, 用 ReentrantLock 更合适, ReentrantLock 还提供了 Condition, 对线程的等待和唤醒等操作更加灵活, 一个 ReentrantLock 可以有多个 Condition 实例, 所以更有扩展性 2.ReentrantLock 必须在 finally 中释放锁, 否则后果很严重, 编码角度来说使用 synchronized 更加简单, 不容易遗漏或者出错 3.ReentrantLock 的性能比 synchronized 会好点 4.ReentrantLock 提供了可轮询的锁请求, 他可以尝试的去取得锁, 如果取得成功则继续处理, 取得不成功, 可以等下次运行的时候处理, 所以不容易产生死锁, 而 synchronized 则一旦进入锁请求要么成功, 要么一直阻塞, 所以更容易产生死锁

  1. Lock 的某些方法可以决定多长时间内尝试获取锁, 如果获取不到就抛异常, 这样就可以一定程度上减轻死锁的可能性 如果锁被另一个线程占据了, synchronized 只会一直等待, 很容易错序死锁
  2. synchronized 的话, 锁的范围是整个方法或 synchronized 块部分;而 Lock 因为是方法调用, 可以跨方法, 灵活性更大
  3. 便于测试, 单元测试时, 可以模拟 Lock, 确定是否获得了锁, 而 synchronized

# other

# 线程池

  • Java 并发专题 带返回结果的批量任务执行 CompletionService ExecutorService.invokeAll (opens new window)
  • JAVA 线程池的分析和使用-ThreadPoolExecutor (opens new window)

Java 四种线程池的使用

link (opens new window)

  • newCachedThreadPool 创建一个可缓存线程池, 如果线程池长度超过处理需要, 可灵活回收空闲线程, 若无可回收, 则新建线程
  • newFixedThreadPool 创建一个定长线程池, 可控制线程最大并发数, 超出的线程会在队列中等待
  • newScheduledThreadPool 创建一个定长线程池, 支持定时及周期性任务执行
  • newSingleThreadExecutor 创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

# 开启线程

  • 开启线程的三种方式 (opens new window)

# 并发容器

  • 工具篇-BlockingQueue (opens new window)

  • Java ArrayDeque 源码剖析 (opens new window)

  • ArrayDeque 集合的妙用,栈;队列 (opens new window)

  • ConcurrentHashMap

  • ConcurrentLinkedDeque

  • LinkedBlockingDeque 并发

  • CopyOnWriteArrayList

接口

  • Queue
  • Deque
  • BlockingDeque

# CopyOnWrite 容器

CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候, 不直接往当前容器添加, 而是先将当前容器进行 Copy, 复制出一个新的容器, 然后新的容器里添加元素, 添加完元素之后, 再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读, 而不需要加锁, 因为当前容器不会添加任何元素。所以CopyOnWrite 容器也是一种读写分离的思想, 读和写不同的容器.

应用场景

CopyOnWrite 并发容器用于读多写少的并发场景。比如白名单, 黑名单, 商品类目的访问和更新场景, 假如我们有一个搜索网站, 用户在这个网站的搜索框中, 输入关键字搜索内容, 但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中, 黑名单每天晚上更新一次。当用户搜索时, 会检查当前关键字在不在黑名单当中, 如果在, 则提示不能搜索

# link

#Java#base
上次更新: 2026/10/18, 22:14:48
throwable
weakreference

← throwable weakreference→

最近更新
01
Codex 使用与会话管理指南
10-17
02
Tailscale 入门:让自己的设备跨网络互联
10-15
03
GitHub CLI(gh)工作流与 CI 排查
10-08
更多文章>
Theme by Vdoing | Copyright © 2019-2026 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式