JUC P1 进程,线程,管程 基础+代码
文章目录
- JUC P1 进程,线程,管程 基础+代码
- 0. 简单概念
- 0.1 进程和线程
- 0.2 并行和并发
- 0.3 同步和异步
- 1. 创建线程的方法
- 2. 查看进程和线程的方法
- 3. 线程运行
- 3.1 栈与栈帧
- 3.2 线程上下文切换
- 3.3 Java Thread 常见方法
- 3.3.1 start() 和 run()
- 3.3.2 sleep() 和 yield()
- 3.3.3 任务优先级
- 3.3.4 sleep() 应用
- 3.3.5 join()
- 3.3.6 interrupt()
- 3.3.7 过时方法
- 3.3.8 守护线程
- 3.4 练习
- 4. 共享模型之管程
- 4.1 共享问题
- 4.2 synchronized 解决方案
- 语法
- 4.3 变量的线程安全分析
- 4.4 练习
- 4.5 Monitor 管程
- 4.5.1 对象头
- 4.5.2 Monitor 管程
- 4.5.3 轻量级锁
- 4.5.4 重量级锁
- 4.5.5 偏向锁(Java 15 弃用)
- 偏向状态
- 为什么废弃偏向锁?
- 4.5.6 锁消除
- 4.6 wait() / notify()
- 4.6.1 sleep() 和 wait() 的区别
- 4.6.2 最佳使用方法
- 4.7 多线程设计模式
- 4.7.1 保护性暂停
- 扩展
- 4.7.2 生产者/消费者(异步)
- 4.8 park() / unpark()
- 4.9 死锁
- 定位死锁
- 哲学家就餐问题
- 活锁
- 饥饿
JUC P1 进程,线程,管程 基础+代码
教程:https://www.bilibili.com/video/BV16J411h7Rd
0. 简单概念
0.1 进程和线程
线程:
- 计算机调度的最小单元
- 更加轻量,加一个线程只需要分配很少的存储空间,可以多个线程共享同一个进程的空间
- 线程同步:互斥锁
进程:
- 计算机分配资源的最小单元
- 一个进程可以有多个线程,至少有一个线程
- fork 创建子进程,当发生写操作时会复制父进程一块内存
- 进程间通信:管道,消息队列,Socket,信号量
0.2 并行和并发
并行:多个线程同时执行,一手画圆,一手画方
并发:每个线程分配一定的时间执行,一会画圆,一会画方
0.3 同步和异步
同步:需要等待返回结果,才能继续运行
异步:不需要等待返回结果,就能继续运行
异步的小例子,主线程不需要等待另外一个线程执行结束就可以直接向下执行:
public static void main(String[] args) throws ClassNotFoundException {new Thread(() -> {try {long start = System.currentTimeMillis();Thread.sleep(2000);System.out.println("我执行完了, 执行时间: " + (System.currentTimeMillis() - start) + " ms");} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();System.out.println("hello");
}
1. 创建线程的方法
参考我之前写的:Java 创建线程的三种方式
这里补充一些细节:
- 使用 FutureTask 实例获取返回值的时候
task.get()
会使当前线程阻塞,一直等到结果返回才继续往下执行。
public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {log.debug("线程执行");Thread.sleep(2000);return 200;}});new Thread(task, "线程1").start();log.debug("{}", task.get());log.debug("执行结束!");
}
简化版:
public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<Integer>(() -> {log.debug("线程执行");Thread.sleep(2000);return 200;});new Thread(task, "线程1").start();log.debug("{}", task.get());log.debug("执行结束!");
}
2. 查看进程和线程的方法
Linux:
ps -ef
查看所有进程信息
kill <pid>
,杀死进程top
实时查看进程
top -Hp <pid>
查看进程中的线程
Java:
jps
查看 java 进程
jstack <pid>
查看某个时刻某个 java 进程中的所有线程详情
jconsole
使用图形化界面查看进程情况
3. 线程运行
3.1 栈与栈帧
每个线程启动 JVM 会分配给该线程一块虚拟机栈内存,每个线程只能有一个活动栈帧,对应当前执行的方法。
3.2 线程上下文切换
切换时机:
- 线程 CPU 时间片结束
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep(),yield(),wait(),join(),park(),synchronized,lock 等方法。
上下文切换需要记录当前线程的状态,JVM 实现此方法的就是程序计数器。
线程不是越多越好,频繁的上下文切换也会导致性能下降。
3.3 Java Thread 常见方法
start()
:启动一个新线程,让一个线程进入就绪,不一定会立刻运行(CPU 时间片还没分配)run()
:线程调用 start() 后会自动调用该方法,不能手动调用,手动调用就相当于一个普通方法来使用join()
:等待线程运行结束join(long n)
:等待线程运行结束,最多等待 n 毫秒setPriority(int)
:设置优先级,默认为 5,最高为 10getState()
: 获取线程状态,Java 中共有 6 种状态
public enum State {NEW, // 新建RUNNABLE, // 可运行线程的线程状态。处于可运行状态的线程正在JVM中执行BLOCKED, // 阻塞WAITING, // 等待TIMED_WAITING, // 限时等待TERMINATED; // 终止
}
Java 中的 RUNNABLE 对应多种 OS 中的状态:
isInterrupted()
:是否被打断,不清除打断标记isAlive()
: 线程是否存活interrupt()
:打断线程,若被打断的线程正在 sleep,wait,join,则会导致被打断的线程抛出InterruptException
,并清除打断标记interrupted()
:判断当前线程是否被打断,清除打断标记sleep(long n)
:当前线程休眠 n 毫秒,让出 cpu 的时间片给其他线程yield()
:提示线程调度器让出当前线程对 CPU 的使用,主要为了测试和调试
3.3.1 start() 和 run()
public static void main(String[] args) {FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("t1 线程执行完毕!");return 200;});new Thread(task, "t1").run();log.debug("main 执行完毕!");
}
主动调用
run()
就相当于调用一个普通方法一样,不会以多线程的方式去执行。正确的方式应该调用
start()
方法,然后让系统自动调用run()
方法。
start()
不能被调用两次,会抛出 IllegalThreadStateException
。
public static void main(String[] args) {FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("t1 线程执行完毕!");return 200;});Thread t1 = new Thread(task, "t1");t1.start();t1.start();log.debug("main 执行完毕!");
}
- 当线程创建后没有调用
start()
之前是NEW
状态 - 调用
start()
之后是RUNNABLE
状态。
3.3.2 sleep() 和 yield()
sleep:
- 调用 sleep 会让线程从
正在运行
进入TIMED_WAITING
- 其他线程使用
interrupt()
方法打断正在睡眠的线程会抛出InterruptedException
- 睡眠结束后线程未必会立即得到执行
public static void main(String[] args) throws InterruptedException {FutureTask<Integer> task = new FutureTask<>(() -> {try {log.debug("t1 进入睡眠...");Thread.sleep(2000);} catch (Exception e) {log.debug("t1 醒来");e.printStackTrace();return 201;}return 200;});Thread t1 = new Thread(task, "t1");t1.start();Thread.sleep(1000);t1.interrupt();log.debug("main 执行完毕!");
}
4. ★建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
TimeUnit.SECONDS.sleep(2); // 表示当前线程睡 2 秒
yield:
- 调用 yield 会让线程从
正在运行
进入RUNNABLE
,然后调度执行其他同等优先级的线程,若没有同等优先级的线程,则不能保证让当前线程暂停 - 具体实现依赖 OS 的任务调度器
3.3.3 任务优先级
- 线程优先级只是提示调度器优先调度该线程,任务调度器可以选择忽略
- Java 中线程默认为 5,最小为 1,最大为 10
- 若 CPU 繁忙,优先级高的线程会获得更多的时间片
- 若 CPU 空闲,优先级几乎没什么用
3.3.4 sleep() 应用
防止 while(true)
导致 CPU 空转,浪费资源。可以使用 yield 或者 sleep 来让出 CPU 的使用权给其他程序,即使死循环 sleep(1) 也能大大减少 CPU 的利用。
- 也可以使用 wait 或者条件变量达到类似的效果(需要加锁,一般适用于同步的场景)
- sleep 适用于无需锁同步的场景
3.3.5 join()
在当前线程中调用另外一个线程的 join()
方法,表示等待另外一个线程结束后,当前线程才继续往下执行。
@Slf4j
public class InitTest {static int a = 0;public static void main(String[] args) throws InterruptedException {FutureTask<Integer> task = new FutureTask<>(() -> {log.debug("t1 线程开始执行...");TimeUnit.SECONDS.sleep(1);a = 10;return 0;});Thread t1 = new Thread(task, "t1");t1.start();// 调用 join 方法同步等待 t1 线程执行结束t1.join();log.debug("a 的值为: {}", a);}
}
join(long n)
可以进行有时间限制的等待。
3.3.6 interrupt()
若以异常的方式打断 wait(), sleep(), join(),会把打断标记置为 false
:
@Slf4j
public class InitTest {public static void main(String[] args) throws InterruptedException {FutureTask<Integer> task = new FutureTask<>(() -> {try {log.debug("t1 进入睡眠...");TimeUnit.SECONDS.sleep(2);} catch (Exception e) {log.debug("t1 醒来");e.printStackTrace();}return 200;});Thread t1 = new Thread(task, "t1");t1.start();TimeUnit.SECONDS.sleep(1);t1.interrupt();log.debug("是否打断: {}", t1.isInterrupted());}
}
正常打断(可以用于正常停止线程):
@Slf4j
public class InitTest {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (true) {boolean interrupted = Thread.currentThread().isInterrupted();if (interrupted) {log.debug("线程 t1 被打断了, 退出循环");break;}}}, "t1");t1.start();TimeUnit.SECONDS.sleep(1);t1.interrupt();log.debug("t1 线程是否被打断: {}", t1.isInterrupted());}
}
两阶段停止:
在一个线程 T1 中如何 “优雅” 停止一个 T2?
- 错误思路1:用
stop()
方法(已弃用),如果 T2 锁住共享资源,那么 T2 被杀死后将无法释放锁 - 错误思路2:
System.exit(int)
方法停止线程,该方法会让整个程序都停止
设计一个后台系统监控程序:
/*** 后台监控程序, 两阶段提交*/
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {MonitorThread monitorThread = new MonitorThread();monitorThread.start();TimeUnit.SECONDS.sleep(5);monitorThread.stop();}
}@Slf4j(topic = "c.MonitorThread")
class MonitorThread {private Thread monitor;// 启动监控public void start() {monitor = new Thread(() -> {while (true) {Thread thread = Thread.currentThread();if (thread.isInterrupted()) {log.debug("准备退出......");break;}try {TimeUnit.SECONDS.sleep(2); // case 1 :该处被打断, 打断标记还是 falselog.debug("执行监控记录"); // case 2 : 该处被打断, 打断标记置为 true} catch (InterruptedException e) {log.debug("睡眠时间被打断, 清除打断标记");e.printStackTrace();thread.interrupt(); // case 2 : 重新设置打断标记}}});monitor.start();}// 停止监控public void stop() {monitor.interrupt();}
}
打断 park() 中的线程,park()
方法若被打断,再次 park()
将执行失效,如果想再次 park()
可以使用 Thread.interrupted()
清除标记:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("park......");LockSupport.park();log.debug("unpark.....");log.debug("当前线程是否被打断: {}", Thread.currentThread().isInterrupted());LockSupport.park(); // 若被打断就无法再次 parklog.debug("unpark.....");});t1.start();TimeUnit.SECONDS.sleep(1);t1.interrupt();}
}
3.3.7 过时方法
以下方法不推荐使用,容易破坏同步代码块,造成线程死锁:
stop()
:停止线程运行suspend()
:挂起(暂停)线程运行resume()
:恢复线程运行
3.3.8 守护线程
所有非守护线程运行结束,即使守护线程的代码没执行完,也会强制结束:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {break;}}log.debug("守护线程结束!");});// 设置 t1 为守护线程t1.setDaemon(true);t1.start();TimeUnit.SECONDS.sleep(1);log.debug("主线程结束");}
}
常见守护线程:
- 垃圾回收器
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待他们处理完当前的请求
Note:
一个进程可以有多个守护线程
t1.setDaemon(true);
和t1.start();
不能更换顺序,否则抛出IllegalThreadStateException
操作系统层面的阻塞状态(比如读文件的时候)在 Java 中是 RUNNABLE 状态。
3.4 练习
烧水泡茶:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();Thread t1 = new Thread(() -> {try {log.debug("洗水壶.... 1 min");TimeUnit.SECONDS.sleep(1);log.debug("烧开水.... 15 min");TimeUnit.SECONDS.sleep(15);} catch (InterruptedException e) {throw new RuntimeException(e);}}, "t1");Thread t2 = new Thread(() -> {try {log.debug("洗茶壶, 洗茶杯, 拿茶叶.... 4min");TimeUnit.SECONDS.sleep(4);t1.join();log.debug("泡茶...");log.debug("总共用时: {} ms", System.currentTimeMillis() - start);} catch (InterruptedException e) {throw new RuntimeException(e);}}, "t2");t1.start();t2.start();}
}
该代码的缺陷:
- t2 线程需要等待 t1 线程烧水结束才能泡茶,假如反过来要实现 t1 线程拿 t2 线程的茶叶泡茶呢?怎么办?
- 在接下来的内容中会对该代码进行优化。
4. 共享模型之管程
- 共享问题
- synchronized
- 线程安全分析
- Monitor
- wait/notify
- 线程状态转换
- 活跃性
- Lock
4.1 共享问题
一个程序运行多个线程本身没问题
问题出现在多个线程访问共享资源
- 多个线程只读共享资源也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作时,称这段代码块为临界区(Critical Section)
多个线程在临界区内执行,由于代码的执行序列不同导致结果无法预测,称之发生了竞态条件(Race Condition)
4.2 synchronized 解决方案
为了实现临界区互斥访问,主要有:
- 阻塞式解决方案:synchronized,Lock
- 非阻塞式解决方案:原子变量
本节主要讲 synchronized
,俗称对象锁:
- 采用互斥的方式让同一时刻最多只能有一个线程持有对象锁,其他线程获取对象锁会进行阻塞。
Note:
互斥保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码
同步是由于线程执行先后不同,顺序不同,需要一个线程等待其他线程运行到某个点
语法
synchronized (object) {// 临界区
}
面向对象的写法:
class Room {private int counter = 0;public void increment() {synchronized (this) {counter++;}}public void decrement() {synchronized (this) {counter--;}}public int getCounter() {synchronized (this) {return counter; }}
}
方法上的 synchronized
:
class Room {public synchronized void test() {}
}// 等价于 ===>
class Room {public void test() {synchronized (this) {}}
}
class Room {public synchronized static void test() {}
}// 等价于 ===>
class Room {public static void test() {synchronized (Room.class) {}}
}
4.3 变量的线程安全分析
成员变量和静态变量是否线程安全?
- 若没共享或者被共享但是只读,则线程安全
- 若被共享且有读写操作,则要考虑线程安全问题
成员变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象未必:
- 若对象没有逃离方法,则线程安全
- 若对象逃离了方法的,则需要考虑线程安全问题
常见的线程安全类:
- String
- Integer
- StringBuffer
- Random
- Vector
- HashTable
- java.util.concurrent 包下的类
这里所说的线程安全类指的是单个方法是线程安全的,但是如果方法组合不一定是线程安全的:
Hashtable<Object, Object> hashtable = new Hashtable<>();
if (hashtable.get("key") == null) {hashtable.put("key", 111);
}
Note:
自定义的类,类中没有成员变量,那么该类是线程安全的
4.4 练习
转账问题:
@Slf4j(topic = "c.InitTest")
public class InitTest {static Random random = new Random();// 随机转账private static int randomAccount() {return random.nextInt(100) + 1;}public static void main(String[] args) throws InterruptedException {Account a = new Account(1000);Account b = new Account(1000);Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {a.transfer(b, randomAccount());}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {b.transfer(a, randomAccount());}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("转账 2000 次后的总金额 : {}", a.getMoney() + b.getMoney());}
}class Account {private int money;public Account(int money) {this.money = money;}public void setMoney(int money) {this.money = money;}public int getMoney() {return money;}// 给 target 账户转账public void transfer(Account target, int amount) {if (this.money >= amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}}
这样的话转账过程中会出现线程安全问题,钱可能越转越多,也可能越转越少:
如果直接在 transfer 方法上加 synchronized,这样解决不了问题的,因为每次锁的都是当前对象,两个线程每次操作的都是两个对象,因此可以直接锁 Account
类:
public void transfer(Account target, int amount) {synchronized (Account.class) {if (this.money >= amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}
}
4.5 Monitor 管程
4.5.1 对象头
32 位虚拟机:
通过 Klass Word
可以找到对象从属的 class 类对象。
普通对象:
数组对象:
Mark Word 结构(不同状态下,里面的结构也不同):
Note:
- 可以看到 age 占 4 位,因此
-XX:MaxTenuringThreShold
可以设置的最大分代年龄就是 15。- 类中的方法,是不占内存的,只有调用的时候才会分配栈帧。
- Integer 对象占的内存:
int 大小 + 对象头长度 = 12
64 位:
4.5.2 Monitor 管程
Note:
我的理解:管程就是一个“城管”,管理各个线程之间状态转换的数据结构。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized
给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
- 被锁住的临界区代码块一次只能有一个线程作为 Owner,EntryList 存储阻塞的线程,WaitSet 存储等待的线程。
- Thread-2 结束后,会唤醒 EntryList 中等待的线程来竞争锁。
- 同步代码块通过
monitorenter
和monitorexit
实现,如果过程中发生异常,系统会进行解锁然后抛出异常。
Note:
Monitor 锁是 OS 提供的,因此程序性能更低
4.5.3 轻量级锁
一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),那么就用轻量级锁进行优化。语法仍然使用 synchronized
。
锁重入的时候会进行 CAS 操作,将对象头中的 Mark Word 替换为指向锁记录的指针:
public static void main(String[] args) {fun1();
}public static void fun1() {synchronized (lock) {// synchronized 默认拥有可重入的特性, 一个线程多次获得锁不会导致死锁log.debug("方法 1 执行...");fun2();}
}public static void fun2() {synchronized (lock) {log.debug("方法 2 执行...");}
}
Note:
锁重入指的是一个线程不会因为多次获取锁导致死锁。
4.5.4 重量级锁
在一个线程尝试对对象加轻量级锁的过程中,CAS 竞争操作不成功,也就是此时有其他线程已经为该对象加了轻量级锁(有竞争),这时候需要先进行自旋操作,重复获取锁,若一直失败,则进行锁膨胀,将轻量级锁变为重量级锁,进入阻塞状态。
会直接锁膨胀,变成重量级锁,当重量级锁竞争失败才会进行自旋,自旋一直失败才进入阻塞状态。
Note:
自旋操作存疑!!!:https://www.nowcoder.com/discuss/604631
另外一个线程执行完毕, CAS 替换 Mark Word 会失败,因为锁已经变成重量级锁,因此释放锁,让阻塞的线程加入。
Note:
自旋操作
是为了防止上下文切换,进入阻塞状态,自旋会消耗 CPU,在单核机下自旋就是浪费,多核 CPU 下自旋操作才能发挥作用。
自旋操作是自适应的,默认开启。
4.5.5 偏向锁(Java 15 弃用)
为了解决轻量级锁每次锁重入都需要进行 CAS 操作,Java 6 引入了偏向锁进行优化:
- 只有第一次使用 CAS 将线程 ID 记录到对象的 Mark Word 和栈帧中,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,只要不发生竞争,这个对象就归该线程所有。
适用于:只有一个线程多次访问同步代码块的场景。
偏向状态
引入依赖:
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version>
</dependency>
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) {Dog dog = new Dog();log.debug(ClassLayout.parseInstance(dog).toPrintable());}
}class Dog {}
- 偏向锁是默认开启的,但是需要等应用程序启动后几秒才能激活,若想避免延迟可以设置
-XX:BiasedLockingStartupDelay=0
- 若确定应用程序中的线程通常都处于竞争状态,可以通过
-XX:-UseBiasedLocking=false
,程序默认进入轻量级锁状态
在 Java 15,上面这两个选项已经被弃用了!!!
Java HotSpot(TM) 64-Bit Server VM warning: Option BiasedLockingStartupDelay was deprecated in version 15.0 and will likely be removed in a future release.
为什么废弃偏向锁?
参考:https://zhuanlan.zhihu.com/p/365454004
- 对于使用了新类库的 Java 应用来说,偏向锁带来的收益已不如过去那么明显,而且在当下多线程应用越来越普遍的情况下,偏向锁带来的锁升级操作反而会影响应用的性能
- 偏向锁为同步系统引入了许多复杂的代码,并且对 HotSpot 的其他组件产生了影响。这种复杂性已经成为理解代码的障碍,也阻碍了对同步系统进行重构
Note:
偏向锁一直存在是因为兼容以前的 Hasttable 和 Vector 这种老古董性能差的类库
,从 Java 15 之后不准备去兼容了,因为已经有了像 ConcurrentHashMap 性能更好的类库。
现在上锁直接就是 thin lock,轻量级锁:
Dog dog = new Dog();
synchronized (dog) {log.debug(ClassLayout.parseInstance(dog).toPrintable());
}
- 看 16 进制数的最后一位,
0
表示轻量级锁
Note:
看 Mark Word 最后两位所表示的锁信息:
00 轻量级锁
01 无锁
10 重量级锁
11 GC 清除标记
4.5.6 锁消除
JVM 中叫同步省略。
简单讲就是如果一个对象只能被一个线程访问,就没必要上锁了,即使你上了锁,即时编译阶段会进行优化,把锁去掉。
可以通过 -XX:-EliminateLocks
关闭该功能。
4.6 wait() / notify()
- Owner 线程发现条件不满足,调用
wait()
方法,即可进入 WaitSet 变为 WATING 状态 - BLOCKED 和 WATING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁的时候唤醒
- WATING 线程会在 Owner 线程调用
notify()
或者notifyAll()
时唤醒,但是醒后不意味着立刻能获得锁,需要进入 EntryList 重新竞争
API:
wait(),notify(),notifyAll()
都是 Object 的函数- 调用的条件是:必须先获得此对象的锁,才能调用这几个方法
static Object lock = new Object();public static void main(String[] args) throws InterruptedException {lock.wait();
}// 抛出异常:IllegalMonitorStateException: current thread is not owner
static Object lock = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (lock) {log.debug("执行...");try {lock.wait(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("其他代码...");}}, "t1").start();new Thread(() -> {synchronized (lock) {log.debug("执行...");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("其他代码...");}}, "t2").start();TimeUnit.SECONDS.sleep(2);log.debug("主线程唤醒 wait 中的对象");synchronized (lock) {// lock.notify(); // 随机唤醒一个lock.notifyAll(); // 唤醒全部}
}
4.6.1 sleep() 和 wait() 的区别
- sleep() 是 Thread 的方法,wait() 是 Object 类的方法
- sleep() 不需要强制和
synchronized
配合使用,wait() 必须和synchronized
配合使用 - sleep() 不会释放锁,wait() 会释放锁
共同点:
- 状态都是 WATING 和 TIMED_WATING
4.6.2 最佳使用方法
为什么用 while 不用 if ?:
- 如果一个线程被唤醒,但是还不满足条件,此时用 if 的话就会出错
synchronized (lock){while (条件判断不成立) {lock.wait();}// 执行代码
}// 唤醒线程
synchronized (lock){lock.notifyAll();
}
4.7 多线程设计模式
4.7.1 保护性暂停
Guarded Suspension,用于一个线程等待另外一个线程的执行结果:
借鉴 join()
的源码实现:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {GuardedObject guardedObject = new GuardedObject();// 等待结果new Thread(() -> {log.debug("{}", guardedObject.get(3000));}, "t1").start();// 通知 t1new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}guardedObject.complete("To: t1, From: t2");}, "t2").start();}
}class GuardedObject {// 结果private Object response;// 获取结果public Object get(long millis) {synchronized (this) {final long startTime = System.nanoTime();long delay = millis;do {try {wait(delay);} catch (InterruptedException e) {throw new RuntimeException(e);}} while (response == null && (delay = millis - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);// 细节:每次需要等待的时间 = 总等待时间 - (当前时间 - 起始时间)}}// 产生结果public void complete(Object response) {synchronized (this) {this.response = response;this.notifyAll();}}
}
- JDK 中,join() 的实现、Future 的实现采用的就是该模式
join()
的源码参考:
public final synchronized void join(final long millis)throws InterruptedException {if (millis > 0) {if (isAlive()) {final long startTime = System.nanoTime();long delay = millis;do {wait(delay);} while (isAlive() && (delay = millis -TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);}} else if (millis == 0) {while (isAlive()) {wait(0);}} else {throw new IllegalArgumentException("timeout value is negative");}}
扩展
在多个类中使用 GuardedObject 需要定义多个,很不方便,因此可以设置一个队列解耦,并且支持多个任务的管理:
实现:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {// 启动三个收信人for (int i = 0; i < 3; i++) {new People().start();}TimeUnit.SECONDS.sleep(1);// 启动三个邮差,根据收件人的信箱 id 送信Mailboxes.getIds().forEach(id -> {new Postman(id, "内容" + id).start();});}
}@Slf4j(topic = "c.People")
class People extends Thread{@Overridepublic void run() {// 收信GuardedObject guardedObject = Mailboxes.createGuardedObject();try {log.debug("开始收信, 信箱 id:{}", guardedObject.getId());Object response = guardedObject.get(5000);log.debug("收信 Id: {}, 内容: {}", guardedObject.getId(), response);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}@Slf4j(topic = "c.Postman")
class Postman extends Thread{private final int id;private final String mail;public Postman(int id, String mail) {this.id = id;this.mail = mail;}@Overridepublic void run() {GuardedObject guardedObject = Mailboxes.getGuardedObject(id);log.debug("送信箱 id:{}, 内容: {}", guardedObject.getId(), mail);guardedObject.complete(mail);}
}abstract class Mailboxes {private static int id = 1;private static final Map<Integer, GuardedObject> boxes = new ConcurrentHashMap<>();// 产生唯一 idpublic static synchronized int generateId() {return id++;}// 根据 id 获取 GuardedObject 对象public static GuardedObject getGuardedObject(int id) {return boxes.remove(id);}// 产生 GuardedObject 对象public static GuardedObject createGuardedObject() {GuardedObject go = new GuardedObject(generateId());boxes.put(go.getId(), go);return go;}public static Set<Integer> getIds() {return boxes.keySet();}
}class GuardedObject {// 设置唯一标识private final int id;// 结果private Object response;public GuardedObject(int id) {this.id = id;}// 获取结果public Object get(long millis) throws InterruptedException {synchronized (this) {final long startTime = System.nanoTime();long delay = millis;do {wait(delay);} while (response == null && (delay = millis - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);}// 直到有结果的时候返回结果, 否则一直 waitreturn response;}// 产生结果public void complete(Object response) {synchronized (this) {this.response = response;this.notifyAll();}}public int getId() {return this.id;}
}
4.7.2 生产者/消费者(异步)
- 不需要生产结果和消费结果的线程一一对应
- 消息队列是有容量限制的队列,空时不会再消耗数据
- JDK 中的各种阻塞队列,采用的就是该模式
实现:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) throws InterruptedException {MessageQueue queue = new MessageQueue(10);for (int i = 0; i < 3; i++) {int id = i;new Thread(() -> {try {queue.put(new Message(id, "值" + id));} catch (InterruptedException e) {throw new RuntimeException(e);}}, "生产者" + i).start();}new Thread(() -> {try {while (true) {TimeUnit.SECONDS.sleep(1);Message take = queue.take();}} catch (InterruptedException e) {throw new RuntimeException(e);}}, "消费者").start();}
}@Slf4j(topic = "c.MessageQueue")
class MessageQueue {// 消息队列集合private final LinkedList<Message> list = new LinkedList<>();// 容量private final int capacity;public MessageQueue(int capacity) {this.capacity = capacity;}// 获取消息public Message take() throws InterruptedException {// 检查对象是否为空synchronized (list) {while (list.isEmpty()) {log.debug("队列空...消费者请等待");list.wait();}Message msg = list.removeFirst();log.debug("取出消息 :" + msg);list.notifyAll();return msg;}}// 存入消息public void put(Message msg) throws InterruptedException {synchronized (list) {// 检查对象是否满while (list.size() == capacity) {log.debug("队列满...生产者请等待");list.wait();}log.debug("存入消息 :" + msg);list.addLast(msg);list.notifyAll();}}
}record Message(int id, Object value) {}
4.8 park() / unpark()
它们是 LockSupport
类中的方法:
park()
:暂停当前线程unpark()
:恢复某个线程的运行,(多次调用只执行一次)unpark()
可以在park()
之前调用,且有效
而且 unpark()
可以以线程为单位进行唤醒,更加精确!
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("start...");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("park");LockSupport.park();log.debug("resume...");});t1.start();TimeUnit.SECONDS.sleep(2);log.debug("unpark");LockSupport.unpark(t1);
}
unpark()
早于 park()
的情况:
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("start...");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("park");LockSupport.park();log.debug("resume...");});t1.start();TimeUnit.SECONDS.sleep(1);log.debug("unpark");LockSupport.unpark(t1);
}
4.9 死锁
t1 线程获得 A 锁,又想去获得 B 锁;
t2 线程获得 B 锁,又想去获得 A 锁;
这样就导致了死锁。
定位死锁
jps
, jstack <pid>
会看到每个线程的状态,并且死锁的位置会被列出
哲学家就餐问题
- 五个哲学家只做两件事情,思考和吃饭
- 吃饭要用两根筷子吃,桌子上只有 5 根筷子,每位哲学家左右手各有一根筷子
- 筷子被身边人拿到,自己就得等待
这样等一会就有可能发生死锁:
@Slf4j(topic = "c.InitTest")
public class InitTest {public static void main(String[] args) {Chopstick c1 = new Chopstick("c1");Chopstick c2 = new Chopstick("c2");Chopstick c3 = new Chopstick("c3");Chopstick c4 = new Chopstick("c4");Chopstick c5 = new Chopstick("c5");new Philosopher("张三", c1, c2).start();new Philosopher("尼古拉斯·赵四", c2, c3).start();new Philosopher("王二麻子", c3, c4).start();new Philosopher("职业法师·刘海柱", c4, c5).start();new Philosopher("IKUN", c5, c1).start();}
}@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {private final Chopstick left;private final Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}private void eat() {log.debug("eating...");try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}}@Overridepublic void run() {while (true) {synchronized (left) {synchronized (right) {eat();}}}}
}record Chopstick(String name) {}
五个线程都进入死锁状态了。
活锁
活锁出现在两个线程相互改变对方的结束条件,最后两个线程谁也无法结束:
@Slf4j(topic = "c.InitTest")
public class InitTest {static final Object lock = new Object();static volatile int count = 10;public static void main(String[] args) {new Thread(() -> {while (count > 0) {try {TimeUnit.MILLISECONDS.sleep(1);count--;log.debug("count: {}", count);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "t1").start();new Thread(() -> {while (count < 20) {try {TimeUnit.MILLISECONDS.sleep(1);count++;log.debug("count: {}", count);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "t2").start();}
}
两个线程谁都无法结束。
饥饿
一个线程优先级太低,导致一直得不到 CPU 的调度执行,也不能结束。
哲学家就餐问题修改条件,把:
new Philosopher("IKUN", c5, c1).start();
改为
new Philosopher("IKUN", c1, c5).start();
其他进程一直得不到执行,其他线程饥饿。
JUC P1 进程,线程,管程 基础+代码相关推荐
- Linux的进程/线程/协程系列4:进程知识深入总结:上篇
Linux的进程/线程/协程系列4:进程/线程相关知识总结 前言 本篇摘要: 1. 进程基础知识 1.1 串行/并行与并发 1.2 临界资源与共享资源 1.3 同步/异步与互斥 1.4 进程控制原语 ...
- linux的进程/线程/协程系列5:协程的发展复兴与实现现状
协程的发展复兴与实现现状 前言 本篇摘要: 1. 协同制的发展史 1.1 协同工作制的提出 1.2 自顶向下,无需协同 1.3 协同式思想的应用 2. 协程的复兴 2.1 高并发带来的问题 2.2 制 ...
- linux的进程/线程/协程系列3:查看linux内核源码——vim+ctags/find+grep
linux的进程/线程/协程系列3:查看linux内核源码--vim+ctags/find+grep 前言 摘要: 1. 下载linux内核源码 2. 打标签方法:vim+ctags 2.1 安装vi ...
- linux的进程/线程/协程系列1:进程到协程的演化
linux的进程/线程/协程系列1:进程到协程的演化 前言 摘要: 1. 一些历史:批处理时代 2. 现代操作系统启动过程 3. 进程(process)的出现 4. 线程(thread)与线程池 5. ...
- 简要说明__python3中的进程/线程/协程
多任务可以充分利用系统资源,极大提升程序运行效率,多任务的实现往往与 多线程,多进程,多协程有关 稳定性: 进程 > 线程 > 协程 系统资源占用量:进程 > 线程 > 协程 ...
- Python之进程+线程+协程(异步、selectors模块、阻塞、非阻塞IO)
文章目录 一.IO多路复用 二.selectors模块 本篇文字是关于IO多路复用的更深入一步的总结,上一篇 Python之进程+线程+协程(事件驱动模型.IO多路复用.select与epoll)对I ...
- 进程 线程 协程 各自的概念以及三者的对比分析
文章目录 1 进程 2 线程 3 进程和线程的区别和联系 3.1 区别 3.2 联系 4 举例说明进程和线程的区别 5 进程/线程之间的亲缘性 6 协程 线程(执行一个函数)和协程的区别和联系 协程和 ...
- java基础巩固-宇宙第一AiYWM:为了维持生计,四大基础之OS_Part_1整起(进程线程协程并发并行、进程线程切换进程间通信、死锁\进程调度策略、分段分页、交换空间、OS三大调度机制)
PART0:OS,这货到底是个啥? OS,是个啥? OS的结构们: 存储器: 存储器的层次结构: 内存:我们的程序和数据都是存储在内存,我们的程序和数据都是存储在内存,每一个字节都对应一个内存地址.内 ...
- 4.19 python 网络编程和操作系统部分(TCP/UDP/操作系统概念/进程/线程/协程) 学习笔记
文章目录 1 网络编程概念 1)基本概念 2)应用-最简单的网络通信 2 TCP协议和UDP协议进阶(网络编程) 1)TCP协议和UDP协议基于socket模块实现 2)粘包现象 3)文件上传和下载代 ...
最新文章
- hdu 1286 找新朋友 (容斥原理 || 欧拉函数)
- 服务器存档修改,云服务器存档修改器
- numpy 归一化_NumPy 数据归一化、可视化
- 历史上的今天:首条海底光缆开通;VeriSign 收购 Network Solutions;计算机图形学先驱诞生...
- css3弧形跑道效果_【Tableau 图表】你是不是真的需要一个跑道图呢?
- 第十周Java学习总结
- ad中pcb双面板怎么设置_html中表格tr的td单元格怎么设置宽度属性
- php函数获取数据库中的表格,初步了解PHP获取数据库表信息函数_PHP教程
- python rsi_使用python与rsi进行算法交易
- Java仓储物流项目_基于jsp的物流仓库管理系统-JavaEE实现物流仓库管理系统 - java项目源码...
- 计算机体系结构实验1——计算机性能评测
- 【工具类】数据脱敏工具类
- 键盘上特殊符号的中英文名称
- 电商时代得流量者得天下,思域流量要怎么做
- 学生个人网页设计作品 学生个人网页模板简单个人主页成品 个人网页制作 HTML学生个人网站作业设计
- Acwing:COW(DP+状态机 Python)
- @Before, @BeforeClass, @BeforeEach 和 @BeforeAll之间的不同
- 4.Python复杂数据类型之字典
- 前端——》手机H5页面九宫格抽奖(含概率及奖品配置)
- js 根据时间戳格式化为24小时的日期形式