并发编程juc

2021年08月16日 阅读数:392
这篇文章主要向大家介绍并发编程juc,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

工做之余充电:https://www.bilibili.com/video/BV16J411h7Rd?p=1html

 笔记待更新前端

 

 

 

 

 

 

1.4 预备知识

 
但愿你不是一个初学者
线程安全问题,须要你接触过 Java Web 开发、Jdbc 开发、Web 服务器、分布式框架时才会遇到
基于 JDK 8,最好对函数式编程、lambda 有必定了解
采用了 slf4j 打印日志,这是好的实践
采用了 lombok 简化 java bean 编写
给每一个线程好名字,这也是一项好的实践
 
pom.xml 依赖以下
 
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
 

logback.xml 配置以下

<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
</encoder>
</appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
 

2. 进程与线程

本章内容
 
  • 进程和线程的概念
  • 并行和并发的概念
  • 线程基本应用
 
 

2.1 进程与线程

进程
  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程当中还须要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就能够视为程序的一个实例。大部分程序能够同时运行多个实例进程(例如记事本、画图、浏览器
  • 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
  • 一个进程以内能够分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以必定的顺序交给 CPU 执行
  • Java 中,线程做为最小调度单位,进程做为资源分配的最小单位。 在 windows 中进程是不活动的,只是做为线程的容器

 

两者对比

 
  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通讯较为复杂
    同一台计算机的进程通讯称为 IPC(Inter-process communication)
    不一样计算机之间的进程通讯,须要经过网络,并遵照共同的协议,例如 HTTP
  • 线程通讯相对简单,由于它们共享进程内的内存,一个例子是多个线程能够访问同一个共享变量
  • 线程更轻量,线程上下文切换成本通常上要比进程上下文切换低
 

2.2 并行与并发

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

 

 

 

 

多核 cpu下,每一个 核(core) 均可以调度运行线程,这时候线程能够是并行的。

 

 

 

 

引用 Rob Pike 的一段描述:
  •   并发(concurrent)是同一时间应对(dealing with)多件事情的能力
  •   并行(parallel)是同一时间动手作(doing)多件事情的能力
例子
  •   家庭主妇作饭、打扫卫生、给孩子喂奶,她一我的轮流交替作这多件事,这时就是并发
  •   家庭主妇雇了个保姆,她们一块儿这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一
     我的用锅时,另外一我的就得等待)
  •   雇了3个保姆,一个专作饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
Rob Pike 资料
 
golang 语言的创造者
Rob Pike - 百度百科
 

2.3 应用

 
* 应用之异步调用(案例1)
以调用方角度来说,若是
  •   须要等待结果返回,才能继续运行就是同步
  •   不须要等待结果返回,就能继续运行就是异步
1) 设计
多线程可让方法执行变为异步的(即不要巴巴干等着)好比说读取磁盘文件时,假设读取操做花费了 5 秒钟,如
果没有线程调度机制,这 5 秒 cpu 什么都作不了,其它代码都得暂停...
 
2) 结论
  • 好比在项目中,视频文件须要转换格式等操做比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步 servlet 也是相似的目的,让用户线程处理耗时较长的操做,避免阻塞 tomcat 的工做线程
  • ui 程序中,开线程进行其余操做,避免阻塞 ui 线程
* 应用之提升效率(案例1)
 
 
充分利用多核 cpu 的优点,提升运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。
计算 1 花费 10 ms
计算 2 花费 11 ms
计算 3 花费 9 ms
汇总须要 1 ms
  • 若是是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
  • 但若是是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个
    线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms
注意
须要在多核 cpu 才能提升效率,单核仍然时是轮流执行
 
1) 设计
  >>>>> 代码见【应用之效率-案例1】<<<<<
 
2) 结论
1. 单核 cpu 下,多线程不能实际提升程序运行效率,只是为了可以在不一样的任务之间切换,不一样线程轮流使用
  cpu ,不至于一个线程总占用 cpu,别的线程无法干活
2. 多核 cpu 能够并行跑多个线程,但可否提升程序运行效率仍是要分状况的
  •   有些任务,通过精心设计,将任务拆分,并行执行,固然能够提升程序的运行效率。但不是全部计算任务都能拆分(参考后文的【阿姆达尔定律】)
  •   也不是全部任务都须要拆分,任务的目的若是不一样,谈拆分和效率没啥意义
3. IO 操做不占用 cpu,只是咱们通常拷贝文件使用的是【阻塞 IO】,这时至关于线程虽然不用 cpu,但须要一
  直等待 IO 结束,没能充分利用线程。因此才有后面的【非阻塞 IO】和【异步 IO】优化
 

3. Java 线程

 
本章内容
  • 建立和运行线程
  • 查看线程
  • 线程 API
  • 线程状态

3.1 建立和运行线程

   方法一,直接使用 Thread
// 建立线程对象
Thread t = new Thread() {
 public void run() {
 // 要执行的任务
 }
};
// 启动线程
t.start();
例如:
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
 log.debug("hello");
 }
};
t1.start();
输出
19:19:00 [t1] c.ThreadStarter - hello

 

方法二,使用 Runnable 配合 Threadjava

 

把【线程】和【任务】(要执行的代码)分开
  • Thread 表明线程
  • Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
 public void run(){
 // 要执行的任务
 }
};
// 建立线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
例如:
// 建立任务对象
Runnable task2 = new Runnable() {
 @Override
 public void run() {
 log.debug("hello");
 }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
输出
19:19:00 [t2] c.ThreadStarter - hello
Java 8 之后能够使用 lambda 精简代码
// 建立任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

 

 

* 原理之 Thread 与 Runnable 的关系

分析 Thread 的源码,理清它与 Runnable 的关系
 
小结
  • 方法1 是把线程和任务合并在了一块儿,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三,FutureTask 配合 Thread
 
FutureTask 可以接收 Callable 类型的参数,用来处理有返回结果的状况
// 建立任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
输出
19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100
 

3.2 观察多个线程同时运行

 
主要是理解
  • 交替执行
  • 谁先谁后,不禁咱们控制

 

3.3 查看进程线程的方法

windows
  • 任务管理器能够查看进程和线程数,也能够用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程
linux
  • ps -fe 查看全部进程
  • ps -fT -p <PID> 查看某个进程(PID)的全部线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的全部线程
Java
  • jps 命令查看全部 Java 进程
  • jstack <PID> 查看某个 Java 进程(PID)的全部线程状态
  • jconsole 来查看某个 Java 进程中线程的运行状况(图形界面)
jconsole 远程监控配置
  • 须要以以下方式运行你的 java 类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`链接端口` -Dcom.sun.management.jmxremote.ssl=是否安全链接 -
Dcom.sun.management.jmxremote.authenticate=是否定证 java类

 

  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名若是要认证访问,还须要作以下步骤
  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件全部者可读写
  • 链接时填入 controlRole(用户名),R&D(密码)

3.4 * 原理之线程运行

栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
 
咱们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每一个线程启动后,虚拟
机就会为其分配一块栈内存。
  •   每一个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  •   每一个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
 
线程上下文切换(Thread Context Switch)
由于如下一些缘由致使 cpu 再也不执行当前的线程,转而执行另外一个线程的代码
  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程须要运行
  • 线程本身调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,须要由操做系统保存当前线程的状态,并恢复另外一个线程的状态,Java 中对应的概念
就是程序计数器(Program Counter Register),它的做用是记住下一条 jvm 指令的执行地址,是线程私有的
  • 状态包括程序计数器、虚拟机栈中每一个栈帧的信息,如局部变量、操做数栈、返回地址等
  • Context Switch 频繁发生会影响性能

3.5 常见方法

 

 

 

 

3.6 start 与 run

调用 run
public static void main(String[] args) {
 Thread t1 = new Thread("t1") {
 @Override
 public void run() {
 log.debug(Thread.currentThread().getName());
 FileReader.read(Constants.MP4_FULL_PATH);
 }
 };
 t1.run();
 log.debug("do other things ...");
}
输出
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
程序仍在 main 线程运行, FileReader.read() 方法调用仍是同步的
 
调用 start
将上述代码的 t1.run() 改成
t1.start();
输出
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
 
小结
  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,经过新的线程间接执行 run 中的代码

3.7 sleep 与 yield

sleep
  • 1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • 2. 其它线程能够使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 3. 睡眠结束后的线程未必会马上获得执行
  • 4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来得到更好的可读性
yield
  • 1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,而后调度执行其它线程
  • 2. 具体的实现依赖于操做系统的任务调度器
线程优先级
  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器能够忽略它
  • 若是 cpu 比较忙,那么优先级高的线程会得到更多的时间片,但 cpu 闲时,优先级几乎没做用
Runnable task1 = () -> {
 int count = 0;
 for (;;) {
 System.out.println("---->1 " + count++);
 }
};
Runnable task2 = () -> {
 int count = 0;
 for (;;) {
 // Thread.yield();
 System.out.println(" ---->2 " + count++);
 }
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
 * 应用之效率(案例2)
  

3.8 join 方法详解

为何须要 join
下面的代码执行,打印 r 是什么?
 
static int r = 0;
public static void main(String[] args) throws InterruptedException {
 test1();
}
private static void test1() throws InterruptedException {
 log.debug("开始");
 Thread t1 = new Thread(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 r = 10;
 });
 t1.start();
 log.debug("结果为:{}", r);
 log.debug("结束");
}

 

分析
  • 由于主线程和线程 t1 是并行执行的,t1 线程须要 1 秒以后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,因此只能打印出 r=0
解决方法
  • 用 sleep 行不行?为何?
  • 用 join,加在 t1.start() 以后便可

 

* 应用之同步(案例1)
 
以调用方角度来说,若是
  • 须要等待结果返回,才能继续运行就是同步
  • 不须要等待结果返回,就能继续运行就是异步

等待多个结果
 
问,下面代码 cost 大约多少秒?
 
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
 test2();
}
private static void test2() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 sleep(1);
 r1 = 10;
 });
 Thread t2 = new Thread(() -> {
 sleep(2);
 r2 = 20;
});
 long start = System.currentTimeMillis();
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

 

分析以下
  • 第一个 join:等待 t1 时, t2 并无中止, 而在运行
  • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 所以也只需再等待 1s
若是颠倒两个 join 呢?
最终都是输出
20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005

 

 

有时效的 join
等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
 test3();
}
public static void test3() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 sleep(1);
 r1 = 10;
 });
 long start = System.currentTimeMillis();
 t1.start();
 // 线程执行结束会致使 join 结束
 t1.join(1500);
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出
20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010
没等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
 test3();
}
public static void test3() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 sleep(2);
 r1 = 10;
 });
 long start = System.currentTimeMillis();
 t1.start();
 // 线程执行结束会致使 join 结束
 t1.join(1500);
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出
20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502

3.9 interrupt 方法详解

打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
private static void test1() throws InterruptedException {
 Thread t1 = new Thread(()->{
 sleep(1);
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
 log.debug(" 打断状态: {}", t1.isInterrupted());
}
输出
java.lang.InterruptedException: sleep interrupted
 at java.lang.Thread.sleep(Native Method)
 at java.lang.Thread.sleep(Thread.java:340)
 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
 at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
 at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
 at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false
打断正常运行的线程
打断正常运行的线程, 不会清空打断状态
private static void test2() throws InterruptedException {
 Thread t2 = new Thread(()->{
 while(true) {
 Thread current = Thread.currentThread();
 boolean interrupted = current.isInterrupted();
 if(interrupted) {
 log.debug(" 打断状态: {}", interrupted);
 break;
 }
 }
 }, "t2");
 t2.start();
 sleep(0.5);
 t2.interrupt();
}
输出
20:57:37.964 [t2] c.TestInterrupt - 打断状态: true
* 模式之两阶段终止
 

  Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另外一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。node

  以下所示:那么线程的isInterrupted()方法能够取得线程的打断标记,若是线程在睡眠sleep期间被打断,打断标记是不会变的,为false,可是sleep期间被打断会抛出异常,咱们据此手动设置打断标记为true;若是是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种状况那咱们就能够放心地来料理后事啦!linux

 

 代码实现以下:ios

@Slf4j
public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        TwoParseTermination twoParseTermination = new TwoParseTermination();
        twoParseTermination.start();
        Thread.sleep(3000);  // 让监控线程执行一下子
        twoParseTermination.stop(); // 中止监控线程
    }
}


@Slf4j
class TwoParseTermination{
    Thread thread ;
    public void start(){
        thread = new Thread(()->{
            while(true){
                if (Thread.currentThread().isInterrupted()){
                    log.debug("线程结束。。正在料理后事中");
                    break;
                }
                try {
                    Thread.sleep(500);
                    log.debug("正在执行监控的功能");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
    public void stop(){
        thread.interrupt();
    }
}
打断 park 线程
打断 park 线程, 不会清空打断状态
private static void test3() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 log.debug("park...");
 LockSupport.park();
 log.debug("unpark...");
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
}
输出
21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true
若是打断标记已是 true, 则 park 会失效
private static void test4() {
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 5; i++) {
 log.debug("park...");
 LockSupport.park();
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }
 });
 t1.start();
 sleep(1);
 t1.interrupt();
}
输出
21:13:48.783 [Thread-0] c.TestInterrupt - park... 
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.812 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
提示
能够使用 Thread.interrupted() 清除打断状态
 

3.10 不推荐的方法

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

 

 

3.11 主线程与守护线程

默认状况下,Java 进程须要等待全部线程都运行结束,才会结束。有一种特殊的线程叫作守护线程,只要其它非守
护线程运行结束了,即便守护线程的代码没有执行完,也会强制结束。
例:
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
 log.debug("开始运行...");
 sleep(2);
 log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
输出
08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束...
注意
  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,因此 Tomcat 接收到 shutdown 命令后,不会等
    待它们处理完当前请求
 

3.12 五种状态

这是从 操做系统 层面来描述的

 

 

  • 【初始状态】仅是在语言层面建立了线程对象,还未与操做系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被建立(与操做系统线程关联),能够由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会致使线程的上下文切换

【阻塞状态】git

  • 若是调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会致使线程上下文切换,进入【阻塞状态】
  • 等 BIO 操做完毕,会由操做系统唤醒阻塞的线程,转换至【可运行状态】
  • 与【可运行状态】的区别是,对【阻塞状态】的线程来讲只要它们一直不唤醒,调度器就一直不会考虑调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

3.13 六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态

 

 

  • NEW 线程刚被建立,可是尚未调用 start() 方法
  • RUNNABLE 当调用了 start() 方法以后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操做系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(因为 BIO 致使的线程阻塞,在 Java 里没法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED 当线程代码运行结束

3.14 习题

阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示
参考图二,用两个线程(两我的协做)模拟烧水泡茶过程
文中办法乙、丙都至关于任务串行
而图一至关于启动了 4 个线程,有点浪费
用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间
附:华罗庚《统筹方法》

 

统筹方法,是一种安排工做进程的数学方法。它的实用范围极普遍,在企业管理和基本建设中,以及关系复
杂的科研项目的组织与管理中,均可以应用。
怎样应用呢?主要是把工序安排好
好比,想泡壶茶喝。当时的状况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么
办?
办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
办法乙:先作好一些准备工做,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了以后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
哪种办法省时间?咱们能一眼看出,第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,能够引出生产管理等方面有用的方法来。
水壶不洗,不能烧开水,于是洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,于是
这些又是泡茶的前提。它们的相互关系,能够用下边的箭头图来表示:

 

 

从这个图上能够一眼看出,办法甲总共要16分钟(而办法乙、丙须要20分钟)。若是要缩短工时、提升工做
效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大
可利用“等水开”的时间来作。
 
是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但
稍有变化,临事而迷的状况,经常是存在的。在近代工业的错综复杂的工艺过程当中,每每就不是像泡茶喝这
么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,每每出现“万事俱
备,只欠东风”的状况。因为一两个零件没完成,耽误了一台复杂机器的出厂时间。或每每由于抓的不是关
键,连夜三班,急急忙忙,完成这一环节以后,还得等待旁的环节才能装配。
 
洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,并且同是一我的的活儿,于是能够合并成为:

 

 

看来这是“小题大作”,但在工做环节太多的时候,这样作就很是必要了。
这里讲的主要是时间方面的事,但在具体生产实践中,还有其余方面的许多事。这种方法虽然不必定能直接
解决全部问题,可是,咱们利用这种方法来考虑问题,也是不无裨益的。
 
 
* 应用之统筹(烧水泡茶)
 

本章小结

本章的重点在于掌握
  • 线程建立
  • 线程重要 api,如 start,run,sleep,join,interrupt 等
  • 线程状态
  • 应用方面
  1. 异步调用:主线程执行期间,其它线程异步执行耗时操做
  2. 提升效率:并行计算,缩短运算时间
  3. 同步等待:join
  4. 统筹规划:合理使用线程,获得最优效果
原理方面
  • 线程运行流程:栈、栈帧、上下文切换、程序计数器
  • Thread 两种建立方式 的源码
模式方面
  • 终止模式之两阶段终止
 

4. 共享模型之管程

本章内容
共享问题
synchronized
线程安全分析
Monitor
wait/notify
线程状态转换
活跃性
Lock
 

4.1 共享带来的问题

小故事
老王(操做系统)有一个功能强大的算盘(CPU),如今想把它租出去,赚一点外快

 

 

小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
但小南不能一天24小时使用算盘,他常常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操做),有
时还须要一根烟,没烟时思路全无(wait)这些状况统称为(阻塞)
 

 

 

  • 在这些时候,算盘没利用起来(不能收钱了),老王以为有点不划算
  • 另外,小女也想用用算盘,若是老是小南占着算盘,让小女以为不公平
  • 因而,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
  • 这样,当小南阻塞的时候,算盘能够分给小女使用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,须要存储一些中间结果,而学生们的脑容量(工做内存)不够,因此老王申请了一个笔记本(主存),把一些中间结果先记在本上
  • 计算流程是这样的

 

 

  • 可是因为分时系统,有一天仍是发生了事故
  • 小南刚读取了初始值 0 作了个 +1 运算,还没来得及写回结果
  • 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],因而小南念叨着 [ 结果是1,结果是1...] 不甘心地
  • 到一边待着去了(上下文切换)
  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 作了一个 -1 运算,将结果 -1 写入笔记本
  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写
  • 入了笔记本

 

小南和小女都以为本身没作错,但笔记本里的结果是 1 而不是 0
 
Java 的体现
两个线程对初始值为 0 的静态变量一个作自增,一个作自减,各作 5000 次,结果是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 5000; i++) {
 counter++;
 }
 }, "t1");
 Thread t2 = new Thread(() -> {
 for (int i = 0; i < 5000; i++) {
 counter--;
 }
 }, "t2");
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 log.debug("{}",counter);
}
问题分析
以上的结果多是正数、负数、零。为何呢?由于 Java 中对静态变量的自增,自减并非原子操做,要完全理
解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生以下的 JVM 字节码指令:

 

 

 

 

 

 

 

 

 

 

临界区 Critical Section
  • 一个程序运行多个线程自己是没有问题的
  • 问题出在多个线程访问共享资源
  1. 多个线程读共享资源其实也没有问题
  2. 在多个线程对共享资源读写操做时发生指令交错,就会出现问题
  • 一段代码块内若是存在对共享资源的多线程读写操做,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++; }
static void decrement() 
// 临界区
{ 
 counter--; }
竞态条件 Race Condition
多个线程在临界区内执行,因为代码的执行序列不一样而致使结果没法预测,称之为发生了竞态条件

4.2 synchronized 解决方案

* 应用之互斥
为了不临界区的竞态条件发生,有多种手段能够达到目的。
  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
的线程能够安全的执行临界区内的代码,不用担忧线程上下文切换
 
 
注意
虽然 java 中互斥和同步均可以采用 synchronized 关键字来完成,但它们仍是有区别的:
  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是因为线程执行的前后、顺序不一样、须要一个线程等待其它线程运行到某个点
synchronized
 
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
 临界区
}
解决
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 5000; i++) {
 synchronized (room) {
 counter++;
 }
 }
 }, "t1");
 Thread t2 = new Thread(() -> {
 for (int i = 0; i < 5000; i++) {
 synchronized (room) {
 counter--;
 }
 }
 }, "t2");
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 log.debug("{}",counter);
}

 

 

你能够作这样的类比:
  • synchronized(对象) 中的对象,能够想象为一个房间(room),有惟一入口(门)房间只能一次进入一人
  • 进行计算,线程 t1,t2 想象成两我的
  • 当线程 t1 执行到 synchronized(room) 时就比如 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行
  • count++ 代码
  • 这时候若是 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切
  • 换,阻塞住了
  • 这中间即便 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),
  • 这时门仍是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 本身再次得到时间片时才
  • 能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥
  • 匙给他。t2 线程这时才能够进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
用图来表示

 

 

思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断。
为了加深理解,请思考下面的问题
 
  • 若是把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
  • 若是 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运做?-- 锁对象
  • 若是 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象

 

面向对象改进
把须要保护的共享变量放入一个类
class Room {
 int value = 0;
 public void increment() {
 synchronized (this) {
 value++;
 }
 }
 public void decrement() {
 synchronized (this) {
 value--;
 }
 }
 public int get() {
 synchronized (this) {
 return value;
 }
 }
}
@Slf4j
public class Test1 {
 
 public static void main(String[] args) throws InterruptedException {
 Room room = new Room();
 Thread t1 = new Thread(() -> {
 for (int j = 0; j < 5000; j++) {
 room.increment();
}
 }, "t1");
 Thread t2 = new Thread(() -> {
 for (int j = 0; j < 5000; j++) {
 room.decrement();
 }
 }, "t2");
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 log.debug("count: {}" , room.get());
 }
}

4.3 方法上的 synchronized

class Test{
 public synchronized void test() {
 
 }
}
等价于
class Test{
 public void test() {
 synchronized(this) {
 
 }
 }
}
class Test{
 public synchronized static void test() {
 }
}
等价于
class Test{
 public static void test() {
 synchronized(Test.class) {
 
 }
 }
}
不加 synchronized 的方法
不加 synchronzied 的方法就比如不遵照规则的人,不去老实排队(比如翻窗户进去的)
 
所谓的“线程八锁”
 
其实就是考察 synchronized 锁住的是哪一个对象
状况1:12 或 21
 
@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

 

状况2:1s后12,或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}
状况3:3 1s 12 或 23 1s 1 或 32 1s 1
@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
 public void c() {
 log.debug("3");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
 new Thread(()->{ n1.c(); }).start();
}
状况4:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}
状况5:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}
状况6:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public static synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}
状况7:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}
状况8:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public static synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}

 

4.4 变量的线程安全分析

成员变量和静态变量是否线程安全
  • 若是它们没有共享,则线程安全
  • 若是它们被共享了,根据它们的状态是否可以改变,又分两种状况
  1. 若是只有读操做,则线程安全
  2. 若是有读写操做,则这段代码是临界区,须要考虑线程安全
局部变量是否线程安全?
  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
  1. 若是该对象没有逃离方法的做用访问,它是线程安全的
  2. 若是该对象逃离方法的做用范围,须要考虑线程安全
局部变量线程安全分析
public static void test1() {
 int i = 10;
 i++; }
每一个线程调用 test1() 方法时局部变量 i,会在每一个线程的栈帧内存中被建立多份,所以不存在共享
public static void test1();
 descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I
如图

 

 

局部变量的引用稍有不一样
先看一个成员变量的例子
class ThreadUnsafe {
 ArrayList<String> list = new ArrayList<>();
 public void method1(int loopNumber) {
 for (int i = 0; i < loopNumber; i++) {
 // { 临界区, 会产生竞态条件
 method2();
 method3();
// } 临界区
 }
 }
 private void method2() {
 list.add("1");
 }
 private void method3() {
 list.remove(0);
 }
}

 

执行
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
 ThreadUnsafe test = new ThreadUnsafe();
 for (int i = 0; i < THREAD_NUMBER; i++) {
 new Thread(() -> {
 test.method1(LOOP_NUMBER);
 }, "Thread" + i).start();
 }
}
其中一种状况是,若是线程2 还未 add,线程1 remove 就会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748)
分析:
不管哪一个线程中的 method2 引用的都是同一个对象中的 list 成员变量
method3 与 method2 分析相同

 

 

将 list 修改成局部变量
class ThreadSafe {
 public final void method1(int loopNumber) {
 ArrayList<String> list = new ArrayList<>();
 for (int i = 0; i < loopNumber; i++) {
 method2(list);
 method3(list);
 }
 }
 private void method2(ArrayList<String> list) {
 list.add("1");
 }
 private void method3(ArrayList<String> list) {
 list.remove(0);
 }
}
那么就不会有上述问题了
分析:
list 是局部变量,每一个线程调用时会建立其不一样实例,没有共享
而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
method3 的参数分析与 method2 相同

 

 

方法访问修饰符带来的思考,若是把 method2 和 method3 的方法修改成 public 会不会代理线程安全问题?
  • 状况1:有其它线程调用 method2 和 method3
  • 状况2:在 状况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafe {
 public final void method1(int loopNumber) {
 ArrayList<String> list = new ArrayList<>();
 for (int i = 0; i < loopNumber; i++) {
 method2(list);
 method3(list);
 }
 }
 private void method2(ArrayList<String> list) {
 list.add("1");
}
 private void method3(ArrayList<String> list) {
 list.remove(0);
 }
}
class ThreadSafeSubClass extends ThreadSafe{
 @Override
 public void method3(ArrayList<String> list) {
 new Thread(() -> {
 list.remove(0);
 }).start();
 }
}
从这个例子能够看出 private 或 fifinal 提供【安全】的意义所在,请体会开闭原则中的【闭】
常见线程安全类
  • String
  • Integer
  • StringBuffffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也能够理解为
 
Hashtable table = new Hashtable();
new Thread(()->{
 table.put("key", "value1");
}).start();
new Thread(()->{
 table.put("key", "value2");
}).start();
它们的每一个方法是原子的
但注意它们多个方法的组合不是原子的,见后面分析
 
线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

 

 

 

不可变类线程安全性
String、Integer 等都是不可变类,由于其内部的状态不能够改变,所以它们的方法都是线程安全的
有同窗或许有疑问,String 有 replace,substring 等方法【能够】改变值啊,那么这些方法又是如何保证线程安
全的呢?
public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
}
若是想增长一个增长的方法呢?
public class Immutable{
 private int value = 0;
 public Immutable(int value){
 this.value = value;
 }
 public int getValue(){
 return this.value;
 }
 
 public Immutable add(int v){
 return new Immutable(this.value + v);
 } 
}
实例分析
例1:
public class MyServlet extends HttpServlet {
 // 是否安全?
 Map<String,Object> map = new HashMap<>();
 // 是否安全?
 String S1 = "...";
 // 是否安全?
 final String S2 = "...";
 // 是否安全?
 Date D1 = new Date();
 // 是否安全?
 final Date D2 = new Date();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
 }
}
例2:
public class MyServlet extends HttpServlet {
 // 是否安全?
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 记录调用次数
 private int count = 0;
 
 public void update() {
 // ...
 count++;
 }
}
例3:
@Aspect
@Component
public class MyAspect {
 // 是否安全?
 private long start = 0L;
 
 @Before("execution(* *(..))")
 public void before() {
 start = System.nanoTime();
 }
 
 @After("execution(* *(..))")
 public void after() {
 long end = System.nanoTime();
 System.out.println("cost time:" + (end-start));
 }
}
例4:
 
public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao { 
 public void update() {
 String sql = "update user set password = ? where username = ?";
 // 是否安全
 try (Connection conn = DriverManager.getConnection("","","")){
 // ...
 } catch (Exception e) {
 // ...
 }
 }
}
例5:
public class MyServlet extends HttpServlet {
 // 是否安全
 private UserService userService = new UserServiceImpl();
 
 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全
 private UserDao userDao = new UserDaoImpl();
 
 public void update() {
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 
                                                             
// 是否安全
 
                                                             
private Connection conn = null;
 
                                                             
public void update() throws SQLException {
 
                                                             
String sql = "update user set password = ? where username = ?";
 
                                                             
conn = DriverManager.getConnection("","","");
 
                                                             
// ...
 
                                                             
conn.close();
 
                                                             
}
 
                                                             
}
 

 

例6:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
 
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
例7:
public abstract class Test {
 
 public void bar() {
 // 是否安全
 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 foo(sdf);
}
 
 public abstract foo(SimpleDateFormat sdf);
 
 
 public static void main(String[] args) {
 new Test().bar();
 }
}
其中 foo 的行为是不肯定的,可能致使不安全的发生,被称之为外星方法
public void foo(SimpleDateFormat sdf) {
 String dateStr = "1999-10-11 00:00:00";
 for (int i = 0; i < 20; i++) {
 new Thread(() -> {
 try {
 sdf.parse(dateStr);
 } catch (ParseException e) {
 e.printStackTrace();
 }
 }).start();
 }
}
请比较 JDK 中 String 类的实现
例8:
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
 List<Thread> list = new ArrayList<>();
 for (int j = 0; j < 2; j++) {
 Thread thread = new Thread(() -> {
 for (int k = 0; k < 5000; k++) {
 synchronized (i) {
 i++;
 }
 }
 }, "" + j);
 list.add(thread);
 }
 list.stream().forEach(t -> t.start());
 list.stream().forEach(t -> {
 try {
 t.join();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
log.debug("{}", i);
 
                                                                 
}
 

4.5 习题

卖票练习
测试下面代码是否存在线程安全问题,并尝试改正
public class ExerciseSell {
 public static void main(String[] args) {
 TicketWindow ticketWindow = new TicketWindow(2000);
 List<Thread> list = new ArrayList<>();
 // 用来存储买出去多少张票
 List<Integer> sellCount = new Vector<>();
 for (int i = 0; i < 2000; i++) {
 Thread t = new Thread(() -> {
 // 分析这里的竞态条件
 int count = ticketWindow.sell(randomAmount());
 sellCount.add(count);
 });
 list.add(t);
 t.start();
 }
 list.forEach((t) -> {
 try {
 t.join();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
 // 买出去的票求和
 log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
 // 剩余票数
 log.debug("remainder count:{}", ticketWindow.getCount());
 }
 // Random 为线程安全
 static Random random = new Random();
 // 随机 1~5
 public static int randomAmount() {
 return random.nextInt(5) + 1;
 }
}
class TicketWindow {
 private int count;
 public TicketWindow(int count) {
 this.count = count;
 }
public int getCount() {
 return count;
 }
 public int sell(int amount) {
 if (this.count >= amount) {
 this.count -= amount;
 return amount;
 } else {
 return 0;
 }
 }
}
另外,用下面的代码行不行,为何?
List<Integer> sellCount = new ArrayList<>();
测试脚本
for /L %n in (1,1,10) do java -cp ".;C:\Users\manyh\.m2\repository\ch\qos\logback\logbackclassic\1.2.3\logback-classic-1.2.3.jar;C:\Users\manyh\.m2\repository\ch\qos\logback\logbackcore\1.2.3\logback-core-1.2.3.jar;C:\Users\manyh\.m2\repository\org\slf4j\slf4japi\1.7.25\slf4j-api-1.7.25.jar" cn.itcast.n4.exercise.ExerciseSell

 

转帐练习
测试下面代码是否存在线程安全问题,并尝试改正
public class ExerciseTransfer {
 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, randomAmount());
 }
 }, "t1");
 Thread t2 = new Thread(() -> {
 for (int i = 0; i < 1000; i++) {
 b.transfer(a, randomAmount());
 }
 }, "t2");
 t1.start();
 t2.start();
 t1.join();
 t2.join();
// 查看转帐2000次后的总金额
 log.debug("total:{}",(a.getMoney() + b.getMoney()));
 }
 // Random 为线程安全
 static Random random = new Random();
 // 随机 1~100
 public static int randomAmount() {
 return random.nextInt(100) +1;
 }
}
class Account {
 private int money;
 public Account(int money) {
 this.money = money;
 }
 public int getMoney() {
 return money;
 }
 public void setMoney(int money) {
 this.money = money;
 }
 public void transfer(Account target, int amount) {
 if (this.money > amount) {
 this.setMoney(this.getMoney() - amount);
 target.setMoney(target.getMoney() + amount);
 }
 }
}
这样改正行不行,为何?
public synchronized void transfer(Account target, int amount) {
 if (this.money > amount) {
 this.setMoney(this.getMoney() - amount);
 target.setMoney(target.getMoney() + amount);
 }
}

4.6 Monitor 概念

 
以 32 位虚拟机为例,普通对象的对象头结构以下,其中的Klass Word为指针,指向对应的Class对象;

 

 

 

 

Monitor 原理

Monitor被翻译为监视器或者说管程程序员

每一个java对象均可以关联一个Monitor,若是使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针。golang

1583652360228

 

  • 刚开始时Monitor中的Owner为nullweb

  • 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的全部者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner

  • 当Thread-2 占据锁时,若是线程Thread-3,Thread-4也来执行synchronized(obj){}代码,就会进入EntryList中变成BLOCKED状态

  • Thread-2 执行完同步代码块的内容,而后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的

  • 图中 WaitSet 中的 Thread-0,Thread-1 是以前得到过锁,但条件不知足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器,不听从以上规则

参考资料

synchronized原理

代码以下 Test17.java

 static final Object lock=new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }

反编译后的部分字节码

0 getstatic #2 <com/concurrent/test/Test17.lock>
 # 取得lock的引用(synchronized开始了)
 3 dup    
 # 复制操做数栈栈顶的值放入栈顶,即复制了一份lock的引用
 4 astore_1
 # 操做数栈栈顶的值弹出,即将lock的引用存到局部变量表中
 5 monitorenter
 # 将lock对象的Mark Word置为指向Monitor指针
 6 getstatic #3 <com/concurrent/test/Test17.counter>
 9 iconst_1
10 iadd
11 putstatic #3 <com/concurrent/test/Test17.counter>
14 aload_1
# 从局部变量表中取得lock的引用,放入操做数栈栈顶
15 monitorexit
# 将lock对象的Mark Word重置,唤醒EntryList
16 goto 24 (+8)
# 下面是异常处理指令,能够看到,若是出现异常,也能自动地释放锁
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return
注意:方法级别的 synchronized 不会在字节码指令中有所体现

synchronized 原理进阶

轻量级锁

轻量级锁的使用场景是:若是一个对象虽然有多个线程要对它进行加锁,可是加锁的时间是错开的(也就是没有人能够竞争的),那么能够使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步块 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步块 B
     }
}

1.每次指向到synchronized代码块时,都会建立锁记录(Lock Record)对象,每一个线程都会包括一个锁记录的结构,锁记录内部能够储存对象的Mark Word和对象引用reference

 

2. 让锁记录中的Object reference指向对象,而且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中

 

 3.若是cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,以下所示

 

 

4.若是cas失败,有两种状况

  1. 若是是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段

  2. 若是是本身的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 做为重入的计数

 

 5.当线程退出synchronized代码块的时候,若是获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减一

 

 

6.当线程退出synchronized代码块的时候,若是获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象

  1. 成功则解锁成功

  2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

若是在尝试加轻量级锁的过程当中,cas操做没法成功,这是有一种状况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

 

 

2.这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  1. 即为对象申请Monitor锁,让Object指向重量级锁地址,而后本身进入Monitor 的EntryList 变成BLOCKED状态

 

 

自旋优化

重量级锁竞争的时候,还能够使用自旋来进行优化,若是当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就能够不用进行上下文切换就得到了锁

1.自旋重试成功的状况

 

 2.自旋重试失败的状况,自旋了必定次数仍是没有等到持锁的线程释放锁

 

 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优点。在 Java 6 以后自旋锁是自适应的,好比对象刚刚的一次自旋操做成功过,那么认为此次自旋成功的可能性会高,就多自旋几回;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 以后不能控制是否开启自旋功能

偏向锁

在轻量级的锁中,咱们能够发现,若是同一个线程对同一个2对象进行重入锁时,也须要执行CAS操做,这是有点耗时滴,那么java6开始引入了偏向锁的东东,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,以后这个入锁线程再进行重入锁时,发现线程ID是本身的,那么就不用再进行CAS了

 

 

偏向状态

 

 

一个对象的建立过程

  1. 若是开启了偏向锁(默认是开启的),那么对象刚建立以后,Mark Word 最后三位的值101,而且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.

  2. 偏向锁默认是延迟的,不会在程序启动的时候马上生效,若是想避免延迟,能够添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟

  3. 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

  4. 实验Test18.java,加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试

public static void main(String[] args) throws InterruptedException {
        Test1 t = new Test1();
        test.parseObjectHeader(getObjectHeader(t));
        synchronized (t){
            test.parseObjectHeader(getObjectHeader(t));
        }
        test.parseObjectHeader(getObjectHeader(t));
    }

输出结果以下,三次输出的状态码都为101

biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
biasedLockFlag (1bit): 1
    LockFlag (2bit): 01

测试禁用:若是没有开启偏向锁,那么对象建立后最后三位的值为001,这时候它的hashcode,age都为0,hashcode是第一次用到hashcode时才赋值的。在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking禁用偏向锁(禁用偏向锁则优先使用轻量级锁),退出synchronized状态变回001

  1. 测试代码Test18.java 虚拟机参数-XX:-UseBiasedLocking

  2. 输出结果以下,最开始状态为001,而后加轻量级锁变成00,最后恢复成001

biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
撤销偏向锁-hashcode方法

测试 hashCode:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁,由于使用偏向锁时没有位置存hashcode的值了

  1. 测试代码以下,使用虚拟机参数-XX:BiasedLockingStartupDelay=0 ,确保咱们的程序最开始使用了偏向锁!可是结果显示程序仍是使用了轻量级锁。 Test20.java

 public static void main(String[] args) throws InterruptedException {
        Test1 t = new Test1();
        t.hashCode();
        test.parseObjectHeader(getObjectHeader(t));

        synchronized (t){
            test.parseObjectHeader(getObjectHeader(t));
        }
        test.parseObjectHeader(getObjectHeader(t));
    }

输出结果

biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
撤销偏向锁-其它线程使用对象

这里咱们演示的是偏向锁撤销变成轻量级锁的过程,那么就得知足轻量级锁的使用条件,就是没有线程对同一个对象进行锁竞争,咱们使用waitnotify 来辅助实现

  1. 代码 Test19.java,虚拟机参数-XX:BiasedLockingStartupDelay=0确保咱们的程序最开始使用了偏向锁!

  2. 输出结果,最开始使用的是偏向锁,可是第二个线程尝试获取对象锁时,发现原本对象偏向的是线程一,那么偏向锁就会失效,加的就是轻量级锁

biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
撤销 - 调用 wait/notify

会使对象的锁变成重量级锁,由于wait/notify方法以后重量级锁才支持

批量重偏向

若是对象被多个线程访问,可是没有竞争,这时候偏向了线程一的对象又有机会从新偏向线程二,便可以不用升级为轻量级锁,可这和咱们以前作的实验矛盾了呀,其实要实现从新偏向是要有条件的:就是超过20对象对同一个线程如线程一撤销偏向时,那么第20个及之后的对象才能够将撤销对线程一的偏向这个动做变为将第20个及之后的对象偏向线程二。Test21.java

 

4.6 wait和notify

建议先看看wait和notify方法的javadoc文档

4.6.1同步模式之保护性暂停

即 Guarded Suspension,用在一个线程等待另外一个线程的执行结果,要点:

  1. 有一个结果须要从一个线程传递到另外一个线程,让他们关联同一个 GuardedObject

  2. 若是有结果不断从一个线程到另外一个线程那么能够使用消息队列(见生产者/消费者)

  3. JDK 中,join 的实现、Future 的实现,采用的就是此模式

  4. 由于要等待另外一方的结果,所以归类到同步模式

代码:Test22.java Test23.java这是带超时时间的

 

 

 Test23.java中jiang'dao'de关于超时的加强,在join(long millis) 的源码中获得了体现:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
        // join一个指定的时间
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

 

多任务版 GuardedObject图中 Futures 就比如居民楼一层的信箱(每一个信箱有房间编号),左侧的 t0,t2,t4 就比如等待邮件的居民,右侧的 t1,t3,t5 就比如邮递员若是须要在多个类之间使用 GuardedObject 对象,做为参数传递不是很方便,所以设计一个用来解耦的中间类,这样不只可以解耦【结果等待者】和【结果生产者】,还可以同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,可是生产者消费者模式并非。rpc框架的调用中就使用到了这种模式。 Test24.java

 

 

 

4.6.2异步模式之生产者/消费者

要点

  1. 与前面的保护性暂停中的 GuardObject 不一样,不须要产生结果和消费结果的线程一一对应

  2. 消费队列能够用来平衡生产和消费的线程资源

  3. 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据

  4. 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据

  5. JDK 中各类阻塞队列,采用的就是这种模式

 

“异步”的意思就是生产者产生消息以后消息没有被马上消费,而“同步模式”中,消息在产生以后被马上消费了。

 

 

 

咱们写一个线程间通讯的消息队列,要注意区别,像rabbit mq等消息框架是进程间通讯的。

 

 

4.7 wait notify
小故事 - 为何须要 wait
因为条件不知足,小南不能继续进行计算
但小南若是一直占用着锁,其它人就得一直阻塞,效率过低 

 

 因而老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,

其它人能够由老王随机安排进屋
直到小 M 将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

 

 小南因而能够离开休息室,从新进入竞争锁的队列

 

 

 

 

API 介绍
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() object 上正在 waitSet 等待的线程所有唤醒
 
它们都是线程之间进行协做的手段,都属于 Object 对象的方法。必须得到此对象的锁,才能调用这几个方法 
final static Object obj = new Object();
public static void main(String[] args) {
 new Thread(() -> {
 synchronized (obj) {
 log.debug("执行....");
 try {
 obj.wait(); // 让线程在obj上一直等待下去
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 log.debug("其它代码....");
 }
 }).start();
 new Thread(() -> {
 synchronized (obj) {
 log.debug("执行....");
 try {
 obj.wait(); // 让线程在obj上一直等待下去
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 log.debug("其它代码....");
 }
 }).start();
 // 主线程两秒后执行
 sleep(2);
 log.debug("唤醒 obj 上其它线程");
 synchronized (obj) {
 obj.notify(); // 唤醒obj上一个线程
 // obj.notifyAll(); // 唤醒obj上全部等待线程
 }
}
notify 的一种结果
 
20:00:53.096 [Thread-0] c.TestWaitNotify - 执行.... 
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行.... 
20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码....
notifyAll 的结果
19:58:15.457 [Thread-0] c.TestWaitNotify - 执行.... 
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行.... 
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码.... 
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码....
notify 为止
wait(long n) 有时限的等待 , n 毫秒后结束等待,或是被 notify
 
  4.8 wait notify 的正确姿式
开始以前先看看
sleep(long n) wait(long n) 的区别
1) sleep Thread 方法,而 wait Object 的方法 2) sleep 不须要强制和 synchronized 配合使用,但 wait 须要
synchronized 一块儿用 3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 4) 它们
状态 TIMED_WAITING
step 1
思考下面的解决方案好很差,为何?
static final Object room = new Object ();
static boolean hasCigarette = false ;
static boolean hasTakeout = false ;
new Thread (() -> {
synchronized ( room ) {
log . debug ( " 有烟没? [{}]" , hasCigarette );
if ( ! hasCigarette ) {
log . debug ( " 没烟,先歇会! " );
sleep ( 2 );
}
log . debug ( " 有烟没? [{}]" , hasCigarette );
if ( hasCigarette ) {
log . debug ( " 能够开始干活了 " );
}
}
}, " 小南 " ). start ();
for ( int i = 0 ; i < 5 ; i ++ ) {
new Thread (() -> {
synchronized ( room ) {
log . debug ( " 能够开始干活了 " );
}
}, " 其它人 " ). start ();
}
sleep ( 1 );
new Thread (() -> {
// 这里能不能加 synchronized (room)
hasCigarette = true ;
log . debug ( " 烟到了噢! " );
}, " 送烟的 " ). start ();
 
 
 
 
 
输出
20:49:49.883 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:49:49.887 [ 小南 ] c.TestCorrectPosture - 没烟,先歇会!
20:49:50.882 [ 送烟的 ] c.TestCorrectPosture - 烟到了噢!
20:49:51.887 [ 小南 ] c.TestCorrectPosture - 有烟没? [true]
20:49:51.887 [ 小南 ] c.TestCorrectPosture - 能够开始干活了
20:49:51.887 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:49:51.887 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:49:51.888 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:49:51.888 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:49:51.888 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
其它干活的线程,都要一直阻塞,效率过低
小南线程必须睡足 2s 后才能醒来,就算烟提早送到,也没法马上醒来
加了 synchronized (room) 后,就比如小南在里面反锁了门睡觉,烟根本无法送进门, main 没加
synchronized 就好像 main 线程是翻窗户进来的
解决方法,使用 wait - notify 机制
 
 
step 2
思考下面的实现行吗,为何?
 
 
new Thread (() -> {
synchronized ( room ) {
log . debug ( " 有烟没? [{}]" , hasCigarette );
if ( ! hasCigarette ) {
log . debug ( " 没烟,先歇会! " );
try {
room . wait ( 2000 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
log . debug ( " 有烟没? [{}]" , hasCigarette );
if ( hasCigarette ) {
log . debug ( " 能够开始干活了 " );
}
}
}, " 小南 " ). start ();
for ( int i = 0 ; i < 5 ; i ++ ) {
new Thread (() -> {
synchronized ( room ) {
log . debug ( " 能够开始干活了 " );
}
}, " 其它人 " ). start ();
}
sleep ( 1 );
new Thread (() -> {
synchronized ( room ) {
hasCigarette = true ;
log . debug ( " 烟到了噢! " );
room . notify ();
}
}, " 送烟的 " ). start ();
 
 
输出
20:51:42.489 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:51:42.493 [ 小南 ] c.TestCorrectPosture - 没烟,先歇会!
20:51:42.493 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:51:42.493 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:51:42.494 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:51:42.494 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:51:42.494 [ 其它人 ] c.TestCorrectPosture - 能够开始干活了
20:51:43.490 [ 送烟的 ] c.TestCorrectPosture - 烟到了噢!
20:51:43.490 [ 小南 ] c.TestCorrectPosture - 有烟没? [true]
20:51:43.490 [ 小南 ] c.TestCorrectPosture - 能够开始干活了
 
解决了其它干活的线程阻塞的问题
但若是有其它线程也在等待条件呢?
 
step 3
new Thread (() -> {
synchronized ( room ) {
log . debug ( " 有烟没? [{}]" , hasCigarette );
if ( ! hasCigarette ) {
log . debug ( " 没烟,先歇会! " );
try {
room . wait ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
log . debug ( " 有烟没? [{}]" , hasCigarette );
if ( hasCigarette ) {
log . debug ( " 能够开始干活了 " );
} else {
log . debug ( " 没干成活 ..." );
}
}
}, " 小南 " ). start ();
new Thread (() -> {
synchronized ( room ) {
Thread thread = Thread . currentThread ();
log . debug ( " 外卖送到没? [{}]" , hasTakeout );
if ( ! hasTakeout ) {
log . debug ( " 没外卖,先歇会! " );
try {
room . wait ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
log . debug ( " 外卖送到没? [{}]" , hasTakeout );
if ( hasTakeout ) {
log . debug ( " 能够开始干活了 " );
} else {
log . debug ( " 没干成活 ..." );
}
}
}, " 小女 " ). start ();
sleep ( 1 );
new Thread (() -> {
synchronized ( room ) {
hasTakeout = true ;
log . debug ( " 外卖到了噢! " );
room . notify ();
}
}, " 送外卖的 " ). start ();
 
  输出
20:53:12.173 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:53:12.176 [ 小南 ] c.TestCorrectPosture - 没烟,先歇会!
20:53:12.176 [ 小女 ] c.TestCorrectPosture - 外卖送到没? [false]
20:53:12.176 [ 小女 ] c.TestCorrectPosture - 没外卖,先歇会!
20:53:13.174 [ 送外卖的 ] c.TestCorrectPosture - 外卖到了噢!
20:53:13.174 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:53:13.174 [ 小南 ] c.TestCorrectPosture - 没干成活 ...
 
   notify 只能随机唤醒一个 WaitSet 中的线程,这时若是有其它线程也在等待,那么就可能唤醒不了正确的线
程,称之为【虚假唤醒】
解决方法,改成 notifyAll 
 
step 4
new Thread (() -> {
synchronized ( room ) {
hasTakeout = true ;
log . debug ( " 外卖到了噢! " );
room . notifyAll ();
}
}, " 送外卖的 " ). start ();
 
输出
20:55:23.978 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:55:23.982 [ 小南 ] c.TestCorrectPosture - 没烟,先歇会!
20:55:23.982 [ 小女 ] c.TestCorrectPosture - 外卖送到没? [false]
20:55:23.982 [ 小女 ] c.TestCorrectPosture - 没外卖,先歇会!
20:55:24.979 [ 送外卖的 ] c.TestCorrectPosture - 外卖到了噢!
20:55:24.979 [ 小女 ] c.TestCorrectPosture - 外卖送到没? [true]
20:55:24.980 [ 小女 ] c.TestCorrectPosture - 能够开始干活了
20:55:24.980 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:55:24.980 [ 小南 ] c.TestCorrectPosture - 没干成活 ...
notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有从新
判断的机会了
解决方法,用 while + wait ,当条件不成立,再次 wait
 
 
step 5
if 改成 while
 
if ( ! hasCigarette ) {
log . debug ( " 没烟,先歇会! " );
try {
room . wait ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
 
改动后
while ( ! hasCigarette ) {
log . debug ( " 没烟,先歇会! " );
try {
room . wait ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
 
输出
 
  20:58:34.322 [ 小南 ] c.TestCorrectPosture - 有烟没? [false]
20:58:34.326 [ 小南 ] c.TestCorrectPosture - 没烟,先歇会!
20:58:34.326 [ 小女 ] c.TestCorrectPosture - 外卖送到没? [false]
20:58:34.326 [ 小女 ] c.TestCorrectPosture - 没外卖,先歇会!
20:58:35.323 [ 送外卖的 ] c.TestCorrectPosture - 外卖到了噢!
20:58:35.324 [ 小女 ] c.TestCorrectPosture - 外卖送到没? [true]
20:58:35.324 [ 小女 ] c.TestCorrectPosture - 能够开始干活了
20:58:35.324 [ 小南 ] c.TestCorrectPosture - 没烟,先歇会!
 
 
synchronized ( lock ) {
while ( 条件不成立 ) {
lock . wait ();
}
// 干活
}
// 另外一个线程
synchronized ( lock ) {
lock . notifyAll ();
}
 
 
 
 
 
 
4.9 Park & Unpark
基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

 

park unpark
 
 
Thread t1 = new Thread(() -> {
 log.debug("start...");
 sleep(1);
 log.debug("park...");
 LockSupport.park();
 log.debug("resume...");
},"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);

 

输出
 
18:42:52.585 c.TestParkUnpark [t1] - start... 
18:42:53.589 c.TestParkUnpark [t1] - park... 
18:42:54.583 c.TestParkUnpark [main] - unpark... 
18:42:54.583 c.TestParkUnpark [t1] - resume...
unpark park
 
Thread t1 = new Thread(() -> {
 log.debug("start...");
 sleep(2);
 log.debug("park...");
 LockSupport.park();
 log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
输出
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...
 
特色
Object wait & notify 相比
wait notify notifyAll 必须配合 Object Monitor 一块儿使用,而 park unpark 没必要
  park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程, notifyAll
是唤醒全部等待线程,就不那么【精确】
park & unpark 能够先 unpark ,而 wait & notify 不能先 notify
 
 
 

4.7.2 park unpark 原理

每一个线程都有本身的一个 Parker 对象,由三部分组成 _counter, _cond和 _mutex

  1. 打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量 _ cond就比如背包中的账篷。_counter 就比如背包中的备用干粮(0 为耗尽,1 为充足)

  2. 调用 park 就是要看需不须要停下来歇息

    1. 若是备用干粮耗尽,那么钻进账篷歇息

    2. 若是备用干粮充足,那么不需停留,继续前进

  3. 调用 unpark,就比如令干粮充足

    1. 若是这时线程还在账篷,就唤醒让他继续前进

    2. 若是这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进

      1. 由于背包空间有限,屡次调用 unpark 仅会补充一份备用干粮

能够不看例子,直接看实现过程

先调用park再调用upark的过程

1.先调用park

  1. 当前线程调用 Unsafe.park() 方法

  2. 检查 _counter ,本状况为 0,这时,得到 _mutex 互斥锁(mutex对象有个等待队列 _cond)

  3. 线程进入 _cond 条件变量阻塞

  4. 设置 _counter = 0

 

 

 2.调用upark

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

  2. 唤醒 _cond 条件变量中的 Thread_0

  3. Thread_0 恢复运行

  4. 设置 _counter 为 0

 

 

 

先调用upark再调用park的过程

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

  2. 当前线程调用 Unsafe.park() 方法

  3. 检查 _counter ,本状况为 1,这时线程无需阻塞,继续运行

  4. 设置 _counter 为 0

 

 

 4.10 从新理解线程状态转换

 

 

 假设有线程 Thread t

状况 1 NEW -- > RUNNABLE
当调用 t.start() 方法时,由 NEW -- > RUNNABLE
状况 2 RUNNABLE < -- > WAITING
t 线程 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时, t 线程 RUNNABLE -- > WAITING
调用 obj.notify() obj.notifyAll() t.interrupt()
竞争锁成功, t 线程 WAITING -- > RUNNABLE
竞争锁失败, t 线程 WAITING -- > BLOCKED 
public class TestWaitNotify {
final static Object obj = new Object ();
public static void main ( String [] args ) {
new Thread (() -> {
synchronized ( obj ) {
log . debug ( " 执行 ...." );
try {
obj . wait ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
log . debug ( " 其它代码 ...." ); // 断点
}
}, "t1" ). start ();
new Thread (() -> {
synchronized ( obj ) {
log . debug ( " 执行 ...." );
try {
obj . wait ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
log . debug ( " 其它代码 ...." ); // 断点
}
}, "t2" ). start ();
 
sleep ( 0.5 );
log . debug ( " 唤醒 obj 上其它线程 " );
synchronized ( obj ) {
obj . notifyAll (); // 唤醒 obj 上全部等待线程 断点
}
}
}
 
状况 3 RUNNABLE < -- > WAITING
当前线程 调用 t.join() 方法时, 当前线程 RUNNABLE -- > WAITING
注意是 当前线程 t 线程对象 的监视器上等待
t 线程 运行结束,或调用了 当前线程 interrupt() 时, 当前线程 WAITING -- > RUNNABLE
状况 4 RUNNABLE < -- > WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE -- > WAITING
调用 LockSupport.unpark( 目标线程 ) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -- >
RUNNABLE
状况 5 RUNNABLE < -- > TIMED_WAITING
t 线程 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时, t 线程 RUNNABLE -- > TIMED_WAITING
t 线程 等待时间超过了 n 毫秒,或调用 obj.notify() obj.notifyAll() t.interrupt()
竞争锁成功, t 线程 TIMED_WAITING -- > RUNNABLE
竞争锁失败, t 线程 TIMED_WAITING -- > BLOCKED
状况 6 RUNNABLE < -- > TIMED_WAITING
当前线程 调用 t.join(long n) 方法时, 当前线程 RUNNABLE -- > TIMED_WAITING
注意是 当前线程 t 线程对象 的监视器上等待
当前线程 等待时间超过了 n 毫秒,或 t 线程 运行结束,或调用了 当前线程 interrupt() 时, 当前线程
TIMED_WAITING -- > RUNNABLE
状况 7 RUNNABLE < -- > TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE -- > TIMED_WAITING
当前线程 等待时间超过了 n 毫秒, 当前线程 TIMED_WAITING -- > RUNNABLE
状况 8 RUNNABLE < -- > TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) LockSupport.parkUntil(long millis) 时, 当前线
RUNNABLE -- > TIMED_WAITING
调用 LockSupport.unpark( 目标线程 ) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从
TIMED_WAITING -- > RUNNABLE
状况 9 RUNNABLE < -- > BLOCKED
t 线程 synchronized(obj) 获取了对象锁时若是竞争失败,从 RUNNABLE -- > BLOCKED
obj 锁线程的同步代码块执行完毕,会唤醒该对象上全部 BLOCKED 的线程从新竞争,若是其中 t 线程 竞争
成功,从 BLOCKED -- > RUNNABLE ,其它失败的线程仍然 BLOCKED
状况 10 RUNNABLE < -- > TERMINATED
当前线程全部代码运行完毕,进入 TERMINATED
 
4.11 多把锁
多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。
如今小南要学习,小女要睡觉,但若是只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
例如
class BigRoom {
public void sleep () {
synchronized ( this ) {
log . debug ( "sleeping 2 小时 " );
Sleeper . sleep ( 2 );
}
}
public void study () {
synchronized ( this ) {
log . debug ( "study 1 小时 " );
Sleeper . sleep ( 1 );
}
}
}
 
执行
BigRoom bigRoom = new BigRoom ();
new Thread (() -> {
bigRoom . compute ();
}, " 小南 " ). start ();
new Thread (() -> {
bigRoom . sleep ();
}, " 小女 " ). start ();
 
某次结果
12:13:54.471 [ 小南 ] c.BigRoom - study 1 小时
12:13:55.476 [ 小女 ] c.BigRoom - sleeping 2 小时
 
改进
class BigRoom {
private final Object studyRoom = new Object ();
private final Object bedRoom = new Object ();
public void sleep () {
synchronized ( bedRoom ) {
log . debug ( "sleeping 2 小时 " );
Sleeper . sleep ( 2 );
}
}
public void study () {
synchronized ( studyRoom ) {
log . debug ( "study 1 小时 " );
Sleeper . sleep ( 1 );
}
}
}
 
某次执行结果
12:15:35.069 [ 小南 ] c.BigRoom - study 1 小时
12:15:35.069 [ 小女 ] c.BigRoom - sleeping 2 小时
 
将锁的粒度细分
好处,是能够加强并发度
坏处,若是一个线程须要同时得到多把锁,就容易发生死锁
 
4.12 活跃性
死锁
有这样的状况:一个线程须要同时获取多把锁,这时就容易发生死锁
t1 线程 得到 A 对象 锁,接下来想获取 B 对象 的锁 t2 线程 得到 B 对象 锁,接下来想获取 A 对象 的锁 例:
Object A = new Object ();
Object B = new Object ();
Thread t1 = new Thread (() -> {
synchronized ( A ) {
log . debug ( "lock A" );
sleep ( 1 );
synchronized ( B ) {
log . debug ( "lock B" );
log . debug ( " 操做 ..." );
}
}
}, "t1" );
Thread t2 = new Thread (() -> {
synchronized ( B ) {
log . debug ( "lock B" );
sleep ( 0.5 );
synchronized ( A ) {
log . debug ( "lock A" );
  log . debug ( " 操做 ..." );
}
}
}, "t2" );
t1 . start ();
t2 . start ();
 
结果
 
  12:22:06.962 [t2] c.TestDeadLock - lock B
12:22:06.962 [t1] c.TestDeadLock - lock A
  
 
定位死锁
检测死锁能够使用 jconsole 工具,或者使用 jps 定位进程 id ,再用 jstack 定位死锁:
 
  cmd > jps
Picked up JAVA_TOOL_OPTIONS: -Dfile .encoding = UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM 进程
11508 Main
28468 Launcher
cmd > jstack 33200
Picked up JAVA_TOOL_OPTIONS: -Dfile .encoding = UTF-8
2018 -12-29 05 :51:40
Full thread dump Java HotSpot(TM) 64 -Bit Server VM (25.91-b14 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry
[0x000000001f54f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at thread.TestDeadLock.lambda $main$1 (TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object)
at thread.TestDeadLock $$Lambda$2 /883049899.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry
[0x000000001f44f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at thread.TestDeadLock.lambda $main$0 (TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
at thread.TestDeadLock $$Lambda$1 /495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
 
// 略去部分输出
Found one Java-level deadlock:
=============================
"Thread-1" :
waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0" :
waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1" :
at thread.TestDeadLock.lambda $main$1 (TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object)
at thread.TestDeadLock $$Lambda$2 /883049899.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0" :
at thread.TestDeadLock.lambda $main$0 (TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
at thread.TestDeadLock $$Lambda$1 /495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
 
 
避免死锁要注意加锁顺序
  • 另外若是因为某个线程进入了死循环,致使其它线程一直等待,对于这种状况 linux 下能够经过 top 先定位到
  • CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪一个线程,最后再用 jstack 排查

 

 

哲学家就餐问题 

 

 有五位哲学家,围坐在圆桌旁。

他们只作两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
若是筷子被身边的人拿着,本身就得等待
 
 
筷子类
class Chopstick {
String name ;
public Chopstick ( String name ) {
this . name = name ;
}
@Override
public String toString () {
return " 筷子 {" + name + '}' ;
}
}
 
哲学家类
class Philosopher extends Thread {
Chopstick left ;
Chopstick right ;
public Philosopher ( String name , Chopstick left , Chopstick right ) {
super ( name );
this . left = left ;
this . right = right ;
}
private void eat () {
log . debug ( "eating..." );
Sleeper . sleep ( 1 );
}
 
@Override
public void run () {
while ( true ) {
// 得到左手筷子
synchronized ( left ) {
// 得到右手筷子
synchronized ( right ) {
// 吃饭
eat ();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
 
 
就餐
Chopstick c1 = new Chopstick ( "1" );
Chopstick c2 = new Chopstick ( "2" );
Chopstick c3 = new Chopstick ( "3" );
Chopstick c4 = new Chopstick ( "4" );
Chopstick c5 = new Chopstick ( "5" );
new Philosopher ( " 苏格拉底 " , c1 , c2 ). start ();
new Philosopher ( " 柏拉图 " , c2 , c3 ). start ();
new Philosopher ( " 亚里士多德 " , c3 , c4 ). start ();
new Philosopher ( " 赫拉克利特 " , c4 , c5 ). start ();
new Philosopher ( " 阿基米德 " , c5 , c1 ). start ();.
 
执行很少会,就执行不下去了
 
  12:33:15.575 [ 苏格拉底 ] c.Philosopher - eating...
12:33:15.575 [ 亚里士多德 ] c.Philosopher - eating...
12:33:16.580 [ 阿基米德 ] c.Philosopher - eating...
12:33:17.580 [ 阿基米德 ] c.Philosopher - eating...
// 卡在这里 , 不向下运行 
 
  使用 jconsole 检测死锁,发现
 
-------------------------------------------------------------------------
名称 : 阿基米德
状态 : cn . itcast . Chopstick @ 1540e19 d ( 筷子 1 ) 上的 BLOCKED , 拥有者 : 苏格拉底
总阻止数 : 2 , 总等待数 : 1
堆栈跟踪 :
cn . itcast . Philosopher . run ( TestDinner . java: 48 )
- 已锁定 cn . itcast . Chopstick @ 6 d6f6e28 ( 筷子 5 )
-------------------------------------------------------------------------
名称 : 苏格拉底
状态 : cn . itcast . Chopstick @ 677327 b6 ( 筷子 2 ) 上的 BLOCKED , 拥有者 : 柏拉图
总阻止数 : 2 , 总等待数 : 1
堆栈跟踪 :
cn . itcast . Philosopher . run ( TestDinner . java: 48 )
- 已锁定 cn . itcast . Chopstick @ 1540e19 d ( 筷子 1 )
-------------------------------------------------------------------------
名称 : 柏拉图
状态 : cn . itcast . Chopstick @ 14 ae5a5 ( 筷子 3 ) 上的 BLOCKED , 拥有者 : 亚里士多德
总阻止数 : 2 , 总等待数 : 0
堆栈跟踪 :
cn . itcast . Philosopher . run ( TestDinner . java: 48 )
- 已锁定 cn . itcast . Chopstick @ 677327 b6 ( 筷子 2 )
-------------------------------------------------------------------------
名称 : 亚里士多德
状态 : cn . itcast . Chopstick @ 7 f31245a ( 筷子 4 ) 上的 BLOCKED , 拥有者 : 赫拉克利特
总阻止数 : 1 , 总等待数 : 1
堆栈跟踪 :
cn . itcast . Philosopher . run ( TestDinner . java: 48 )
- 已锁定 cn . itcast . Chopstick @ 14 ae5a5 ( 筷子 3 )
-------------------------------------------------------------------------
名称 : 赫拉克利特
状态 : cn . itcast . Chopstick @ 6 d6f6e28 ( 筷子 5 ) 上的 BLOCKED , 拥有者 : 阿基米德
总阻止数 : 2 , 总等待数 : 0
堆栈跟踪 :
cn . itcast . Philosopher . run ( TestDinner . java: 48 )
- 已锁定 cn . itcast . Chopstick @ 7 f31245a ( 筷子 4 )
 
这种线程没有按预期结束,执行不下去的状况,归类为【活跃性】问题,除了死锁之外,还有活锁和饥饿者两种情
 
活锁
活锁出如今两个线程互相改变对方的结束条件,最后谁也没法结束,例如
 
public class TestLiveLock {
static volatile int count = 10 ;
static final Object lock = new Object ();
public static void main ( String [] args ) {
new Thread (() -> {
// 指望减到 0 退出循环
while ( count > 0 ) {
sleep ( 0.2 );
count -- ;
log . debug ( "count: {}" , count );
}
}, "t1" ). start ();
new Thread (() -> {
// 指望超过 20 退出循环
while ( count < 20 ) {
sleep ( 0.2 );
count ++ ;
log . debug ( "count: {}" , count );
}
}, "t2" ). start ();
}
}
 
饥饿
不少教程中把饥饿定义为,一个线程因为优先级过低,始终得不到 CPU 调度执行,也不可以结束,饥饿的状况不
易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决以前的死锁问题
 
 

 

 顺序加锁的解决方案

 

 

 

4.13 ReentrantLock
相对于 synchronized 它具有以下特色
可中断
能够设置超时时间
能够设置为公平锁
支持多个条件变量
synchronized 同样,都支持可重入
基本语法
 
// 获取锁
reentrantLock . lock ();
try {
// 临界区
} finally {
// 释放锁
reentrantLock . unlock ();
}

 

可重入
可重入是指同一个线程若是首次得到了这把锁,那么由于它是这把锁的拥有者,所以有权利再次获取这把锁
若是是不可重入锁,那么第二次得到锁时,本身也会被锁挡住
 
  static ReentrantLock lock = new ReentrantLock ();
public static void main ( String [] args ) {
method1 ();
}
public static void method1 () {
lock . lock ();
try {
log . debug ( "execute method1" );
method2 ();
} finally {
lock . unlock ();
}
}
public static void method2 () {
lock . lock ();
try {
log . debug ( "execute method2" );
method3 ();
} finally {
lock . unlock ();
}
}
public static void method3 () {
lock . lock ();
try {
log . debug ( "execute method3" );
} finally {
lock . unlock ();
}
}
 
输出
 
  17:59:11.862 [main] c.TestReentrant - execute method1
17:59:11.865 [main] c.TestReentrant - execute method2
17:59:11.865 [main] c.TestReentrant - execute method3
 
 
可打断
示例.
ReentrantLock lock = new ReentrantLock ();
Thread t1 = new Thread (() -> {
log . debug ( " 启动 ..." );
try {
lock . lockInterruptibly ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
log . debug ( " 等锁的过程当中被打断 " );
return ;
}
try {
log . debug ( " 得到了锁 " );
} finally {
lock . unlock ();
}
}, "t1" );
lock . lock ();
log . debug ( " 得到了锁 " );
t1 . start ();
try {
sleep ( 1 );
t1 . interrupt ();
log . debug ( " 执行打断 " );
} finally {
lock . unlock ();
}
 
 
输出
18:02:40.520 [main] c.TestInterrupt - 得到了锁
18:02:40.524 [t1] c.TestInterrupt - 启动 ...
18:02:41.530 [main] c.TestInterrupt - 执行打断
java.lang.InterruptedException
at
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron
izer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17)
at java.lang.Thread.run(Thread.java:748)
18:02:41.532 [t1] c.TestInterrupt - 等锁的过程当中被打断
 
 
注意若是是不可中断模式,那么即便使用了 interrupt 也不会让等待中断
 
  ReentrantLock lock = new ReentrantLock ();
Thread t1 = new Thread (() -> {
log . debug ( " 启动 ..." );
lock . lock ();
try {
log . debug ( " 得到了锁 " );
} finally {
lock . unlock ();
}
}, "t1" );
lock . lock ();
log . debug ( " 得到了锁 " );
t1 . start ();
try {
sleep ( 1 );
t1 . interrupt ();
log . debug ( " 执行打断 " );
sleep ( 1 );
} finally {
log . debug ( " 释放了锁 " );
lock . unlock ();
}
 
输出
  18:06:56.261 [main] c.TestInterrupt - 得到了锁
18:06:56.265 [t1] c.TestInterrupt - 启动 ...
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并无被真正打断 , 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 得到了锁
 
 
锁超时
马上失败
 
ReentrantLock lock = new ReentrantLock ();
Thread t1 = new Thread (() -> {
log . debug ( " 启动 ..." );
if ( ! lock . tryLock ()) {
log . debug ( " 获取马上失败,返回 " );
return ;
}
try {
log . debug ( " 得到了锁 " );
} finally {
lock . unlock ();
}
}, "t1" );
lock . lock ();
log . debug ( " 得到了锁 " );
t1 . start ();
try {
sleep ( 2 );
} finally {
lock . unlock ();
}
 
输出
 
  18:15:02.918 [main] c.TestTimeout - 得到了锁
18:15:02.921 [t1] c.TestTimeout - 启动 ...
18:15:02.921 [t1] c.TestTimeout - 获取马上失败,返回
 
 
超时失败
 
  ReentrantLock lock = new ReentrantLock ();
Thread t1 = new Thread (() -> {
log . debug ( " 启动 ..." );
try {
if ( ! lock . tryLock ( 1 , TimeUnit . SECONDS )) {
log . debug ( " 获取等待 1s 后失败,返回 " );
return ;
}
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
try {
log . debug ( " 得到了锁 " );
} finally {
lock . unlock ();
}
}, "t1" );
lock . lock ();
log . debug ( " 得到了锁 " );
t1 . start ();
try {
sleep ( 2 );
} finally {
lock . unlock ();
}
 
输出
18:19:40.537 [main] c.TestTimeout - 得到了锁
18:19:40.544 [t1] c.TestTimeout - 启动 ...
18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败,返回
 
 
  使用 tryLock 解决哲学家就餐问题
 
class Chopstick extends ReentrantLock {
String name ;
public Chopstick ( String name ) {
this . name = name ;
}
@Override
public String toString () {
return " 筷子 {" + name + '}' ;
}
}
 
class Philosopher extends Thread {
Chopstick left ;
Chopstick right ;
public Philosopher ( String name , Chopstick left , Chopstick right ) {
super ( name );
this . left = left ;
this . right = right ;
}
@Override
public void run () {
while ( true ) {
// 尝试得到左手筷子
if ( left . tryLock ()) {
try {
// 尝试得到右手筷子
if ( right . tryLock ()) {
try {
eat ();
} finally {
right . unlock ();
}
}
} finally {
left . unlock ();
}
}
}
}
private void eat () {
log . debug ( "eating..." );
Sleeper . sleep ( 1 );
}
}
 
公平锁
ReentrantLock 默认是不公平的
 
ReentrantLock lock = new ReentrantLock ( false );
lock . lock ();
for ( int i = 0 ; i < 500 ; i ++ ) {
new Thread (() -> {
lock . lock ();
try {
System . out . println ( Thread . currentThread (). getName () + " running..." );
} finally {
lock . unlock ();
}
}, "t" + i ). start ();
}
// 1s 以后去争抢锁
Thread . sleep ( 1000 );
new Thread (() -> {
System . out . println ( Thread . currentThread (). getName () + " start..." );
lock . lock ();
try {
System . out . println ( Thread . currentThread (). getName () + " running..." );
} finally {
lock . unlock ();
}
}, " 强行插入 " ). start ();
lock . unlock ();
 
 
 
 
强行插入,有机会在中间输出
注意 :该实验不必定总能复现
 
t39 running...
t40 running...
t41 running...
t42 running...
t43 running...
强行插入 start...
强行插入 running...
t44 running...
t45 running...
t46 running...
t47 running...
t49 running... 
 
 
改成公平锁后
ReentrantLock lock = new ReentrantLock ( true );
 
  强行插入,老是在最后输出
t465 running...
t464 running...
t477 running...
t442 running...
t468 running...
t493 running...
t482 running...
t485 running...
t481 running...
强行插入 running...
 
公平锁通常没有必要,会下降并发度,后面分析原理时会讲解
 
 
条件变量
synchronized 中也有条件变量,就是咱们讲原理时那个 waitSet 休息室,当条件不知足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就比如
synchronized 是那些不知足条件的线程都在一间休息室等消息
ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤
 
 
 
使用要点:
  • await 前须要得到锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取从新竞争 lock
  • 竞争 lock 锁成功后,从 await 后继续执行
 
 
例子:
static ReentrantLock lock = new ReentrantLock ();
static Condition waitCigaretteQueue = lock . newCondition ();
static Condition waitbreakfastQueue = lock . newCondition ();
static volatile boolean hasCigrette = false ;
static volatile boolean hasBreakfast = false ;
public static void main ( String [] args ) {
new Thread (() -> {
try {
lock . lock ();
while ( ! hasCigrette ) {
try {
waitCigaretteQueue . await ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
log . debug ( " 等到了它的烟 " );
} finally {
lock . unlock ();
}
}). start ();
new Thread (() -> {
try {
lock . lock ();
while ( ! hasBreakfast ) {
try {
waitbreakfastQueue . await ();
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}
log . debug ( " 等到了它的早餐 " );
} finally {