多线程
快来分享你的内容吧~
深入剖析 Java synchronized
在 Java 的进阶学习中,多线程环境下对 **临界区代码的同步处理** 是一个非常重要的内容。其中,`synchronized` 关键字作为实现临界资源同步最直接、最简洁的方式,扮演着关键角色 尽管 `synchronized` 使用简单,但它的底层实现却涉及 **JVM 锁机制**、**对象头**、**Monitor**、**锁升级** 等复杂概念。很多人仅仅停留在“加锁能保证线程安全”的层面,却不知道: + 为什么 `synchronized` 既能修饰方法,也能修饰代码块? + 锁到底存储在对象的哪个部分? + `synchronized` 源码是如何实现的 + **重量级锁** 加锁解锁流程 + 锁升级是如何优化性能的? 本文将从 **使用方式**、**字节码分析**、**对象头结构**、**锁升级过程**、**性能优化** 等多个维度,深入解析 `synchronized` 的底层原理,帮助你在高并发场景下写出更高效的代码,本文大量参考了 B 站视频 UP [<font style="color:rgb(24, 25, 28);">黑马程序员满老师</font>](https://space.bilibili.com/416747307)。 #### 临界区 **临界区** 是指一段可能被多个线程同时执行的代码,而这段代码访问了 **共享资源**(变量、集合或者文件),由于多个线程同时访问这些共享资源,可能会导致数据不一致或程序行为异常,例如下面这个非常经典的例子 <img src="https://pic.code-nav.cn/post_picture/1621031759393169410/8POLyKFt5ZTsUseO.webp" alt="" width="100%" /> 两个线程同时对 `count` 进行累加和累减操作,最终结果应当为 0,但由于 `count++` 和 `count--` 操作不是原子性的而导致最后结果五花八门 这就是最常见的线程安全问题,使用 synchronzed 就能规避这个问题🙋 #### synchronized 的三种实现 `synchronized` 可以用于 **方法** 或 **代码块** 中,其主要有以下三种典型使用方式: ##### 修饰代码块 在 `synchronized` 中使用同一个 **锁对象** 锁住临界区的代码 ```java private static final Object LOCK = new Object(); Thread thread1 = new Thread(() -> { synchronized (LOCK) { for (int i = 0; i < LOOP_NUM; i++) { count++; } } }); Thread thread2 = new Thread(() -> { synchronized (LOCK) { for (int i = 0; i < LOOP_NUM; i++) { count--; } } }); ``` 这是最灵活的方法,通过锁住具体的代码可以实现更细粒度的锁控制 ##### 修饰实例方法 `synchronized` 也可以加载实例方法上,相当于对方法整体加锁,加锁对象为 **当前实例对象** ```java class ThreadSafeCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public int getCount() { return this.count; } } ThreadSafeCounter threadSafeCounter = new ThreadSafeCounter(); Thread thread1 = new Thread(() -> { for (int i = 0; i < LOOP_NUM; i++) { threadSafeCounter.increment(); } }) Thread thread2 = new Thread(() -> { for (int i = 0; i < LOOP_NUM; i++) { threadSafeCounter.decrement(); } }); ``` 使用的时候创建出 **实例对象**,等同于 `synchronized(this)`,当然创建出两个实例,两个实例之间的对象加锁是互不干扰的 ##### 修饰静态方法 `synchronized` 关键字还可以加在 `静态方法` 上,加锁对象为当前 `class 类` ```java private static synchronized void increment() { for (int i = 0; i < LOOP_NUM; i++) { count--; } } ``` 例如下面两个方法,加锁的对象就是 `SynchronizedLearning.class` ```java Thread thread2 = new Thread(SynchronizedLearning::increment); Thread thread1 = new Thread(SynchronizedLearning::decrement); ``` #### 字节码分析 对字节码的分析能让我们对 java 程序更加了解,首先对 `synchronized` 加锁的代码块进行分析,通过 `javap` 工具可以查看字节码的信息 ```java private static void lambda$main$0(); descriptor: ()V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=3, args_size=0 0: getstatic #17 // Field LOCK:Ljava/lang/Object; 3: dup 4: astore_0 5: monitorenter 6: iconst_0 7: istore_1 8: iload_1 9: ldc #19 // int 100000 11: if_icmpge 28 14: getstatic #13 // Field count:I 17: iconst_1 18: iadd 19: putstatic #13 // Field count:I 22: iinc 1, 1 25: goto 8 28: aload_0 29: monitorexit 30: goto 38 33: astore_2 34: aload_0 35: monitorexit 36: aload_2 37: athrow 38: return Exception table: from to target type 6 30 33 any 33 36 33 any ``` 上面的字节码对应了上面 thread1 的代码部分,我们主要对加锁的字节码进行重点分析 ```java 0: getstatic ``` 用于获取 **锁对象**,是本类中的静态字段 `LOCK`,并压入操作数栈中 ```java 3: dup ``` `dup` 命令用于 **复制操作数栈顶部元素**(也就是 `LOCK` 对象)原因是接下来两个地方要用这个锁对象 + `monitorenter` 加锁操作需要使用 + 如果发生异常,`monitorexit` 需要再次使用到 ```java 5: monitorenter ``` 🔒 加锁指令!为什么是 monitor,后面会提到 synchronized 在不同线程竞争情况下,采用操作系统中 monitor 实现 + JVM 会尝试获取对象锁(即对 LOCK 对象加锁) + 如果其它线程已持有锁,则会阻塞直到获取锁 从这里开始进入 `synchronized` 控制的代码块区域 ```java 29: monitorexit 30: goto 38 ...... 38: return ``` 这里 `monitorexit` 是去释放锁,执行完代码块的任务,然后 38 行🔚 方法执行结束 但是可以看到在方法执行结束之前,还有部分字节码 ```java 33: astore_2 34: aload_0 35: monitorexit 36: aload_2 37: athrow ``` 这部分就是 JVM 高明的地方,考虑到开发者都没考虑到的地方,这里要结合下面的 `Exception table` ```java Exception table: from to target type 6 30 33 any 33 36 33 any ``` 这张表里记录了所有可能 **出现异常的代码块部分** 和 处理部分 这里第一行 `6 - 30` 行代码是整个加锁和处理部分,如果这部分代码出现了任何异常,字节码都会跳到 `33` 行字节码开始执行,也就是上面部分代码 这里就做了一件事情,`monitorexit` 去释放 `synchronized` 加的锁,防止出现这部分代码一直占住锁 这部分是 JVM 给我们生成的,开发者不需要去考虑 `synchronized` 代码块中出现异常时 **锁释放** 问题,这也正是 JVM 厉害👍的地方 以上是 synchronized 针对代码块进行加锁,如果是针对 **实例方法** 或者 **静态方法** 加锁,对应的方法字节码如下 ```java public synchronized void increment(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field count:I 5: iconst_1 6: iadd 7: putfield #2 // Field count:I 10: return LineNumberTable: line 89: 0 line 90: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lioc/ThreadSafeCounter; ``` 其中 flags 中会记录 `ACC_SYNCHRONIZED`,标志这个方法被 synchronized 关键词修饰,如果是 **静态方法(ACC_STATIC)**,JVM 就会锁住 `class`,如果是 **实例方法**,则锁住对象本身(`this`) ```java private static void lambda$main$0(ioc.ThreadSafeCounter); descriptor: (Lioc/ThreadSafeCounter;)V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC ``` #### Mark Word 在 JVM 中,每个对象在堆内存中的存储结构分为 **对象头**、**实例数据**、**对齐填充** 三部分: + **对象头** 是 JVM 为每个对象预留的**元信息区域**,最重要的内容有两部分 - **Mark Word**,后面将会重点介绍 - **Klass Pointer**,是指向 **元数据(Class Metadata)**,找到自己的类信息,包括字段定义、方法表、接口、父类、GC信息等等 + **实例对象**,用于存储类的字段信息 + **对齐填充(padding)**,为了提高内存访问效率,JVM 要求对象大小是 8 字节的整数倍,若对象头 + 实例数据加起来不是 8 的倍数,就会 **自动添加填充字节** **Mark Word** 是 `synchronized` 实现锁的关键,存储了当前锁的类型和一些对象状态,下面是 64 位 JVM 中 Mark Word 的结构,在不同锁状态下 Mark Word 存储内容也不同 ```java |--------------------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |--------------------------------------------------------------------------------|--------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | lock: 01 | Normal | |--------------------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock: 01 | Biased | |--------------------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | lock: 00 | Lightweight Locked | |--------------------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | lock: 10 | Heavyweight Locked | |--------------------------------------------------------------------------------|--------------------| | | lock: 11 | Marked for GC | |--------------------------------------------------------------------------------|--------------------| ``` 其中 lock 位表示了当前对象的不同加锁状态: + `01`:无锁 / 偏向锁 + `00`:轻量级锁 + `10`:重量级锁 + `11`:GC 标记 接下来结合 synchronized 锁升级的过程来详细讲述 Mark Word 中的内容 #### synchronized 锁升级 这里为了能让 Java 输出 **Mark Word** 数据,使用了 `JOL` 工具来查看当前对象 **Mark Word** 信息,但 JOL 生成对象头信息过多,不方便我们查看,这里我自己写了一个工具类将对象头中 Mark Word 信息展示出来,并转成 **二进制**,方便我们查看 ```xml <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.17</version> </dependency> ``` 在 JDK 15 之后废弃⚠️偏向锁,考虑到目前 JDK 都已经升级到 24版本,**不对偏向锁做介绍** ##### 无锁状态 对象刚创建时,属于无锁状态 ```java Object lock = new Object(); System.out.println(MarkWordUtils.getMarkWordBinary(lock)); ``` 通过 `JOL` 工具在控制台打印出了 `lock` **对象头信息** ```java 0000000000000000000000000000000000000000000000000000000000000001 ``` 如上面介绍的 `lock` 为 `01`,`biased_lock` 为 `0`,表示这个对象处于 **无锁状态** ##### 轻量级锁 ###### 1)加锁场景 当两个或者多个线程 **交替** 执行同步代码块,注意 **没有竞争!没有竞争!没有竞争!** ```java System.out.println("锁创建时 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t\t锁状态:" + MarkWordUtils.getLock(lock)); Thread t1 = new Thread(() -> { synchronized (lock) { System.out.println("t1 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); t1.start(); t1.join(); // 阻塞等待,保证 t1 线程执行完成再执行 t2 线程 Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); t2.start(); t2.join(); System.out.println("锁全部释放 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); ``` 在上面的代码中,`t1` 和 `t2` 虽然都是使用 `lock` 作为锁对象,但是两个线程并 **不是同时加锁**,轮流使用 lock 对象,下面是输出内容  可以看到当 `t1` 进入同步代码块的时候,`lock` 的 **Mark Word** 后两位变成了 `00`,即锁当前状态为轻量级锁 ###### 2)源码分析 这里查看 `synchronized` 加锁的 **jdk 17 源码** 来分析轻量级加锁过程,代码位置在 `src/hotspot/share/runtime/synchronizer.hpp` 中 ```cpp void ObjectSynchronizer::enter(Handle obj, BasicLock*lock, JavaThread*current) { // ... 前面代码省略 ... markWord mark = obj -> mark(); assert (!mark.has_bias_pattern(),"should not see bias pattern here"); if (mark.is_neutral()) { lock -> set_displaced_header(mark); if (mark == obj()->cas_set_mark(markWord::from_pointer (lock), mark)){ return; } // ... 后续代码省略 ... } // ... 后续代码省略 ... } ``` 这里只要先看 `if` 部分代码即可,`ObjectSynchronizer::enter` 方法对应上面字节码中的 `monitorenter` 命令,现在逐行来看加锁过程 + `mark.is_neutral()` 用来确定 **对象未被锁定(无锁状态)** + `lock->set_displaced_header(mark)`:保存原始 Mark Word,`lock` 是当前线程栈上分配的 **锁记录(Lock Record)**, `set_displaced_header` 将 mark 设置到 **锁记录** 中 **轻量级锁记录** 源码在同目录下 `basicLock` 中 ```cpp class BasicLock { // ... 前面代码省略 ... private: volatile markWord _displaced_header; // ... 后续代码省略 ... } class BasicObjectLock { // ... 前面代码省略 ... private: BasicLock _lock; // the lock, must be double word aligned oop _obj; // object holds the lock; // ... 后续代码省略 ... }; ``` 锁记录中主要存储了两个重要信息 + `BasicLock _lock` 用于保存 **锁对象的原始 Mark Word** + `oop _obj` 用于指向 **加锁对象** 再回到轻量级锁加锁源码中,`obj()->cas_set_mark(markWord::from_pointer(lock), mark)` 中 **原子交换** 当前对象 Mark Word 和 锁记录指针进行交换 + `from_pointer(lock)` 获取轻量级锁的 **地址指针** + `obj()->cas_set_mark` 是一个 **CAS(Compare-And-Sweep)**操作,简化成 `obj->cas(new, old)`,CAS 操作保证了操作的 **原子性** - 比较 old 值和 obj->mark 值是否相同,都是原始对象 Mark Word - new 值是轻量级锁记录 **地址指针 + 00**(轻量级锁 Mark Word 标识) - new 和 old 值进行交换,解锁的时候能根据地址解锁 + lock 对象 Mark Word 最后两位变成 `00`,轻量级锁加成功  ###### 3)重入锁 当前对象已经加了轻量级锁,并且是自己再次加锁。允许 **同一个线程** 多次获取 **同一把锁**,这种锁被称为「**可重入的(reentrant)**」 ```java new Thread(() -> { synchronized (lock) { synchronized (lock) { } } }).start(); ``` 对于已经 **持有轻量级锁** 的当前线程再次加锁,往栈桢中添加 **对象存储 null 的锁记录**,源码也很好理解 ```java else if (mark.has_locker() && current->is_lock_owned((address)mark.locker())) { assert(lock != mark.locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark().value(), "don't relock with same BasicLock"); lock->set_displaced_header(markWord::from_pointer(NULL)); return; } ``` `if` 条件判断当前是重入轻量级锁,添加锁记录代码为 `lock->set_displaced_header(markWord::from_pointer(NULL));`,往栈桢中添加存储为 NULL 锁记录 ###### 3)解锁步骤 源码中解锁方法为 `ObjectSynchronizer::exit`,对应字节码中的 `monitorexit` 命令,下面是轻量级锁的解锁部分源码 ```c void ObjectSynchronizer::exit(oop object, BasicLock*lock, JavaThread*current) { markWord mark = object -> mark(); markWord dhw = lock -> displaced_header(); // 判断是否为重入的锁 if (dhw.value() == 0) { return; } // 轻量级锁解锁流程 if (mark == markWord::from_pointer (lock)){ if (object -> cas_set_mark(dhw, mark) == mark) { return; } } // 省略后续代码... } ``` 首先先获取加 **锁对象的 Mark Word** 和 **锁记录头中存储的信息**,这里对象信息已经交换,`mark` 存储的是 锁记录地址指针,`dhw` 存储原始对象 Mark Word `dhw.value() == 0` 上面提到,如果是同一个线程重入的锁的锁记录头中存储的信息为 NULL,则不用执行任何操作,直接返回 当 `mark == markWord::from_pointer(lock)` 为 true 时,说明 + Mark Word 最后两位是 `00`(轻量级锁标识) + 锁被当前线程持有 > markWord::from_pointer(lock) 为 false 时说明: > > + 当前线程不持有轻量级锁 > + 当前锁对象已经成为重量级锁 > > 这是就会都会进入锁膨胀,并执行重量级锁解锁步骤 > 因此可以确认 **当前线程仍持有该对象的轻量级锁**,进入解锁流程,`object -> cas_set_mark(dhw, mark) == mark` 再把锁记录中的原始 Mark Word 通过 **CAS** 交换到锁对象的 Mark Word 中 + CAS 执行成功,成功解锁轻量级锁,对象重新变为 `无锁状态` + CAS 解锁失败,进入重量级锁解锁流程 ##### 重量级锁 如果轻量级锁加锁失败,分成下面几种情况: + 默认关闭轻量级锁加锁步骤 + CAS 竞争失败 + 其他线程已经占用轻量级锁 此时都会升级成为 **重量级锁**,进入重量级锁的加锁流程(锁膨胀),交由 **内核** 中的 `Monitor` 管理 ```c // An async deflation can race after the inflate() call and before // enter() can make the ObjectMonitor busy. enter() returns false if // we have lost the race to async deflation and we simply try again. while (true) { ObjectMonitor* monitor = inflate(current, obj(), inflate_cause_monitor_enter); if (monitor->enter(current)) { return; } } ``` ###### 1)Moniter 对象 当前有多个线程进行竞争时,就会触发 **锁膨胀**,从轻量级锁升级为 **重量级锁**,重量级锁会由 **Moniter** 对象进行管理 ```java System.out.println("锁创建时 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t\t锁状态:" + MarkWordUtils.getLock(lock)); Thread t1 = new Thread(() -> { synchronized (lock) { TimeUnit.MICROSECONDS.sleep(1); System.out.println("t1 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); // 启动线程,阻塞等待,省略代码 System.out.println("锁全部释放 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); ``` 这时两个对象发生争抢,`lock` 对象的 Mark Word 最后两位为 `10`,锁状态为 `重量级锁`  这里锁释放了不会恢复 `无锁状态`,依旧会处于重量级锁状态 `Monitor` 对象负责管理重量级锁,主要由三部分组成:**WaitSet**,EntryList 和 **Owner**  + **Owner**:当前持有线程,唯一能执行同步代码块,唯一能调用 `wait()` 方法 + **EntryList**:「想要锁但还没得到」的线程队列,线程状态为 `BLOCKED` + **WaitSet**:「主动放弃 Owner,等待条件」的线程(调用 `wait()` 方法),等待 `notify` 唤醒 ###### 2)锁膨胀 锁膨胀总共分为 4 种情况 + **Inflated**:已经膨胀完毕 + **Stack-locked**:从轻量级锁开始膨胀 + **Inflating**:膨胀中 + **Neutral**:从无锁状态开始膨胀 ```cpp for (;;) { const markWord mark = object->mark(); assert(!mark.has_bias_pattern(), "invariant"); // The mark can be in one of the following states: // * Inflated - just return // * Stack-locked - coerce it to inflated // * INFLATING - busy wait for conversion to complete // * Neutral - aggressively inflate the object. // * BIASED - Illegal. We should never see this } ``` 锁膨胀在一个 **死循环** 中,每一次都会获取对象的 Mark Word,判断当前状态,其中从无锁对象膨胀和从轻量级锁开始膨胀逻辑相似,只介绍从轻量级锁开始膨胀 > 1. Inflated 膨胀完成 > ```c // CASE: inflated if (mark.has_monitor()) { ObjectMonitor* inf = mark.monitor(); markWord dmw = inf->header(); return inf; } ``` 如果当前已经是重量级锁,直接获取 `Moniter` 对象返回 > 2. Stack-lock 从轻量级锁开始膨胀 > ```c if (mark.has_locker()) { // 1. 创建 moniter 对象 ObjectMonitor* m = new ObjectMonitor(object); // 2. 将当前对象头设置为全0,为膨胀中标识 markWord cmp = object->cas_set_mark(markWord::INFLATING(), mark); // 3. CAS 失败,删除 moniter 对象 if (cmp != mark) { delete m; continue; // Interference -- just retry } // 4.1 获取原始 mark word 值 markWord dmw = mark.displaced_mark_helper(); // 4.2 将原始 Mark word 存入 moniter 中 m->set_header(dmw); // 4.3 设置 mointer 对持锁线程 m->set_owner_from(NULL, mark.locker()); // 4.4 锁对象头设置新的 mark word object->release_set_mark(markWord::encode(m)); _in_use_list.add(m); return m; } ``` `mark.has_locker()` 用来判断当前对象是否为轻量级锁,创建一个新的 `moniter` 对象,通过 CAS 并将当前对象 mark word 全部置为 0,标志 **膨胀中** 状态,关键是下面设置重量级锁的 4 个步骤 1. `mark.displaced_mark_helper()` 获取原始的 **mark word** 的值 2. `m->set_header(dmw)` 把原始 mark word 值存储在 `moniter` 中 3. `m->set_owner_from(NULL, mark.locker());` 设置 `moniter` 的 **Owner**,即当前持重量级锁的线程 4. `object->release_set_mark(markWord::encode(m))` 将 **monitor 的地址** encode 编码 + `10` 存入锁对象 mark word 中 经过上面操作,会保证只有一个线程针对 `lock` 锁对象加 **重量级锁** 成功 > 3. inflating 膨胀中 > 当前处于膨胀中的状态代码很好理解,`read_stable_mark` 方法等待一会 ```cpp if (mark == markWord::INFLATING()) { read_stable_mark(object); continue; } ``` 所有处于膨胀中的线程都是上一步 CAS 加锁竞争失败的,会等待真正加锁的线程加锁成功,在这里 **循环等待** ###### 3)误区:轻量级锁真的有自旋吗? 网上有些说法说 **轻量级锁** 膨胀为 **重量级锁** 时会去尝试自旋等待轻量级锁释放,自旋 10 次失败升级为重量级锁  甚至问 AI 有时也会得到这样的结果,还有一个合理的理由:避免升级成重量级锁的性能开销。但实际上却是如此吗? **这个说法完全是错误的**。上面关于轻量级锁膨胀为重量级锁的源码已经剖析过了,当有线程发生竞争的时候,就会立刻执行锁膨胀流程,不会去自旋 10 次。而上面唯一有循环等待的过程只有当线程处于 **膨胀中 **时会去循环等待加锁线程成功 ###### 4)重量级锁自旋 当对象已经升级成为 **重量级锁** 后,其他线程再来争抢重量级锁失败后,不会直接进入 Moniter 对象中的 EntryList,也就是并不会直接进入 `BLOCKED` 堵塞状态,而会选择 **自旋** 的方式尝试等待获取 **重量级锁** 重量级锁自旋获取锁的逻辑在源码中为 `ObjectMonitor::TrySpin` 方法,其中自旋方式分为: + 固定自旋 1000 次(jdk 1.6 后被禁用) + 自适应自旋 这里源码过长,挑重要的部分片段来解释 **自适应自旋** 获取锁的逻辑,与自适应自旋相关参数 ```c // 自旋次数上限 int ObjectMonitor::Knob_SpinLimit = 5000; // 成功奖励 static int Knob_Bonus = 100; // 失败惩罚 static int Knob_Penalty = 200; // 自旋次数下限 static int Knob_Poverty = 1000; // 预自旋次数 static int Knob_PreSpin = 10; ``` 首先会进入 **预自旋** 流程,自旋 **11** 次 ```cpp for (ctr = Knob_PreSpin + 1; --ctr >= 0;) { // 进行自旋获取锁... } ``` + 自旋成功获取锁,直接返回 + 自旋失败,进入下面 **完整自旋** 进入 **完整自旋** 逻辑,当前自旋次数由全局变量 `_SpinDuration` (初始 5000 次)记录 + 自旋失败,下次 `-200` 次,最少减为 `0`(0 表示下次完成预自旋直接进入阻塞) + 自旋成功,下次 `+200` 次,至少加为 `1000`(1000 值是指从 0 开始累加的话,直接从 1000 开始累加) 当自旋全部失败后,失败线程就会进入重量级锁加锁流程 ###### 6)_cxq 队列 这里的加锁流程是指当前 Owner 已经被占用,其他线程的「挣扎过程」,当失败线程执行完自旋获取锁的流程后,会进入 `ObjectMonitor::EnterI` 方法 1. 线程使用 `TATAS (Test-And-Test-And-Set)` 尝试 **快速获取锁**,即使上面 5000 次自旋失败了,线程还会努力 **挣扎一次**,如果成功了,直接将 Owner 设置为当前线程,直接返回 ```c // Try the lock - TATAS if (TryLock (current) > 0) { // ... return; } ``` 2. 再次进入 **自适应自旋尝试**,因为上一次失败了次数 `-200`,如果上次自旋次数是 5000 次,那这次会自旋 4800 次 ```c if (TrySpin(current) > 0) { return; } ``` 3. 自旋还是失败后,会讲当前线程加入 `Monitor` 对象中的 **_cxq 栈** 中 ```c ObjectWaiter node(current); node.TState = ObjectWaiter::TS_CXQ; // CAS操作 将当前线程加入_cxq队列 for (;;) { node._next = nxt = _cxq; if (Atomic::cmpxchg(&_cxq, nxt, &node) == nxt) break; // 如果CAS失败,重试获取锁 if (TryLock(current) > 0) return; } ``` 创建 `ObjectWaiter` 节点表示当前线程,压入栈(**LIFO 后进先出**),并设置一个 **责任线程** ```c if (nxt == NULL && _EntryList == NULL) { Atomic::replace_if_null(&_Responsible, current); } ``` 如果没有责任线程且队列为空,设置当前线程为 **责任线程**。所有加入 _cxq 栈中的线程都会使用 `park()` 命令让自己 `阻塞` 4. 设置责任线程定期唤醒,非责任线程一直阻塞 ```c for (;;) { if (TryLock(current) > 0) break; // 责任线程使用定时park,非责任线程无限期park if (_Responsible == current) { current->_ParkEvent->park((jlong)recheckInterval); recheckInterval = MIN2(recheckInterval*8, MAX_RECHECK_INTERVAL); } else { current->_ParkEvent->park(); } // 唤醒后再次尝试获取锁 if (TryLock(current) > 0) break; } ``` 通过设置责任线程的机制,能够防止线程永久挂起,并且节省了性能的开销 ###### 7)解锁步骤 + 若当前线程为轻量级锁,但调用的是 `ObjectMonitor::exit` 重锁解锁方法,会先加重锁,再解锁 + 若为重入锁(重量级锁重入的时候会对 `Monitor` 中的 `_recursions` 累加),则计数 `-1` 直接返回 重量级锁正常解锁过程,首先讲 **Owner** 设置为 NULL,顺序执行下来会出现 4 中不同情况 1. **_cxq** 和 **EntryList** 全部为空,直接返回 ```c // 没有等待线程,直接返回 if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0) { return; } ``` 2. 若 EntryList 为空,_cxq 有数据,则将 _cxq 列表全部转移到 EntryList 中 ```c for (;;) { assert(w != NULL, "Invariant"); ObjectWaiter* u = Atomic::cmpxchg(&_cxq, w, (ObjectWaiter*)NULL); if (u == w) break; w = u; } ``` 通过使用 `Atomic::cmpxchg` 保证队列转移的原子性 ```c 转换前 _cxq (LIFO 单向链表): +---+ +---+ +---+ | C |--->| B |--->| A |---> NULL +---+ +---+ +---+ 转换步骤: 1. 断开 _cxq 链接 2. 遍历链表建立双向链接 转换后 _EntryList (FIFO 双向链表): +---+ +---+ +---+ NULL <--- | A | <->| B | <->| C | ---> NULL +---+ +---+ +---+ ^ | _EntryList 指向这里 ``` 3. 当前 **EntryList** 不为空,从队列获取 **头节点**,`unpark()` 唤醒,返回 ```c w = _EntryList; if (w != NULL) { ExitEpilog(current, w); return; } ``` `w` 是 **EntryList** 的头结点,这里调用 ExitPilog 方法来唤醒 `w` 线程,主要有四个步骤 ```c // 1. 设置继承者 _succ = Wakee->_thread; // 2. 释放锁 release_clear_owner(current); // 3. 内存屏障保证操作顺序 OrderAccess::fence(); // 4. 唤醒线程 Trigger->unpark(); ``` `OrderAccess::fence()` 是一个 **全内存屏障(full memory barrier)**,用于保证了 2 步骤一定先于 4 步骤执行,即肯定先释放锁再唤醒线程 最后这里用一张流程图来简单梳理 **重量级锁的解锁流程**  ###### 8)误区:锁释放,会从 EntryList 中随机挑选一个线程唤醒吗? 很多八股文都会说当 Owner 释放了重量级锁后,会随机从 EntryList 中挑选一个线程唤醒获得锁,这的是这样吗? **结论显然是错误的**,我们上面已经剖析过源码,竞争的线程会加入 _cxq 栈中的(LIFO,先进后出),然后再将 _cxq 内容加入 EntryList 队列中(FIFO,先进先出),因此唤醒顺序一定是确认的,下面的示例代码将证明这一点 ```java final Object lock = new Object(); new Thread(() -> { synchronized (lock) { try { System.out.println("t0 加锁成功"); System.in.read(); // 阻塞 t0 } catch (IOException e) { throw new RuntimeException(e); } } }, "t0").start(); TimeUnit.MICROSECONDS.sleep(100); new Thread(() -> { synchronized (lock) { System.out.println("t1 获取锁"); } }, "t1").start(); TimeUnit.MICROSECONDS.sleep(100); new Thread(() -> { synchronized (lock) { System.out.println("t2 获取锁"); } }, "t2").start(); TimeUnit.MICROSECONDS.sleep(100); new Thread(() -> { synchronized (lock) { System.out.println("t3 获取锁"); } }, "t3").start(); ``` `t0` 线程优先获取锁,并阻塞,让 t1,t2,t3 先后加锁,最后查看获取锁的先后情况  最后执行结果说明了顺序一定是如下图所示,EntryList 是一个 **先进先出** 的队列,t3 最先被唤醒,并非网上说的随机挑选一个线程获取锁  ###### 9)为什么有了 EntryList 还需要 _cxq 逻辑上,我们只需要一个队列就可以管理阻塞的线程,然后唤醒。但实现上,我们把这个队列分成了两部分:**_cxq** 和 **EntryList**,主要为了避免 **ABA 问题** 和 **惊群效应**,以及 **兼顾了吞度量和公平性** > 1. **ABA 问题** > **ABA 问题** 是并发编程中的一个经典问题,主要出现在特别是在使用 **CAS(Compare-And-Swap)** 操作时可能发生 对于共享变量 x = a, 执行 cas(a, b) 是原子性的将 x 的值交换成 b,并且会把交换的旧值返回 + 当前 x 为 a,则交换成功,x = b + 当前 x 被其他线程修改成了 c,则交换失败,本线程需要重试  当 CAS 操作结合 **栈** 时,就会引发 ABA 问题:,例如下面的示例  这个图已经基本反映了 **ABA 问题** 的核心逻辑:一个线程在用 CAS 操作时,只判断了“值有没有变化”,没考虑“值是否曾经被改过” + **_cxq** 是 LIFO(栈)结构,插入简单,不容易出现 ABA + 只有 JVM 自己在合适时机把 **_cxq** 中的线程“批量转移”到 **EntryList** → 所以可以集中加锁、避免高频竞 → 避免 ABA 问题  > 2. **惊群效应** > **惊群效应** 指的是当 **一个资源释放时,所有等待的线程都被唤醒并竞争**,但最终只有一个线程能成功获取锁,其他线程又得重新挂起,导致 **大量无意义的上下文切换和CPU争抢** 通过 `_cxq` + `EntryList` 每次只唤醒一个线程(EntryList) 减少竞争,提高吞吐量 > 3. **兼顾吞吐量和公平性** > `_cxq` 相当于缓存了最近竞争的线程,提高了整体的 **吞吐量** `EntryList` 采用 **FIFO** 顺序可以一定的 **公平性**,防止线程 **饿死**(长时间获取不到锁)
并发编程基础概念
前言:大家好!今天给大家讲一下并发编程相关概念,这些概念与任何语言无关,无论你使用任何语言都可能遇到这些问题。本篇文章会给每个知识点下一个定义,然后通过举例方式让小白也能轻松理解并发。相信你认真看完这篇文章一定会有所收获。 ## 并发和并行 下面我们将看到一个小故事,用来帮助我们理解并发和并行。故事中电视可以理解为CPU资源,小明看动画片可以理解为任务A,爸爸看新闻可以理解为任务B。 ### 并发 **定义:并发是指多个任务在逻辑上同时推进,但在单核CPU上是通过时间片轮转交替执行,多核CPU上可能同时执行多个任务。** 例子:有天小明放学后,匆匆地跑回家,因为他最喜欢的动画片即将开播。一进门,他就立刻打开电视看了起来。正当剧情达到最高峰突然插播了一条广告,小明只好失望地去做其他事情。这时,爸爸走过来将频道切换到新闻台观看。没过多久,新闻节目也进入了广告时段,小明便趁机换回动画频道继续观看。就这样,父子俩轮流切换频道,既让小明看上了动画片,也让爸爸看完了新闻。 **这里模拟单核CPU,** 执行过程如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/WYgqgZawX3YxLyIE.png" alt="" width="532px" /></p> ### 并行 **定义:并行表示任务真正意义上的在同一时刻同时运行,需要多核CPU的支持。** 例子:这天小明放学回家时特别开心,因为今天是他最喜欢的动画片大结局。然而,当他兴冲冲地想打开电视时,却发现爸爸正在看新闻。小明向爸爸要遥控器,却被爸爸训斥道:"作业写完了吗?没写完就回屋写作业去!"小明只好垂头丧气地回房间写作业。 <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/odXd84eTIUJa11VS.jpg" alt="" width="294px" /></p> 后来,为了能准时收看动画片,小明开始缠着爸爸再买一台电视机。经过他坚持不懈的软磨硬泡,爸爸终于同意添置了一台新电视。现在,即使动画片和新闻同时播出,小明可以看他的动画片,爸爸也能安心看新闻了。 执行过程如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/jhB4KXXAHwtYJXfs.png" alt="" width="533px" /></p> ## 线程的由来 在探讨线程的起源之前,我们有必要先来看看操作系统的发展历程。 ### 无操作系统 计算机最初的设计,只能接收用户简单的输入,每当输入一个指令,计算机就做出一个操作。程序执行过程中用户去做别的事情时,计算机就会处于等待状态,这样就会白白浪费计算机的计算能力。 ### 批处理操作系统 **无操作系统用户既要输入指令又要输入数据,有的时候还特别容易出错。人工输入的速度是远远慢于计算机运算的速度,** 那么有没有什么方法可以实现自动化读取执行的操作呢? 为了解决这些问题,批处理系统就诞生了,**它可以把多个任务写在磁带上,然后交由计算机去读取并逐个执行这些程序,** 计算机就可以不断读取指令来进行操作,这样可以大大缓解人机矛盾的问题。 ### 进程的诞生 批处理系统的诞生的确增加了计算机使用的效率,但是**批处理系统的任务只能串行执行,内存、CPU什么的全被一个任务给占着。** 假如现在有A和B两个任务,A任务先执行,当A有大量数据输入(即I/O操作,这里需要提一下I/O操作是非常耗时的),而任务B和CPU只能干等着。也就是说一个CPU同时只能一个一个的处理任务,即使有任务处于阻塞状态其他任务也只能等待。有没有什么方法当任务A有数据输入的时候先让任务B来执行呢? 此时进程就诞生了,**操作系统会把多个待执行的任务同时加载进计算机,为每个不同任务分配独立的内存空间。当任务A有I/O操作时,系统会暂时保存任务A的运行状态,并让出CPU资源供任务B执行。** 如果CPU采用时间片轮转的方式运行任务此时就达到了并发的效果,即使任务A没有I/O操作等时间片用完也会切换为任务B执行。 ### 线程的由来 进程虽然解决了多任务并发问题,但是**每个进程只能处理一个任务,当我有多个任务,只能让这些任务在进程中一个一个的执行。** 我们有没有方法让这些任务在进程中同时执行?这时,就提出了线程概念。 这里肯定有人会说我多开几个进程不一样能让这些任务并发执行吗?这样想确实没错,但是会有几个问题。 - 进程间的通信比较复杂,而线程间的通信比较简单,复杂的通信机制无疑增加我们开发成本。一般我们多个任务的耦合性比较高,需要大量数据交换,我们把这些任务放到一个进程执行,肯定比开多个进程效果要好。 - 进程间切换开销是大于线程的,进程创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,而线程只需要保存寄存器和栈信息。 **进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。** ## 同步和异步 **定义:同步和异步通常是一种形容方法、函数或接口的调用。同步调用一旦开始,调用方就必须等待方法返回结果,才能继续执行后续的行为。异步调用更像是一种消息传递,调用一旦开始,调用方不需要等待方法返回结果,可以继续执行后续操作。** 异步方法调用通常会在另一个线程执行,不会影响调用方的线程。如果想要得到异步方法的返回值,异步方法完成后通常会通知调用者。 例子:在主线程中有三个接口调用很耗时,第2和第3个接口需要依赖第1个接口的返回值,2和3接口没有相互依赖的关系,同时主线程需要等待2和3接口调用的返回结果,后面两个接口就可以使用异步调用(这里因为主线程需要等待2和3接口调用结果,所以可以只使用一个异步调用)。调用过程如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/EVUrWbFP9Ia6Q4tU.png" alt="" width="563px" /></p> ## 临界区 **定义:用来形容公共资源或者共享数据,可以被多个线程使用。但是每一次只能由一个线程进入临界区使用共享资源,一旦临界区资源被占用,其他线程想要使用这个资源就必须等待。** 例子:比如打印机,每次只能有一个线程使用,其他线程想要使用该打印机就必须等待第一个线程完成打印任务。 ## 阻塞和非阻塞 **阻塞和非阻塞通常来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区外等待。等待会导致线程挂起,这种情况就是阻塞。** 如果占用资源的线程一直不释放资源,其它所有阻塞在这个临界区上的线程都不能工作。 非阻塞与阻塞相反,它指的是线程与线程之间互不影响,所有线程都会不断向前执行。 ## 死锁、饥饿和活锁 ### 死锁 **定义:死锁是指两个或两个以上的进程/线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。** 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称/线程为死锁进程/线程。 例子:比如有两个线程A和B,另外有两个共享资源a和b,每个共享资源在同一时刻只能有一个线程使用。 - 当线程A占用资源a,线程B占用资源b; - 线程A等待共享资源b释放才能继续执行,线程B等待共享资源a释放才能继续执行; - 线程想要释放占用资源需要先获得,每个线程等待对方对方线程释放资源,从而形成一个环路; 如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/lsr98r3RzrQ0kfwc.png" alt="" width="569px" /></p> ### 饥饿 **定义:饥饿是指某一个或者多个线程因为种种原因无法获得所需的资源,导致一直无法执行。比如优先级太低,而优先级高的线程不断抢占资源,导致优先级低的无法正常执行。** 例子:在自然界中,母鸟给雏鸟喂食的时候很容易出现这种情况。由于雏鸟很多,食物有限,雏鸟之间的食物竞争可能非常厉害,经常抢不到食物的雏鸟有可能会被饿死。 ### 活锁 **定义:活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行。** 例子:比如在转弯时,迎面突然来了一个人,于是,你很礼貌的靠左走,避让对方。同时,对方也非常礼貌的靠右走,希望避让你。结果,你们又撞上了。于是,你们都意识到了问题,希望尽快避让对方,你立即向右走,同时他向左边走,结果又撞上了! ## 线程不安全的原因 ### 原子性 **定义:原子性指的是一个操作是不可中断的,要么全部执行完成,要么都不执行。** 例子: 在业务上,比如某用户购买了1件商品,这时需要对库存减1操作。首先要把这个操作分解为多步: - 查询数据库; - 对库存数量减1; - 把结果写回数据库; 查询库存还剩10件商品,然后减1变为9件商品,最后写回数据库。乍一看好像没问题,但是如果有**两个用户同时购买该商品,他们查询库存结果查到的都等于10件商品,然后他们同时对商品减1算出的结果都是9,最后写回结果发现商品只扣了1件。事实上商品被用户购买了两次,** 如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/7OwQQFiMiaWVY19h.webp" alt="" width="100%" /></p> 此时减库存这个操作应该被设计成原子性的就没有上述问题,比如加锁和事务机制,这样就能保证每次只有一个线程对库存进行修改。如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/xKD1FmBRbeNfUYto.webp" alt="" width="100%" /></p> 在程序中定义一个整数类型变量a = 1。比如对a+=1操作,在多线程环境下也会出现上述减库存的问题,在cpu中有很多指令,每个指令是有原子性的,但是a+=1操作在计算机中是被分解为多条指令: - 取数据a = 1; - 加1操作; - 写回; 可以看到这里操作和上面减库存非常相似,这里就不作过多赘述。如果想要线程安全的操作,可以使用**CAS操作或者加锁。** ### 可见性 **定义:一个线程对共享变量的修改,其他线程能够立即看到。** 栗子:如果在CPU1和CPU2上各运行了一个线程,它们共享变量t,由于编译器优化或者硬件优化的缘故,**在CPU1上的线程将变量t进行了优化,然后将其缓存在cache中或者寄存器里。这时如果CPU2上的某个线程修改了变量t的实际值,那么CPU1上的线程可能无法知道这个改动,依然会读取cache中或者寄存器里的数据原始值。** ### 有序性 **定义:由于指令重排序问题,代码执行顺序未必就是编写代码时候的顺序。** 为什么要使用指令重排序?**CPU出于性能的考虑,如何高效利用高速缓存(使用空间局部性和时间局部性原理提高缓存命中率),如何高效利用流水线技术。** 说白了就是我们写的代码执行的性能不高,重排序后能更高效的执行。**指令重排序可以保证单线程语义一致,但是没有义务保证多线程的语义也一致,因为最开始指令重排序并没有考虑多线程情况。** 例子:如何高效利用流水线,**在CPU中一条指令的执行可以分为取指、译码、执行、访存和写回这五步,** 这些操作在CPU中都是独立的模块(我们可以下想象生活中的流水线,比如一条流水线上有5道工序,每条指令可以想象成每个产品需要顺序的经过这5道工序加工)LW表示取数据到寄存器中,ADD表示把寄存器数据相加(SUB表示寄存器数据相减),SW表示把寄存器中的数据保存存储器中。如图展示了a = b + c和d = e - f在没有使用指令重排序执行过程。**可以看到在程序执行过程中有很多断层红叉,ADD执行前需要等待R2的数据准备好,所以会等待一个时钟周期,这样就会触发连锁反应后面的执行都会跳过一个时钟周期。** 减法同理也会这样。执行过程如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/Da4bVQNHDdrSgww6.webp" alt="" width="100%" /></p> 如果使用指令重排序就会避免这样的问题,如下图,可以看到**我们只是改变一下执行的顺序就可以节省两个时钟周期,而且在重排序之后不会影响执行的结果。** 执行过程如下图: <p align="center"><img src="https://pic.code-nav.cn/post_picture/1639857653502976002/n9NvSDYgpJ2w9MuL.webp" alt="" width="100%" /></p> ## 参考文献 [进程与线程的区别](https://blog.csdn.net/ThinkWon/article/details/102021274) 《Java高并发程序设计实战》
线程名称解读:Thread[pool-1-thread-1,5,main]
## 背景 今天学习的时候用到了线程池,然后在观察输出结果的时候,发现线程名字很长,自己不太清楚都是什么意思,于是去查询了下,现在此做下记录。 ## 问题 线程名:Thread[pool-1-thread-1,5,main] ## 解读 ``` pool-1:第一个线程池 thred-1:第一个线程 5:优先级为5 main:属于main线程组 ``` 在 Java 中,线程名称通常以 `[ThreadName, Priority, ThreadGroup]` 的格式显示。 `Thread[pool-1-thread-1,5,main]` 是一个典型的线程名称格式,其中每个部分都有特定的含义。我们逐一解释这些部分: ### 1. **`ThreadName`:`pool-1-thread-1`** - **`pool-1`**:表示这是一个线程池中的线程,`pool-1` 是线程池的标识符。通常,Java 的 `ExecutorService`(如 `ThreadPoolExecutor`)会为每个线程池生成一个唯一的标识符。`pool-1` 表示这是第一个创建的线程池。 - **`thread-1`**:表示这是该线程池中的第一个线程。线程池中的线程通常会按顺序编号,`thread-1` 表示这是第一个线程,`thread-2` 表示第二个线程,依此类推。 因此,`pool-1-thread-1` 表示这是第一个线程池中的第一个线程。 ### 2. **`Priority`:`5`** - **`5`**:表示线程的优先级。Java 线程的优先级范围是从 1 到 10,默认优先级是 5。优先级越高,线程被调度执行的可能性越大,但请注意,优先级并不保证线程一定会比其他优先级低的线程先执行,它只是一个提示给线程调度器的建议。 - **1**:最低优先级 - **5**:默认优先级 - **10**:最高优先级 在这个例子中,`5` 表示该线程使用的是默认优先级。 ### 3. **`ThreadGroup`:`main`** - **`main`**:表示该线程所属的线程组。线程组是 Java 中用于组织和管理线程的一种机制。每个线程都属于一个线程组,默认情况下,所有用户创建的线程都会属于 `main` 线程组,而 `main` 线程组是由 JVM 自动创建的。 - **`main`**:这是主线程组,通常包含应用程序启动时创建的主线程以及通过 `ExecutorService` 创建的线程池中的线程。 ### 总结 `Thread[pool-1-thread-1,5,main]` 的含义如下: - **`pool-1-thread-1`**:这是第一个线程池中的第一个线程。 - **`5`**:该线程的优先级为 5,即默认优先级。 - **`main`**:该线程属于 `main` 线程组,通常是应用程序的主线程组。 ### 示例解释 假设你使用了 `Executors.newFixedThreadPool(10)` 创建了一个固定大小为 10 的线程池,并提交了一些任务。当你看到 `Thread[pool-1-thread-1,5,main]` 时,这意味着: - 这是第一个线程池(`pool-1`)中的第一个线程(`thread-1`)。 - 该线程的优先级是 5(默认优先级)。 - 该线程属于 `main` 线程组。 如果你继续提交任务,可能会看到类似 `Thread[pool-1-thread-2,5,main]`、`Thread[pool-1-thread-3,5,main]` 等线程名称,表示线程池中的其他线程正在执行任务。 ### 扩展:线程池的工作原理 当你使用 `ExecutorService` 创建线程池时,线程池会根据配置创建一定数量的线程,并将任务分配给这些线程执行。线程池中的线程会反复从任务队列中获取任务并执行,直到任务队列为空或线程池关闭。 - **`newFixedThreadPool(int nThreads)`**:创建一个固定大小的线程池,线程池中的线程数量始终保持不变。 - **`newCachedThreadPool()`**:创建一个可以根据需要创建新线程的线程池,适用于任务数量不确定且任务执行时间较短的场景。 - **`newSingleThreadExecutor()`**:创建一个单线程的线程池,适用于需要顺序执行任务的场景。 - **`newScheduledThreadPool(int corePoolSize)`**:创建一个支持定时和周期性任务执行的线程池。 ### 结论 `Thread[pool-1-thread-1,5,main]` 是一个典型的线程名称格式,它提供了关于线程池、线程优先级和线程组的信息。通过理解这些信息,你可以更好地调试和优化多线程程序。
【鸿蒙实战开发】ArkTS多线程的多线程系列(二):基于Sendable共享对象实现跨线程通信及UI状态刷新
主线程启动文件上传、下载、持久化等耗时操作时,往往都需要通过子线程完成相关操作,为了让UI体验更好,文件下载或者报错的过程中往往都需要提供进度条提示以提升用户感知。同时对于文件列表类型的上传、下载往往还会提供类似暂停\继续的能力,类似如下场景: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ysipOxVuomiQVQ8i.webp" alt="" width="359px" /> 本案例将使用Sendable共享对象实现以下两个主要功能: 1、子线程的计算结果刷新UI(进度通知、下载结果通知)。 2、主线程控制子线程业务逻辑(暂停下载、接续下载)。 **方案介绍** 1. 通过Sendable构建可跨线程共享的对象DownloadVideoInfo; 2. 主线程通过构建new taskpool.Task(downloadVideo, this.dvi)将DownloadVideoInfo共享对象同步给子线程(this.dvi是DownloadVideoInfo对象实例,downloadVideo是通过@Concurrent修饰的多线程任务); 3. 当点击启动时,通过taskpool.execute()方法启动子线程; 4. 主线程通过的onReceiveData()注册下载进度更新回调,已实现下载进度的UI刷新。 * 子线程在共享对象中更新下载进度,并通过taskpool.Task.sendData("UPDATE_DOWNLOAD_PROGRESS")通知主线程从共享对象中更新下载进度。 * 主线程通过this.downloadTask.onReceiveData()方法注册子线程发送来的消息回调,此处通过监听"UPDATE_DOWNLOAD_PROGRESS"消息,实现UI进度条更新。 5. 下载命令控制,暂停/继续下载时需要通过主线程想子线程发送消息,由于taskpool不支持该能力,因此通过共享对象实现此能力。 **核心代码** Step1:构建@Sendable对象DownloadVideoInfo、VideoInfo、ActorInfo。此处需要注意,**DownloadCommandEnum枚举需要使用const进行修饰**。 * DownloadVideoInfo共享类中,使用了基础类型和其他共享对象作为属性,并携带成员方法。 ``` // src\main\ets\model\DownloadVideoInfo.ets import { DownloadStateEnum } from './DownloadStateEnum'; import { VideoInfo } from './VideoInfo'; @Sendable export class DownloadVideoInfo { videoUrl: string = "URL"; downloadProgress: number = 0; command: DownloadCommandEnum = DownloadCommandEnum.CONTINUE_DOWNLOADING; videoInfo : VideoInfo = new VideoInfo(); constructor(videoUrl : string) { this.videoUrl = videoUrl; } getDownloadProgress(): number { return this.downloadProgress; } increaseDownloadProgress20Percent() { this.downloadProgress += 20; } } ``` * 由于TaskPool没有主线程给子线程发送消息的接口,因此此处定义DownloadCommandEnum用来实现主线给子线程下发具体的Download命令(暂停下载、继续下载等)。 枚举必须使用const进行修饰 ``` // src\main\ets\model\DownloadCommandEnum.ets export const enum DownloadCommandEnum { STOP_DOWNLOADING, CONTINUE_DOWNLOADING, CANCEL_DOWNLOADING } ``` * VideoInfo共享类中的Array容器来自于@arkts.collections,容器类中的ActorInfo也必须是Sendable类型 ``` // src\main\ets\model\VideoInfo.ets import collections from '@arkts.collections'; @Sendable export class VideoInfo { long: number = 0; actors : collections.Array<ActorInfo> = new collections.Array<ActorInfo>() } ActorInfo类必须是Sendable类型 // src\main\ets\model\ActorInfo.ets import { Sex } from './SexEnum'; @Sendable export class ActorInfo { name : string = ""; sex : Sex = Sex.MALE; ... } ``` Step2:将共享对象this.dvi作为Task的构造函数传入构造函数new taskpool.Task(downloadVideo, this.dvi),这样父、子线程就都将持有DownloadVideoInfo对象实例的引用。downloadData为组件入参。 ``` @Concurrent function downloadVideo(dvi : DownloadVideoInfo) { ... } @Component struct DownloadComponent { @Require downloadData !: DownloadData; private dvi !: DownloadVideoInfo private downloadTask !: taskpool.Task aboutToAppear(): void { this.dvi = new DownloadVideoInfo(this.downloadData.downloadUrl); this.downloadTask = new taskpool.Task(downloadVideo, this.dvi) } ... } ``` Step3:点击启动按钮时,通过 taskpool.execute(this.downloadTask)启动子线程,并结合then方法,在子线程任务完成后,将状态UI显示内容置为“完成” ``` Button(this.buttonValue, { type: ButtonType.Normal, stateEffect: true, buttonStyle: ButtonStyleMode.TEXTUAL }) .onClick( ent => { if (this.buttonValue === '完成') { return; } else { this.start = !this.start; // 通过点击按钮,实现对暂停下载和继续下载的命令控制 this.buttonValue = this.start ? '暂停' : '继续' if (this.start) { console.info("==== start task") // 启动下载或者接续下载,整个任务始终使用一个共享变量。 this.dvi.command = DownloadCommandEnum.CONTINUE_DOWNLOADING; this.task = taskpool.execute(this.downloadTask); this.task.then(() => { if(this.dvi.downloadProgress == 100) { this.buttonValue = '完成' this.downloadData.long = this.dvi.videoInfo.long; this.showDetail = true; } }) } else { // 暂停下载 this.dvi.command = DownloadCommandEnum.STOP_DOWNLOADING; } } }) } ``` Step4:当子线程需要更新下载进度条数据,将下载进度设置到DownloadVideoInfo共享对象中,并给主线程发送"UPDATE_DOWNLOAD_PROGRESS"事件,主线程接收到事件后从共享对象中获取下载进度,并刷新UI。 4.1 子线程:模拟每间隔一秒将共享对象中的下载进度downloadProgress增加20,并通过sendDate接口,将更新下载进度消息("UPDATE_DOWNLOAD_PROGRESS")发送给主线程 ``` @Concurrent export function downloadVideo(dvi : DownloadVideoInfo) { console.info("==== execute task") let start : number; while (dvi.downloadProgress < 100) { start = Date.now(); while (Date.now() - start < 1000) { // 模拟等待1秒 } console.info("==== sendData from task") dvi.increaseDownloadProgress20Percent(); taskpool.Task.sendData("UPDATE_DOWNLOAD_PROGRESS") // 线程收到停止下载通知 if (dvi.command == DownloadCommandEnum.STOP_DOWNLOADING) { return; } } dvi.videoInfo.long = 120 } ``` 4.2 主线程通过onReceiveData接收子线程消息,当判断消息是需要更新下载进度("UPDATE_DOWNLOAD_PROGRESS")时,将共享对象中的下载进度赋值给状态变量,更新UI。 ``` struct DownloadComponent { @Require downloadData !: DownloadData; ... aboutToAppear(): void { ... this.downloadTask.onReceiveData((msg : string) => { switch (msg){ case "UPDATE_DOWNLOAD_PROGRESS" : this.downloadData.downloadProgress = this.dvi.getDownloadProgress(); break; default : console.error("==== switch default"); } }) } ... ``` Step5:下载过程中若主线程要给子线程发送消息(比如停止下载、继续下载)在taskpool中可以通过共享对象变量实现。当需要暂停或者继续时,将命令下发给共享对象DownloadVideoInfo.command属性,子线程中在业务合适的时机对参数就行判断,若条件满足则执行相关逻辑即可。主线程: ``` this.dvi.command = DownloadCommandEnum.STOP_DOWNLOADING; ``` 子线程:此处子线程感知共享对象的下载命令变为STOP_DOWNLOADING时,结束子线程,在实际业务中需要根据业务诉求设置状态判断点以及业务处理逻辑。 ``` // 线程收到停止下载通知 if (dvi.command == DownloadCommandEnum.STOP_DOWNLOADING) { return; } ``` 当点击接续下载是,只需要在主线程再次启动子线程进行下载即可,前一个线程的下载状态会保存在共享对象中。此处需要将command的状态修改为接续下载CONTINUE_DOWNLOADING,否则子线程会退出。 ``` if (this.start) { this.dvi.command = DownloadCommandEnum.CONTINUE_DOWNLOADING; this.task = taskpool.execute(this.downloadTask); this.task.then(() => { if (this.dvi.downloadProgress == 100) { this.buttonValue = '完成' this.downloadData.long = this.dvi.videoInfo.long; this.showDetail = true; } } ``` **补充的知识** 此处的状态变量是通过@ObservedV2和@Trace进行标注的,因此在代码中看不到@State。@Request表示downloadData参数必须通过父组件赋值。DownloadData对象申明如下。当long和downloadProgress属性发送变化时,UI会跟随刷新,如前面代码提到的this.downloadData.long = this.dvi.videoInfo.long;和this.downloadData.downloadProgress = this.dvi.getDownloadProgress(); ``` @ObservedV2 export class DownloadData { videoName !: string; downloadUrl !:string; @Trace long !:number; @Trace downloadProgress : number = 0; constructor(videoName : string, downloadUrl : string) { this.videoName = videoName; this.downloadUrl = downloadUrl; } } ``` ## 写在最后 **如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:** * 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力; * 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识; * **想要获取更多完整鸿蒙最新学习知识点,可关注B站:码牛课堂鸿蒙开发;**
【鸿蒙实战开发】ArkTS多线程的多线程系列(一):ArkTS多线能力入门
HarmonyOS应用的UI操作必须在主线程执行(如修改UI控件,更新视图这些操作必须在UI线程中进行),如果主线程出现阻塞,那么UI界面就会出现明显的卡顿。因此为了解决此类问题,我们需要将一些耗时的操作例如加载网络数据、查询本地文件、数据等放到子线程中,以提升应用的响应速度和性能。 **多线程能力介绍** **进程与线程** 在单线程中执行的代码都是串行的,即按顺序执行,直到执行完成后,程序才会退出。当程序需要执行多个任务时,每个任务必须等待前一个任务执行完成后才能继续执行,这使得程序的性能非常低下。多线程技术通过使用多个线程来充分利用CPU资源,同时执行多个任务,从而提高程序执行的效率。每个线程都是相互独立的,并能够单独执行、暂停、继续和停止。进程(Process):是操作系统进行资源分配的最小单元。线程(Thread):是操作系统进行运算调度的最小单元,它被包含在进程之中,是进程中的实际运作单位。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/bseMIaJVriK74pW2.webp" alt="" width="100%" /> **多线程的使用场景** 多线程的应用场景包括但不限于以下场景: 1. CPU密集型:数据处理、图像处理。当需要大量的数据处理时,可以使用多线程,以提高处理效率 2. I/O密集型:文件读写、网络请求。当需要发起大量的I/O请求时,可以使用多线程,以避免卡主线 3. 后台任务:自动化作业处理,比如需要定期完成特定任务。 **ArkTS的多线程解决方案** 在HarmonyOS的ArkTS侧为多线程提供了两种方式:TaskPool和Worker,应用可以结合自身业务诉求,选择对应的实现方案。 **TaskPool简介** 任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且开发者无需关心线程实例的生命周期。TaskPool提供了多种不同的任务能力: | 能力 | 任务构建 | 任务执行 | 场景描述 | | --- | --- | --- | --- | | 普通任务 | new taskpool.Task() | taskpool.execute() | 立即执行的短时任务,耗时不能超过3分钟。 | | 延时任务 | new taskpool.Task() | taskpool.executeDelayed() | 为了不影响应用启动的性能,一些不影响启动的初始化类任务往往期望放在延时任务中执行,如拉取线上的配置信息等。 | | 长时任务 | new taskpool.LongTask() | taskpool.execute() | 希望长时运行的任务一直保持执行,已为其他模块提供特定的服务,比如日志埋点,后台长链接保活等。 | | 串行任务 | new taskpool.SequenceRunner(),new taskpool.Task() | SequenceRunner. execute() | 用于执行一组需要串行执行的任务 | | 依赖任务 | task1.addDependency(task2), task1.removeDependency(task2) | taskpool.execute() | 任务之间存在先后依赖关系 | | 任务的优先级设置 | 待支持 | 待支持 | 在应用运行时,期望可以给设置空闲时任务,当应用处于闲时(比如冷启动完成后停留在首页)做一些相应的预加载动作(如预加载其他页面),从而提升应用整体性能。 | **TaskPool注意事项** * 实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。 * 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用,具体请见多线程安全注意事项。 * 序列化传输的数据量大小限制为16MB。 **推荐使用场景** TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容) * 需要设置优先级的任务。 * 大量或者调度点较分散的任务。 * 需要频繁取消的任务。 **TaskPool线程池扩容策略** 总体算法TaskPool需要的工作线程数由任务平均执行时间和当前任务数共同决定。任务池会根据过往的执行数据计算出一个预期线程数,但是各个任务的执行时间并不能准确衡量TaskPool当前的负载,因此需要通过任务数来反映。 扩容机制TaskWorker线程创建有一定耗时。为了优化启动阶段的性能和加快任务执行,TaskPool默认创建和预留了一个线程。结合JS的async/await机制和TaskPool的线程复用特性,当任务较少时,一个线程能够正常处理所有任务,此时并不一定会触发扩容机制。当首次执行且任务执行耗时较长的时候,上述算法的平均执行时间并不能立刻得出,因此有新的任务时,将会额外新建两个线程,避免线程池拥塞。当任务执行完成后,仍会采用上述算法。 正常流程下,每当开发者向任务池中抛任务时,都会触发一次扩容检测。扩容检测首先会判断当前的空闲线程数是否大于任务数,若大于,则说明线程池存在空闲线程,不需要扩容即可执行完新的任务。否则通过算法判断需要的线程数进而创建线程到指定数目。 缩容机制当任务耗时且较多时,TaskPool将会新建多个TaskWorker线程。但在空闲时仍保留这么多线程将会导致资源浪费和内存无法下降。因此TaskPool使用了定时器,定时检测TaskPool的负载,负载计算方式仍然采用上述算法。但是考虑到频繁创建和销毁带来的开销,缩容并不会立刻缩到计算出的数目,而是整体沿用了急涨缓停的思想,即立刻扩容到指定数目,但在缩容阶段采用阶梯式下降的方式。空闲时每个检测阶段会尝试释放2个线程(当前策略)。由于涉及到资源释放,需要确保不会因为错误的释放带来野指针等crash行为,缩容阶段仍会去检测当前空闲线程是否可以释放。仅有满足条件的线程才能够被释放。 **Worker简介** Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本操作耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。 **Worker注意事项** * Worker的创建和销毁耗费性能,建议开发者合理管理已创建的Worker并重复使用。 * Worker存在数量限制,支持最多同时存在64个Worker。当Worker数量或者内存超出限制时,会抛出相应错误。 **推荐的使用场景** * 常驻后台的线程任务 **TaskPool与Worker对比** 本节将从实现特点和适用场景两个方面来进行TaskPool与Worker的比较。 | 实现 | TaskPool | Worker | | --- | --- | ---| | 内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 | | 参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移、SharedArrayBuffer共享、sendable共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 | | 参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 | | 方法调用 | 直接将方法传入调用。 | 在Worker线程中进行消息解析并调用对应方法。 | | 返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 | | 生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 | | 任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。 | | 任务执行时长上限 | 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。 | 无限制。 | | 设置任务的优先级 | 支持配置任务优先级。 | 不支持。 | | 执行任务的取消 | 支持取消已经发起的任务。 | 不支持。 | | 线程复用 | 支持。 | 不支持。 | | 任务延时执行 | 支持。 | 不支持。 | | 设置任务依赖关系 | 支持。 | 不支持。 | | 串行队列 | 支持。 | 不支持。 | | 任务组 | 支持。 | 不支持。 | **多线程开发常见场景&解决方案** **创建&停止线程** **TaskPool线程的创建&停止** 通过taskpool.Task或者taskpool.LongTask构建TaskPool线程池任务,构造方法如let task: taskpool.Task = new taskpool.Task(taskName, taskFun, args);。 * taskName为任务名称,此任务名无法在子线程中获取,如果子线程需要使用任务名,则需要将任务名作为参数传递给子线程。 * taskFun为要执行的逻辑函数,该函数必须使用@Concurrent装饰器装饰。 * args为任务执行函数的入参。默认值为undefined。 ``` import taskpool from '@ohos.taskpool'; @Concurrent function add(num1: number, num2: number): number { return num1 + num2; } async function ConcurrentFunc(): Promise<void> { try { let task: taskpool.Task = new taskpool.Task(add, 1, 2); console.info("taskpool res is: " + await taskpool.execute(task)); } catch (e) { console.error("taskpool execute error is: " + e); } } @Entry @Component struct Index { build() { Row() { Column() { Text('Do Something in Taskpool') .onClick(() => { ConcurrentFunc(); }) } } } } ``` TaskPool任务销毁:对于长时任务(LongTask),除了通过启动外,开发者还需要在任务完成后调用terminateTask方法终止此任务,系统不会主动回收此任务。 ``` let longTask: taskpool.LongTask = new taskpool.LongTask(longTask, 1000); // 1000: sleep time taskpool.execute(longTask).then((res: Object)=>{ taskpool.terminateTask(longTask); }); ``` **Worker线程的创建&停止** 通过new worker.ThreadWorker('entry/ets/workers/MyWorker.ets')的方式加载Worker。HAP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHAP: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets'); ``` HSP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHSP: worker.ThreadWorker = new worker.ThreadWorker('hsp/ets/workers/worker.ets'); ``` HAR中的Worker加载,路径规则为:@{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHAR: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets'); ``` Worker线程销毁 Worker的生命周期需要开发者自行进行管理,也就是说创建出Worker线程后,开发者需要管理Worker线程实例,因此当不需要再使用该线程后,需要显示调用Worker的terminate()方法,将Worker线程实例销毁掉。 ``` const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ets"); workerInstance.terminate(); ``` **线程间通信数据类型说明** 此处只做简单说明,具体规格参考相关文档 **Sendable类型共享数据类型** Sendable协议定义了ArkTS的可共享对象体系及其规格约束。符合Sendable协议的数据(以下简称 Sendable 数据)可以在ArkTS并发实例间传递。默认情况下,Sendable数据在ArkTS并发实例(主线程、TaskPool、Worker)间通过引用传递。同时,ArkTS也支持Sendable数据在ArkTS并发实例间的拷贝传。 Sendable对象可以支持的属性类型: 1. 属性限制:包含基础类型(boolean, number,string,bigint,null,undefined)、其他共享对象、枚举、@arkts.collections下的容器。 2. 方法限制:可以传递共享对象中的方法。 当前Sendable对象使用限制较多,如: 1. Sendable class不能使用除了@Sendable的其他装饰器 2. 不能使用字面量初始化Sendable类型 3. 非Sendable类型不可以as成Sendable类型 更多的规则可参考 Sendable使用规则 Sendable对象的使用: * taskpool中使用Sendable对象构建Task时传递参数是,通过args参数将Sendable对象的引用传递给子线程new taskpool.Task(taskName, taskFun, args)。 * worker中使用Sendable对象通过workerPort.postMessageWithSharedSendable()方法,将Sendable对象的引用传递给子线程。 **普通数据类型** 普通对象传输采用标准的结构化克隆算法(Structured Clone)进行序列化,此算法可以通过递归的方式拷贝传输对象,相较于其他序列化的算法,支持的对象类型更加丰富。 序列化支持的类型包括:除Symbol之外的基础类型、Date、String、RegExp、Array、Map、Set、Object(仅限简单对象,比如通过“{}”或者“new Object”创建,普通对象仅支持传递属性,不支持传递其原型及方法)、ArrayBuffer、TypedArray。 **可转移对象** 可转移对象(Transferable object)传输采用地址转移进行序列化,不需要内容拷贝,会将ArrayBuffer的所有权转移给接收该ArrayBuffer的线程,转移后该ArrayBuffer在发送它的线程中变为不可用,不允许再访问。 **可共享对象** 共享对象SharedArrayBuffer,拥有固定长度,可以存储任何类型的数据,包括数字、字符串等。 共享对象传输指SharedArrayBuffer支持在多线程之间传递,传递之后的SharedArrayBuffer对象和原始的SharedArrayBuffer对象可以指向同一块内存,进而达到内存共享的目的。 SharedArrayBuffer对象存储的数据在同时被修改时,需要通过原子操作保证其同步性,即下个操作开始之前务必需要等到上个操作已经结束。 **Native绑定对象** 当前支持序列化传输的Native绑定对象主要包含:Context、RemoteObject和PixelMap。
【鸿蒙实战开发】基于Taskpool的多线程操作
## **场景描述** **场景一**:周期性任务处理,业务通过taskpool周期性处理业务。 **场景二**:延迟业务处理,业务一段时间后,通过taskpool处理业务。 **场景三**:串行业务处理,业务开展过程中,需要处理一系列的事务,事务处理过程中,存在先后次序。 **场景四**:业务的处理存在紧急优先次序,支持设置taskpool优先级处理。 **场景五**:ArkTS与Native协作开展业务,在ArkTS层触发业务,通过NAPI接口,传递到Native C++层,作业务管理等处理。 ## **方案描述** ### **场景一:周期性任务** 方案: 1)定时器判断周期性事务执行。 2)Taskpool来处理任务执行。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Oy7PWV7vDzLNJfyH.webp" alt="" width="100%" /> 核心代码: ``` @Concurrent function ServiceHandle(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 0; return result; } let count = 0; function TimerOutHandle(pars:number) { count++; let task: taskpool.Task = new taskpool.Task(ServiceHandle, pars); hilog.info(0x0000, 'testTag', 'Timer handle count :%{public}d,pars %{public}d', count, pars); taskpool.execute(task, taskpool.Priority.HIGH).then((res: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res); if (g_callback != null) { g_callback(count); } }); } let timerId = -1; export function TimerTest() { count = 0; let value = 88; timerId = setInterval(TimerOutHandle, 3000, value); } ``` 定时器每3秒超时一次,进入TimerOutHandle函数处理,TimerOutHandle函数体中,通过taskpool创建异步并发任务执行业务。 **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/oWYY1Lceok77VNxd.webp" alt="" width="100%" /> 界面上,每超时一次,会呈现运行次数: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/koRLKLR306s6e9cy.webp" alt="" width="417px" /> ### **场景二:延迟任务** 方案: 1)通过setTimeout来延迟处理。 2) 通过executeDelayed来延迟处理。 核心代码: 1)setTimeout的处理如下: ``` @Concurrent function ServiceHandle(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 0; return result; } let count = 0; function TimerOutHandle(pars:number) { count++; let task: taskpool.Task = new taskpool.Task(ServiceHandle, pars); hilog.info(0x0000, 'testTag', 'Timer handle count :%{public}d,pars %{public}d', count, pars); taskpool.execute(task, taskpool.Priority.HIGH).then((res: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res); if (g_callback != null) { g_callback(count); } }); } export function OneTimerCallTest() { count = 0; if (g_callback != null) { g_callback(count); } let value = 99; hilog.info(0x0000, 'testTag', 'start setTimeout'); timerId = setTimeout(TimerOutHandle, 3000, value); } ``` 定时器3秒超时(仅仅执行一次)后,就会进入TimerOutHandle函数处理,TimerOutHandle函数体中,通过taskpool创建异步并发任务执行业务。 2)executeDelayed来延迟 ``` @Concurrent function TaskDelayServiceHandle(pars: number): number { let t: number = Date.now(); hilog.info(0x0000, 'testTag', 'enter TaskDelayServiceHandle, timer is :%{public}d', t); // 业务处理过程,并将结果返回 let result = 0; return result; } export function TaskPoolDelayTest() { count = 0; if (g_callback != null) { g_callback(count); } let value = 100; let t: number = Date.now(); hilog.info(0x0000, 'testTag', 'taskpool start time is :%{public}d', t); let task: taskpool.Task = new taskpool.Task(TaskDelayServiceHandle, value); taskpool.executeDelayed(3000, task).then(() => { count++; let t: number = Date.now(); hilog.info(0x0000, 'testTag', 'taskpool execute success, time is :%{public}d', t); if (g_callback != null) { g_callback(count); } }).catch((e: BusinessError) => { console.error(`taskpool execute: Code: ${e.code}, message: ${e.message}`); }) } ``` 调用executeDelayed函数3秒后,会进入TaskDelayServiceHandle函数执行,返回返回后,会进入executeDelayed后面的then的函数体中执行。 **运行结果:** 1)使用setTimeout运行结果 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Fc84EA4Dl4BwLvdc.webp" alt="" width="100%" /> 2)使用executeDelayed运行结果 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/RezpwzM8Zh2GkzFZ.webp" alt="" width="100%" /> ### **场景三:串行任务** 方案: 1)最简单的方案就是后面任务执行时,根据前面任务的执行结果来处理。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/4qW5I7ZwqqEUKlNa.webp" alt="" width="286px" /> 2)后面任务的执行,依赖另一个任务的一些处理结果后,继续执行。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/MJyjjvOH2JYhD6It.webp" alt="" width="459px" /> 核心代码: 1)通过业务逻辑的结果来处理 ``` @Concurrent function ServiceHandle1(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle1:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 0; return result; } @Concurrent function ServiceHandle2(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle2:%{public}d', pars); // 业务处理过程,并将结果返回 let result = 1; return result; } export function SyncHandle() { let task1: taskpool.Task = new taskpool.Task(ServiceHandle1, 1); hilog.info(0x0000, 'testTag', 'sync handle'); taskpool.execute(task1, taskpool.Priority.HIGH).then((res1: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res1); if (g_callback != null) { g_callback('task1 finish.'); } if ((res1 as Number) == 0) { let task2: taskpool.Task = new taskpool.Task(ServiceHandle2, 2); taskpool.execute(task2, taskpool.Priority.HIGH).then((res2: object) => { hilog.info(0x0000, 'testTag', 'ServiceHandle2 result :%{public}d', res2); if (g_callback != null) { g_callback('task2 finish.'); } }); } }); } ``` task1执行完毕后,根据if判断启动task2任务执行。 2)通过addDependency或SequenceRunner处理。 ``` @Concurrent function DependencyHandle(args: number): number { let t: number = Date.now(); while ((Date.now() - t) < 1000) { continue; } return args; } export function AddDependencyTest() { let task1:taskpool.Task = new taskpool.Task(DependencyHandle, 100); let task2:taskpool.Task = new taskpool.Task(DependencyHandle, 200); let task3:taskpool.Task = new taskpool.Task(DependencyHandle, 300); hilog.info(0x0000, 'testTag', 'dependency: add dependency start'); task1.addDependency(task2); task2.addDependency(task3); hilog.info(0x0000, 'testTag', 'dependency: add dependency end'); hilog.info(0x0000, 'testTag', 'dependency: start execute second'); taskpool.execute(task1).then(() => { hilog.info(0x0000, 'testTag', 'dependency: first task1 success'); if (g_callback != null) { g_callback('task1 finish.'); } }) taskpool.execute(task2).then(() => { hilog.info(0x0000, 'testTag', 'dependency: second task2 success'); if (g_callback != null) { g_callback('task2 finish.'); } }) taskpool.execute(task3).then(() => { hilog.info(0x0000, 'testTag', 'dependency: third task3 success'); if (g_callback != null) { g_callback('task3 finish.'); } }) } ``` task1依赖task2,task2依赖task3,上面任务执行的顺序是:task3执行完毕后再执行task2,最后执行task。 ``` @Concurrent function additionDelay(delay:number): void { let start: number = new Date().getTime(); while (new Date().getTime() - start < delay) { continue; } } @Concurrent function waitForRunner(finalString: string): string { return finalString; } export async function SeqRunnerTest() { let finalString:string = ""; let task1:taskpool.Task = new taskpool.Task(additionDelay, 3000); let task2:taskpool.Task = new taskpool.Task(additionDelay, 2000); let task3:taskpool.Task = new taskpool.Task(additionDelay, 1000); let task4:taskpool.Task = new taskpool.Task(waitForRunner, finalString); let runner:taskpool.SequenceRunner = new taskpool.SequenceRunner(); runner.execute(task1).then(() => { finalString += 'task1 finish.'; hilog.info(0x0000, 'testTag', 'seqrunner: task1 done.'); if (g_callback != null) { g_callback('task1 finish.'); } }); runner.execute(task2).then(() => { finalString += 'task2 finish.'; hilog.info(0x0000, 'testTag', 'seqrunner: task2 done.'); if (g_callback != null) { g_callback('task2 finish.'); } }); runner.execute(task3).then(() => { finalString += 'task3 finish.'; hilog.info(0x0000, 'testTag', 'seqrunner: task3 done.'); if (g_callback != null) { g_callback('task3 finish.'); } }); await runner.execute(task4); hilog.info(0x0000, 'testTag', 'seqrunner: task4 done, finalString is %{public}s', finalString); } ``` task1执行完毕后,执行task2,最后是task3执行完毕。 运行结果: 1)通过业务逻辑的结果来处理 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ag2Xtr1MArKC8o7p.webp" alt="" width="100%" /> 2)通过addDependency或SequenceRunner处理 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/xSei3GWXbe23FxuU.webp" alt="" width="100%" /> ## **场景四:优先级任务** 方案: 在taskpool.execute的参数二种设置线程的优先级,优先级分三个级别:LOW、MEDIUM(默认)、HIGH。通过设置优先级来运行taskpool任务。 核心代码: ``` @Concurrent function ServiceHandle(pri: string): string { hilog.info(0x0000, 'testTag', 'enter ServiceHandle:%{public}s', pri); hilog.info(0x0000, 'testTag', 'end ServiceHandle:%{public}s', pri); return pri; } export function CallPriorityHanel() { let task1: taskpool.Task = new taskpool.Task(ServiceHandle, "LOW"); let task2: taskpool.Task = new taskpool.Task(ServiceHandle, "MEDIUM"); let task3: taskpool.Task = new taskpool.Task(ServiceHandle, "HIGH"); taskpool.execute(task1, taskpool.Priority.LOW).then((res: object) => { hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res); }); taskpool.execute(task2, taskpool.Priority.MEDIUM).then((res: object) => { hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res); }); taskpool.execute(task3, taskpool.Priority.HIGH).then((res: object) => { hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res); }); } ``` 当前的设备都是多核的,并不是说将优先级设置程HIGH,该任务就会最先调度。 **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/vdhpoXsJVLhBw5VZ.webp" alt="" width="100%" /> **场景五:taskpool****的Napi****调用** 方案:C++层编译的库,在ArkTS层通过import库的方式引用后,在taskpool的回调函数中调用接口。核心代码: ``` @Concurrent function ServiceHandle(pars: number): number { hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars); // 业务处理过程,并将结果返回 testNapi.jsServiceHandle(88, 99); return 0; } export function CallHandle() { let task: taskpool.Task = new taskpool.Task(ServiceHandle, 1); taskpool.execute(task,).then((res: object) => { hilog.info(0x0000, 'testTag', 'printArgs result :%{public}d', res); }); } typedef struct TestData { int data; int type; } TestData; static napi_value JsServiceHandle(napi_env env, napi_callback_info info) { size_t argc = 2; napi_value args[2] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); TestData testData; napi_get_value_int32(env, args[0], &testData.data); napi_get_value_int32(env, args[1], &testData.type); OH_LOG_INFO(LOG_APP, "Native C++ Service handle:%{public}d,type:%{public}d", testData.data, testData.type); return nullptr; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"jsServiceHandle", nullptr, JsServiceHandle, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END ``` **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/QwLDkk5Y0LbuFqTw.webp" alt="" width="100%" />
面试官:手写线程池,我:?面试官:回去等通知吧!
本文主要是整理一些多线程面试常见的手撕题,可能不是很全,后续会继续补充,感兴趣的同学可以看一下! 本文详细代码全都放在 Github 了,有需要的同学可以上 Github 自取: <https://github.com/DIDA-lJ/Juc-interview> ## 1. 写两个线程轮流打印 1 - 100 这个是一道简单的 Java 多线程程序题,一个线程打印奇数,另外一个线程打印偶数,线程之间通过 wait()和 notifyAll()方法进行协调,确保轮流打印数字 ``` /** * @author linqi * @version 1.0.0 * @description 两个线程轮流打印 1~100 */ public class AlternatePrinting { private int currentNumber = 1; private final Object lock = new Object(); public static void main(String[] args) { AlternatePrinting ap = new AlternatePrinting(); // 创建奇数打印线程 Thread oddPrinter = new Thread(() -> { ap.printNumber(true); }); oddPrinter.start(); // 创建偶数打印线程 Thread evenPrinter = new Thread(() -> { ap.printNumber(false); }); evenPrinter.start(); } /** * 根据 isOdd 标志打印奇数或者偶数 * * @param flag true:奇数 false:偶数 */ private void printNumber(boolean flag) { while (currentNumber <= 100) { synchronized (lock) { while ((flag && currentNumber % 2 == 0) || (!flag && currentNumber % 2 == 1)) { try { // 如果当前线程不应该打印,等待 lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if (currentNumber <= 100) { System.out.println("Thread " + (flag ? "Odd " : "Even") + ": " + currentNumber); currentNumber++; lock.notifyAll(); } } } } } ``` ## 2. 三个线程交替顺序打印出 1-100 针对每个线程分配一个打印范围,第一个线程打印 3 的倍数,第二个线程打印 3n + 1 的数,第三个线程打印 3n + 2 的数(其中n是非负整数),同时使用一种机制来确保三个线程交替执行。 ``` public class AlternatePrintingThreeThreads { /** * 当前要打印的数字 */ private int currentNumber = 1; /** * 用于同步的锁对象 */ private final Object lock = new Object(); /** * 控制哪个线程应该打印的标识 0:3n ,1:3n + 1,2: 3n + 2 */ private int turn = 0; public static void main(String[] args) { AlternatePrintingThreeThreads ap = new AlternatePrintingThreeThreads(); // 创建并启动三个线程 Thread t1 = new Thread(() -> ap.printNumbers(0)); Thread t2 = new Thread(() -> ap.printNumbers(1)); Thread t3 = new Thread(() -> ap.printNumbers(2)); t1.start(); t2.start(); t3.start(); } /** * 根据 turn 的值打印对应范围的数字 * * @param offset 0:3n 1:3n+1 2:3n+2 */ private void printNumbers(int offset) { while (currentNumber <= 100) { synchronized (lock) { while ((turn % 3) != offset) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (currentNumber <= 100 && (currentNumber - 1) % 3 == offset) { System.out.println("Thread " + (offset + 1) + " printed: " + currentNumber); currentNumber++; turn = (turn + 1) % 3; lock.notifyAll(); } } } } } ``` 在程序中,使用 ture 变量来控制线程的打印,然后每个线程打印前检查一下 turn 的值,如果不是轮到自己打印,就调用了 wait 方法进入等待状态。当一个线程打印完毕之后,更新 turn 的值,并且通过 notifyAll()方法唤醒其他可能在等待的线程。 ## 3. 多线程问题:线程 A,B,C 分别打印 1、2、3,顺序执行 10 次 为了顺序执行10次打印任务,其中线程A打印1,线程B打印2,线程C打印3,我们可以使用一个共享的计数器来跟踪当前的打印轮次,并确保每个线程在正确的轮次中执行。 ``` /** * @author linqi * @version 1.0.0 * @description 线程 A、B、C 分别打印 1,2,3 顺序执行 10 次 */ public class SequentialPrinting { /** * 当前线程打印的次数 */ private int count = 0; /** * 用于同步锁对象 */ private final Object lock = new Object(); public static void main(String[] args) { SequentialPrinting printer = new SequentialPrinting(); // 创建并启动线程 Thread tA = new Thread(() -> printer.printNumber(1), "A"); Thread tB = new Thread(() -> printer.printNumber(2), "B"); Thread tC = new Thread(() -> printer.printNumber(3), "C"); tA.start(); tB.start(); tC.start(); } private void printNumber(int numberToPrint) { for (int i = 0; i < 10; i++) { synchronized (lock) { while (count % 3 != numberToPrint - 1) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if (count < 30) { System.out.println("Thread " + Thread.currentThread().getName() + ": " + numberToPrint); count++; lock.notifyAll(); } } } } } ``` 在这个程序中,count变量用来跟踪当前的打印轮次,并且确保每个线程在正确的轮次执行。每个线程都会检查count的值,如果当前轮次不是自己的,就会调用wait()方法进入等待状态。当一个线程打印完毕后,它会增加count的值,并通过notifyAll()方法唤醒其他可能在等待的线程。 注意,count变量的上限设置为30(10轮,每轮3个数字),以确保程序只执行10轮打印。每个线程都会检查这个条件,以避免超出所需的打印轮次。 此程序会按顺序(线程A、线程B、线程C)执行打印任务,每个线程打印自己的数字,总共进行10轮。 ## 4. **计数累加怎么线程安全,可以怎么实现,100个线程,每个线程累加100次** 以下是一个简单的Java示例,演示了如何使用AtomicInteger来实现线程安全的计数累加。在这个示例中,我们将创建100个线程,并且每个线程将对计数器执行100次累加操作。 ``` public class AtomicCounterDemo { private static final AtomicInteger counter = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { // 创建100个线程来增加计数器的值 ExecutorService executor = Executors.newFixedThreadPool(100); // 提交 100 个任务,每个任务执行 100 次累加 for (int i = 0; i < 100; i++) { executor.submit(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { counter.incrementAndGet(); } } }); } // 关闭线程池 executor.shutdown(); // 等待所有任务完成 executor.awaitTermination(1, TimeUnit.HOURS); // 输出最终的值 System.out.println("Final counter value: " + counter.get()); } } ``` 在这个示例中,我们定义了一个 AtomicInteger 类型的静态变量counter作为我们的计数器。incrementCounterHundredTimes方法包含了一个循环,它会使计数器递增100次。由于AtomicInteger的incrementAndGet方法是线程安全的,所以我们不需要额外的同步措施。 在main方法中,我们创建了一个拥有100个线程的线程池,并提交了100个任务,每个任务都会执行incrementCounterHundredTimes方法。在所有任务完成后,我们输出最终的计数值。由于每个线程都会将计数器增加100,所以最终的计数值应该是 100 * 100 = 10000。 ## 5. 线程交叉打印12A34B56C,多种实现方式(一个打印数字,一个打印字母) **示例 1:使用wait()和notifyAll()** ``` /** * @author linqi * @version 1.0.0 * @description 线程交替打印 12A34B56C 使用 wait() 和 notifyAll() */ public class CrossPrint { private static final Object lock = new Object(); private static boolean printNumber = true; public static void main(String[] args) { Thread printNumberThread = new Thread(() ->{ for(int i = 1; i <= 52; i=i+2){ synchronized (lock){ while(!printNumber){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.print(i); System.out.print(i + 1); printNumber = false;// 打印切换标志 lock.notifyAll();// 唤醒等待的线程 } } }); Thread printLetterThread = new Thread(() ->{ for(char c = 'A'; c <= 'Z'; c++){ synchronized (lock){ while(printNumber){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.print(c); printNumber = true;// 打印切换标志 lock.notifyAll();// 唤醒等待的线程 } } }); printLetterThread.start(); printNumberThread.start(); } } ``` **示例 2:使用ReentrantLock和Condition** ``` import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author linqi * @version 1.0.0 * @description */ public class CrossPrintWithLock { private static final Lock lock = new ReentrantLock(); private static final Condition printNumberCondition = lock.newCondition(); private static final Condition printLetterCondition = lock.newCondition(); private static boolean printNumber = true; public static void main(String[] args) { Thread printNumberThread = new Thread(() ->{ for(int i = 1; i <= 52; i=i+2){ synchronized (lock){ while(!printNumber){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.print(i); System.out.print(i + 1); printNumber = false;// 打印切换标志 lock.notifyAll();// 唤醒等待的线程 } } }); Thread printLetterThread = new Thread(() ->{ for(char c = 'A'; c <= 'Z'; c++){ synchronized (lock){ while(printNumber){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.print(c); printNumber = true;// 打印切换标志 lock.notifyAll();// 唤醒等待的线程 } } }); printLetterThread.start(); printNumberThread.start(); } } ``` ## 6. 两个线程交替打印ABCD..Z字母,一个大写一个小写 目标输出:AbCdEfGhIjKlMnOpQrStUvWxYz 代码示例: ``` /** * @author linqi * @version 1.0.0 * @description */ public class AlternateLetterPrinting { private static final Object lock = new Object(); private static char currentLetter = 'A'; private static boolean printUpperCase = true; public static void main(String[] args) { Thread upperCasePrinter = new Thread(()-> printLetter(true)); Thread lowerCasePrinter = new Thread(()-> printLetter(false)); upperCasePrinter.start(); lowerCasePrinter.start(); } private static void printLetter(boolean isUpperCaseThread) { while (currentLetter <= 'Z'){ synchronized (lock){ while (printUpperCase != isUpperCaseThread) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if (currentLetter > 'Z') { break; } if (isUpperCaseThread) { System.out.print( currentLetter); } else { System.out.print(Character.toLowerCase( currentLetter)); } printUpperCase = !printUpperCase; currentLetter++; lock.notifyAll(); } } } } ``` ## 7. 两个线程交替打印出a1b2c3.....z26 **示例代码:** ``` /** * @author linqi * @version 1.0.0 * @description */ public class AlternatePrintingNumberLetter { private static final Object lock = new Object(); /** * 用于计数,确认打印的字母和数字 */ private static int count = 1; /** * 控制标记,用于控制是否打印数字 */ private static boolean printNumber = false; public static void main(String[] args) { // 创建打印数字的线程 Thread printNumberThread = new Thread(() ->{ while(count <= 26){ synchronized (lock){ while(!printNumber){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if(count <= 26){ System.out.print(count); count++; printNumber = false; lock.notifyAll(); } } } }); // 创建打印字母的线程 Thread printLetterThread = new Thread(()->{ while(count <= 26){ synchronized (lock){ while(printNumber) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (count <= 26){ char letter = (char)('a' + count - 1); System.out.print(letter); printNumber = true; lock.notify(); } } } }); printNumberThread.start(); printLetterThread.start(); } } ``` 这个demo中,我们使用了Object类的wait()和notifyAll()方法来控制两个线程的交替执行。wait()方法会使当前线程等待,直到其他线程调用notifyAll()方法唤醒所有在该对象上等待的线程。 我们使用count变量来跟踪打印的进度,printNumber标志来控制哪个线程应该打印。当一个线程打印完毕后,它会更改printNumber标志,并通过notifyAll()唤醒其他线程。这样,两个线程就可以交替执行,直到打印完z26。 ## 8. **两个线程,一个打印abcd,一个打印1234,需求交替打印出a1b2c3d4a1b2c3d4 ; 打印10轮** 我们需要一个共享资源来控制线程之间的交替执行,并确保它们按顺序打印。以下是一个简单的Java示例,其中包含必要的同步机制以实现所需的交替打印: ``` /** * @author linqi * @version 1.0.0 * @description */ public class AlternatePrintDemo { private static final Object lock = new Object(); /** * 0 表示打印字母,1 表示数字 */ private static int state = 0; private static int round = 0; public static void main(String[] args) { Thread printLetters = new Thread(() ->{ for(int i = 0; i < 40;i++){ synchronized (lock){ while(state != 0){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if(round < 10){ char letter = (char)('a' + (i % 4)); System.out.print(letter); state = 1; lock.notifyAll(); } } } }); Thread printNumbers = new Thread(() ->{ for(int i = 0; i < 40 ;i++){ synchronized (lock){ while(state != 1){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if(round < 10){ int number = (i % 4 )+ 1; System.out.print(number); if( (i + 1) % 4 == 0){ round++; } state = 0; lock.notifyAll(); } } } }); printNumbers.start(); printLetters.start(); } } ``` 在这个示例中,我们使用了一个state变量来控制哪个线程应该打印。state为0时,字母线程打印;state为1时,数字线程打印。我们还使用了一个round变量来跟踪已经完成的打印轮数。当达到10轮时,两个线程将停止打印。 注意,这个示例依赖于Java对象的内置锁(通过synchronized关键字实现)和等待/通知机制(通过wait()和notifyAll()方法实现)。这种方法确保了线程之间的正确同步,以实现交替打印。 ## 9. **假设有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?** **方式一:只使用 join** ``` import java.util.Random; /** * @author linqi * @version 1.0.0 * @description T2 在 T1 之后执行,T3 在 T2 之后执行 */ public class ThreadJoinDemo { public static void main(String[] args) { Thread t1 = new Thread(new Task("T1"), "T1"); Thread t2 = new Thread(new Task("T2"), "T2"); Thread t3 = new Thread(new Task("T3"), "T3"); // 启动 t1 t1.start(); try { t1.join(); t2.start(); t2.join(); t3.start(); t3.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } private static class Task implements Runnable { private String name; public Task(String name) { this.name = name; } @Override public void run() { long startTime = System.currentTimeMillis(); System.out.println(name + " 开始执行!"); try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(name + " 执行完毕!"); long endTime = System.currentTimeMillis(); System.out.println(name + " 执行时间:" + (endTime - startTime) + "ms"); System.out.println("--------------------------------"); } } } ``` 在这个示例中,Task是一个实现了Runnable接口的类,它代表了一个可以被线程执行的任务。在main方法中,我们创建了三个线程t1、t2和t3,分别对应T1、T2和T3。我们首先启动t1,然后使用t1.join()等待它完成。一旦t1完成,我们启动t2并等待它完成,依此类推。这种方法确保了线程按照T1 -> T2 -> T3的顺序执行。 **方式二:使用join配合CountDownLatch的一个简单示例:** ``` import java.util.concurrent.CountDownLatch; /** * @author linqi * @version 1.0.0 * @description */ public class ThreadExecutionOrderDemo { private static CountDownLatch t1ToT2Latch = new CountDownLatch(1); private static CountDownLatch t2ToT3Latch = new CountDownLatch(1); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { long startTime = System.currentTimeMillis(); System.out.println("T1 开始执行!"); try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } long endTime = System.currentTimeMillis(); System.out.println("T1 执行时间:" + (endTime - startTime) + "ms"); System.out.println("T1 执行完毕!"); System.out.println("------------------------------------------------"); t1ToT2Latch.countDown(); }, "T1"); Thread t2 = new Thread(() -> { try{ t1ToT2Latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } long startTime = System.currentTimeMillis(); System.out.println("T2 开始执行!"); try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } long endTime = System.currentTimeMillis(); System.out.println("T2 执行时间:" + (endTime - startTime) + "ms"); System.out.println("T2 执行完毕!"); System.out.println("------------------------------------------------"); t2ToT3Latch.countDown(); }, "T2"); Thread t3 = new Thread(() -> { try{ t2ToT3Latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } long startTime = System.currentTimeMillis(); System.out.println("T3 开始执行!"); try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } long endTime = System.currentTimeMillis(); System.out.println("T3 执行时间:" + (endTime - startTime) + "ms"); System.out.println("T3 执行完毕!"); }, "T3"); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); } } ``` 在这个示例中,t1ToT2Latch和t2ToT3Latch是两个CountDownLatch实例,分别用于控制T1到T2和T2到T3的执行顺序。T2线程在开始执行其主要任务之前会等待t1ToT2Latch计数到0,而T3线程则会等待t2ToT3Latch计数到0。这样,我们就可以确保T2在T1完成后执行,T3在T2完成后执行。 ## 10. **仿购票系统,目前有1000张票,同时有10个购票窗口,模拟购票流程,打印购票结果,比如:从1窗口购买1张票,剩余999张票** 以下是一个简单的Java示例,模仿了一个购票系统。这个系统有1000张票,并且有10个购票窗口。每个窗口可以购买票,每次购票后,系统会更新剩余票数并打印出来。 ``` import java.util.Random; /** * @author linqi * @version 1.0.0 * @description 购票系统 */ public class TicketSystemDemo { /** * 总共票数 */ private static final int TOTAL_TICKETS = 1000; /** * 剩余票数 */ private static int remainingTICKETS = TOTAL_TICKETS; /** * 锁对象,用于同步 */ private static final Object lock = new Object(); public static void main(String[] args) { // 创建线程,并且启动 for (int i = 0; i < 10; i++) { new Thread(new TicketSeller(i)).start(); } } private static class TicketSeller implements Runnable { private int windowNumber; public TicketSeller(int windowNumber) { this.windowNumber = windowNumber; } @Override public void run() { while (true) { synchronized (lock) { if (remainingTICKETS > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } buyTicket(); } else { break; } // 模拟购票后的其他操作,增加随机性 try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } private void buyTicket() { int number = new Random().nextInt(10); if(remainingTICKETS >= number && number > 0){ remainingTICKETS = remainingTICKETS - number; System.out.println("从窗口 G1000" + windowNumber + " 购买了 " + number + " 张票, 还剩 " + remainingTICKETS + " 张票"); } } } } ``` 在这个示例中,TicketSystemDemo 类包含了一个模拟购票系统的主程序。TicketWindow 类实现了 Runnable接口,代表一个购票窗口。每个窗口在一个单独的线程中运行,尝试购买票,直到票卖完为止。 关键点是使用 synchronized 块来同步对剩余票数的访问,以避免多个线程同时修改票数导致数据不一致。当剩余票数为0时,窗口线程将结束执行。 ## 11. **有一批任务Tasks, 现在我需要实现按批次执行,并且批次可以动态指定,例如[1,3,5,7]第一批执行,[11,13,15,17]第二批执行,..., 最后没有指定的任务就最后一起执行掉。批次之间需要按顺序,前一批执行完了才执行下一批** ``` import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * @author linqi * @version 1.0.0 * @description */ public class TaskRunner { public static void main(String[] args) { // 假设创建了 100 个任务 List<Runnable> tasks = TaskRunner.createTasks(); // 第一批和第二批执行的任务索引 List<Integer> batch1 = new ArrayList<Integer>(Arrays.asList(1, 3, 5, 7)); List<Integer> batch2 = new ArrayList<Integer>(Arrays.asList(11, 13, 15, 17)); List<List<Integer>> batchs = new ArrayList<>(); batchs.add(batch1); batchs.add(batch2); try { runTasksInBatches(tasks, batchs); System.out.println("====== 全部任务执行完成!======"); } catch (ExecutionException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } } private static void runTasksInBatches(List<Runnable> tasks, List<List<Integer>> batchs) throws ExecutionException, InterruptedException { // 使用固定大小的线程池 ExecutorService executor = Executors.newFixedThreadPool(10); // 执行第一批任务 List<Future<Void>> futures = new ArrayList<Future<Void>>(); Set<Integer> allBatchTaskIndexs = new HashSet<Integer>(); for (int i = 0; i < batchs.size(); i++) { System.out.println("====== 第 " + (i + 1) + " 批任务开始执行!======"); for (int index : batchs.get(i)) { Future<Void> future = (Future<Void>) executor.submit(tasks.get(index)); futures.add(future); allBatchTaskIndexs.add(index); } // 等待一批任务完成 for (Future<Void> f : futures) { f.get(); } futures.clear(); System.out.println("====== 第 " + (i + 1) + " 批任务执行完成!======"); } // 执行剩下的任务 System.out.println("====== 最后 1 批任务开始执行!======"); for (int i = 0; i < tasks.size(); i++) { if (!allBatchTaskIndexs.contains(i)) { executor.submit(tasks.get(i)); } } // 关闭线程池,并且等待所有任务执行 executor.shutdown(); while (!executor.isTerminated()) { // 等待所有任务执行完毕 } System.out.println("====== 最后 1 批任务执行完成!======"); } private static List<Runnable> createTasks() { List<Runnable> tasks = new ArrayList<>(); // 创建任务的逻辑 for (int i = 0; i < 30; i++) { final int taskId = i; tasks.add(() -> { try { long startTime = System.currentTimeMillis(); Thread.sleep((long) (Math.random() * 1000)); long endTime = System.currentTimeMillis(); System.out.println("执行任务,任务 ID :" + taskId + ",耗时:" + (endTime - startTime) + "ms"); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } return tasks; } } ``` ## 12. 手写线程池 ``` import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** * @author linqi * @version 1.0.0 * @description */ public class ThreadPoolTrader implements Executor { private final AtomicInteger ctl = new AtomicInteger(0); private volatile int corePoolSize; private volatile int maximumPoolSize; private final BlockingQueue<Runnable> workQueue; public ThreadPoolTrader(int corePoolSize, int maximumPoolSize, BlockingQueue<Runnable> workQueue) { this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; } @Override public void execute(Runnable command) { int c = ctl.get(); if (c < corePoolSize) { if (!addWorker(command)) { reject(); } return; } if (!workQueue.offer(command)) { if (!addWorker(command)) { reject(); } } } private boolean addWorker(Runnable firstTask) { if (ctl.get() >= maximumPoolSize) { return false; } Worker worker = new Worker(firstTask); worker.thread.start(); ctl.incrementAndGet(); return true; } private final class Worker implements Runnable { final Thread thread; Runnable firstTask; public Worker(Runnable firstTask) { this.thread = new Thread(this); this.firstTask = firstTask; } @Override public void run() { Runnable task = firstTask; try { while (task != null || (task = getTask()) != null) { task.run(); if (ctl.get() > maximumPoolSize) { break; } task = null; } } finally { ctl.decrementAndGet(); } } private Runnable getTask() { for (; ; ) { try { System.out.println("workQueue.size:" + workQueue.size()); return workQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } } private void reject() { throw new RuntimeException("Error!ctl.count:" + ctl.get() + " workQueue.size:" + workQueue.size()); } public static void main(String[] args) { ThreadPoolTrader threadPoolTrader = new ThreadPoolTrader(2, 2, new ArrayBlockingQueue<Runnable>(10)); for (int i = 0; i < 10; i++) { int finalI = i; threadPoolTrader.execute(() -> { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务编号:" + finalI); }); } } } ```
java面试题(十二)之多线程篇
<html> <head></head> <body> <div class="content ql-editor"> <p><a href="https://juejin.cn/post/7196842026485383225" target="_blank">java面试题(十二)之多线程篇 - 掘金 (</a><a href="http://juejin.cn" target="_blank">juejin.cn</a><a href="https://juejin.cn/post/7196842026485383225" target="_blank">)</a></p> <p><a href="https://blog.csdn.net/weixin_60257072/article/details/128896860?spm=1001.2014.3001.5501" target="_blank">java面试题(十二)之多线程篇_林寻星辰的博客-CSDN博客</a></p> <p><br></p> <p><br></p> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.1 创建线程有哪几种方式?</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">通过继承Thread类来创建并启动线程的步骤如下:</span></p> <ol> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">创建Thread子类的实例,即创建了线程对象。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">调用线程对象的start()方法来启动该线程。</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">通过实现Runnable接口来创建并启动线程的步骤如下:</span></p> <ol> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">调用线程对象的start()方法来启动该线程。</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">通过实现Callable接口来创建并启动线程的步骤如下:</span></p> <ol> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">使用FutureTask对象作为Thread对象的target创建并启动新线程。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。</span></li> </ol> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">扩展阅读</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">通过继承Thread类、实现Runnable接口、实现Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">采用实现Runnable、Callable接口的方式创建多线程的优缺点:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">采用继承Thread类的方式创建多线程的优缺点:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。</span></p> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.2 说说Thread类的常用方法</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread类常用构造方法:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread()</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread(String name)</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread(Runnable target)</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread(Runnable target, String name)</span></li> </ol> <p><em style="font-size: 14px; color: rgb(51, 51, 51);">其中,参数 name为线程名,参数 target为包含线程体的目标对象。</em></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread类常用静态方法:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">currentThread():返回当前正在执行的线程;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">interrupted():返回当前执行的线程是否已经被中断;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">sleep(long millis):使当前执行的线程睡眠多少毫秒数;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">yield():使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行;</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">Thread类常用实例方法:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">getId():返回该线程的id;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">getName():返回该线程的名字;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">getPriority():返回该线程的优先级;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">interrupt():使该线程中断;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">isInterrupted():返回该线程是否被中断;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">isAlive():返回该线程是否处于活动状态;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">isDaemon():返回该线程是否是守护线程;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">setDaemon(boolean on):将该线程标记为守护线程或用户线程,如果不标记默认是非守护线程;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">setName(String name):设置该线程的名字;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">setPriority(int newPriority):改变该线程的优先级;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">join():等待该线程终止;</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">join(long millis):等待该线程终止,至多等待多少毫秒数。</span></li> </ol> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.3 run()和start()有什么区别?</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。</span></p> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.4 线程是否可以重复启动,会有什么后果?</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。</span></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">扩展阅读</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。</span></p> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.5 介绍一下线程的生命周期</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。当发生如下情况时,线程将会进入阻塞状态:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程调用sleep()方法主动放弃所占用的处理器资源。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程在等待某个通知(notify)。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">调用sleep()方法的线程经过了指定时间。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程调用的阻塞式IO方法已经返回。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程成功地获得了试图取得的同步监视器。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程正在等待某个通知时,其他线程发出了一个通知。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">处于挂起状态的线程被调用了resume()恢复方法。</span></li> </ol> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">线程会以如下三种方式结束,结束后就处于死亡状态:</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">run()或call()方法执行完成,线程正常结束。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">线程抛出一个未捕获的Exception或Error。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。</span></li> </ol> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">扩展阅读</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">线程5种状态的转换关系,如下图所示:</span></p> <p></p> <p class="image-uploading"><span><img src="https://pic.code-nav.cn/planet_post_image/1609216259264610306/2x0f9nrz.jpeg"></span></p> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.6 如何实现线程同步?</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <ol> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">同步方法即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">同步代码块即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">ReentrantLockJava 5新增了一个</span><a href="http://java.util.concurrent" target="_blank" style="font-size: 14px; color: rgb(51, 51, 51);">java.util.concurrent</a><span style="font-size: 14px; color: rgb(51, 51, 51);">包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">volatilevolatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">原子变量在java的</span><a href="http://util.concurrent.atomic" target="_blank" style="font-size: 14px; color: rgb(51, 51, 51);">util.concurrent.atomic</a><span style="font-size: 14px; color: rgb(51, 51, 51);">包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。</span></li> </ol> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.7 说一说Java多线程之间的通信方式</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">在Java中线程通信主要有以下三种方式:</span></p> <ol> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">wait()、notify()、notifyAll()如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">await()、signal()、signalAll()如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在</span><a href="http://lock.lock" target="_blank" style="font-size: 14px; color: rgb(51, 51, 51);">lock.lock</a><span style="font-size: 14px; color: rgb(51, 51, 51);">()和</span><a href="http://lock.unlock" target="_blank" style="font-size: 14px; color: rgb(51, 51, 51);">lock.unlock</a><span style="font-size: 14px; color: rgb(51, 51, 51);">之间才可以使用。事实上,await()/signal()/signalAll() 与 wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">BlockingQueueJava 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。</span></li> </ol> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.8 说一说Java同步机制中的wait和notify</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">wait()、notify()、notifyAll()用来实现线程之间的通信,这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写,并且只有采用synchronized实现线程同步时才能使用这三个方法。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()方法用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。</span></p> <p><span style="font-size: 14px; color: rgb(51, 51, 51);">每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。</span></p> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.9 说一说sleep()和wait()的区别</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <ol> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;</span></li> <li data-list="ordered"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。</span></li> </ol> <p><br></p> <h1><span style="font-size: 14px; color: rgb(51, 51, 51);">4.10 说一说notify()、notifyAll()的区别</span></h1> <p><br></p> <p><strong style="font-size: 14px; color: rgb(51, 51, 51);">参考答案</strong></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">notify()用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。</span></li> <li data-list="bullet"><span class="ql-ui"></span><span style="font-size: 14px; color: rgb(51, 51, 51);">notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。</span></li> </ol> <p><br></p> <p><br></p> </div> </body> </html>
深入浅出Java多线程
笔者在读完市面上关于Java并发编程的资料后,感觉有些知识点不是很清晰,于是在RedSpider社区内展开了对Java并发编程原理的讨论。鉴于开源精神,我们决定将我们讨论之后的Java并发编程原理整理成书籍,分享给大家。 站在巨人的肩上,我们可以看得更远。本书内容的主要来源有博客、书籍、论文,对于一些已经叙述得很清晰的知识点我们直接引用在本书中;对于一些没有讲解清楚的知识点,我们加以画图或者编写Demo进行加工;而对于一些模棱两可的知识点,本书在查阅了大量资料的情况下,给出最合理的解释。 写本书的过程也是对自己研究和掌握的技术点进行整理的过程,希望本书能帮助读者快速掌握并发编程技术。 地址:https://redspider.gitbook.io/concurrent/
《Java并发编程实战》
百度网盘提取码:d31k 地址:https://pan.baidu.com/s/1ySvDUGbk_leHEY2gqerihA
