【并发】一词涵盖了在 Java 平台上的
pom.xml
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.7version>
dependency>
dependencies>
logback.xml
<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%npattern>
encoder>
appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
root>
configuration>
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在
指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器
等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows
下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感
觉是 同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,
一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
引用 Rob Pike 的一段描述:
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
- 并行(parallel)是同一时间动手做(doing)多件事情的能力
以调用方角度来讲,如果
多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如
果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。
计算 1 花费 10 ms
计算 2 花费 11 ms
计算 3 花费 9 ms
汇总需要 1 ms
注意
需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
每个Java程序在启动时,已经创建了一个线程,就是main线程,若想在主线程之外,使用其他线程,则可使用如下方法:
package com.test.demo01;
import lombok.extern.slf4j.Slf4j;
/**
* @author zhangzengxiu
* @date 2022/9/29
*/
@Slf4j(topic = "c.MyThread01")
public class MyTest01 {
/**
* main线程就是java默认的线程
* 两次打印是两个不同的指令去执行的
* 有可能底层是CPU并行执行的
*
* @param args
*/
public static void main(String[] args) {
//创建线程
Thread thread = new Thread() {
@Override
public void run() {
//执行的任务代码
log.debug("thread is running...");
}
};
/*
此时创建好的线程是java程序中的线程,
还未与操作系统的线程对象相关联,
还没有被任务调度器去调度,交给CPU去执行
*/
thread.setName("t1");
/*
启动线程
交给任务调度器,分配时间片,交给CPU去执行
*/
thread.start();
log.debug("main is running...");
}
}
把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)
package com.test.demo01;
import lombok.extern.slf4j.Slf4j;
/**
* @author zhangzengxiu
* @date 2022/9/29
*/
@Slf4j(topic = "")
public class MyTest02 {
/**
* 任务和线程分离
*
* @param args
*/
public static void main(String[] args) {
//任务
Runnable runnable = new Runnable() {
@Override
public void run() {
//要执行的任务
log.debug("runnable is running...");
}
};
//创建线程对象
Thread thread = new Thread(runnable);
thread.setName("t2");
//启动线程
thread.start();
log.debug("main is running...");
}
}
Java8后可使用lambda表达式进行精简。
public static void main(String[] args) {
//任务
Runnable runnable = () -> {
//要执行的任务
log.debug("runnable is running...");
};
}
原理之 Thread 与 Runnable 的关系
分析 Thread 的源码,理清它与 Runnable 的关系
小结
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
不推荐直接操作线程对象,推荐操作Runnable的任务对象
Runnable的run方法返回值是void,并不能方便的在两个线程之间,把一个结果传给另一个线程。
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
Callable:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
有返回值,有抛出异常
Runnable:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
无返回值,无抛出异常
代码
package com.test.demo01;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author zhangzengxiu
* @date 2022/9/29
*/
@Slf4j(topic = "c.Test03")
public class MyTest03 {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("future task is running...");
Thread.sleep(1000);
return 100;
}
});
//创建线程
Thread thread = new Thread(task, "t1");
thread.start();
//一直阻塞等待返回结果
Integer res = task.get();
/*
t1线程和main线程是并行执行的,
main线程会阻塞等待结果
*/
log.debug("{}", res);
}
}
任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist 查看进程
taskkill 杀死进程
ps -fe 查看所有进程
ps -fT -p 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写 H 切换是否显示线程
top -H -p 查看某个进程(PID)的所有线程
jps 命令查看所有 Java 进程
jstack 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
jconsole 远程监控配置
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类
如果要认证访问,还需要做如下步骤
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
机就会为其分配一块栈内存。
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活 (还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
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() 方法调用还是同步的
将上述代码的 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() 方法调用是异步的
小结
sleep
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
睡眠结束后的线程未必会立刻得到执行 (并不是立刻能获得CPU的使用权)
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
具体的实现依赖于操作系统的任务调度器
CPU任务调度器不会把时间片分给阻塞状态的线程,但是会分给就绪状态的线程。
分配时间片时不会考虑阻塞状态的线程,只会分给就绪状态的线程
优先级从1到10,默认优先级5.
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
最终还是由任务调度器来决定的
示例代码
public static void main(String[] args) {
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();
}
示例代码
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() 之后即可
t1.start();
//main线程在此等待t1线程运行完成
t1.join();
以调用方角度来讲,如果
示例2
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
只等待有限时间,就继续执行下面代码
等够时间
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
打断 sleep,wait,join 的线程
可以打断睡眠线程和正在运行线程
阻塞状态的线程,任务调度器不会分配时间片给这些线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
package com.test.demo02;
import lombok.extern.slf4j.Slf4j;
/**
* @author zhangzengxiu
* @date 2022/10/8
*/
@Slf4j(topic = "c.Test01")
public class Test01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
try {
log.debug("sleeping");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
//main线程休眠2秒
Thread.sleep(2000);
//打断线程
t1.interrupt();
/*
sleep wait join 打断标记为false
*/
log.debug("interrupt status={}", 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
若打断标记仍为true,可能是打断标记未被清除的原因。尝试让主线程多睡眠几秒。
package com.test.demo02;
import lombok.extern.slf4j.Slf4j;
/**
* @author zhangzengxiu
* @date 2022/10/10
*/
@Slf4j(topic = "c.Test02")
public class Test02 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.debug("thread status ={}", interrupted);
break;
}
}
}
};
t1.start();
Thread.sleep(1000);
//打断正常运行的线程
t1.interrupt();
}
}
输出
23:02:40 [Thread-0] c.Test02 - thread status =true
打断只是说有人想要你停下来,并不会强制停止线程,线程可自己选择是否停止该线程
代码演示
package com.test.demo02;
import lombok.extern.slf4j.Slf4j;
/**
* @author zhangzengxiu
* @date 2022/10/10
*/
@Slf4j(topic = "c.Test03")
public class Test03 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
private Thread montior;
/**
* 启动监控线程
*/
public void start() {
montior = new Thread("t1") {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
//被打断
log.debug("监控线程被打断,终止线程");
break;
}
//没被打断
try {
Thread.sleep(1000);
log.debug("监控...");
} catch (InterruptedException e) {
e.printStackTrace();
//睡眠过程中被打断,需要重置打断标记
thread.interrupt();
}
}
}
};
montior.start();
}
/**
* 停止监控线程
*/
public void stop() {
montior.interrupt();
}
}
interrupted:会清除打断标记
isInterrupted:不会清除打断标记
package com.test.demo02;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
* @author zhangzengxiu
* @date 2022/10/10
*/
@Slf4j(topic = "c.Test04")
public class Test04 {
public static void main(String[] args) throws InterruptedException {
test();
}
private static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
/*
park方法 被打断后 下次就不会再生效
*/
LockSupport.park();
log.debug("unpark...");
/*
isInterrupted 不会清除打断标记
*/
//log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
/*
isInterrupted 会清除打断标记
park才能重新生效
*/
log.debug("打断状态:{}", Thread.interrupted());
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
park方法,在被打断后,打断标记为真后,后续的线程,则不会生效。
可以将打断标记设为假,后续则可继续生效。可借助 Thread.interrupted()方法
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | 功能说明 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
主线程:默认情况下,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 命令后,不会等待它们处理完当前请求
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为
是可运行)
//TODO
代码实现6种状态
package com.test.demo04;
import lombok.extern.slf4j.Slf4j;
/**
* 共享带来的线程安全问题
*
* @author zhangzengxiu
* @date 2022/10/12
*/
@Slf4j(topic = "c.MyTest01")
public class MyTest01 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 1; i <= 5000; i++) {
count++;
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 1; i <= 5000; i++) {
count--;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count={}", count);
}
}
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理
解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
但多线程下这 8 行代码可能交错运行:
根本原因:
上下文切换引起指令交错,导致多线程访问共享资源时的线程安全问题。
临界区 Critical Section
例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++; }
static void decrement()
// 临界区
{
counter--; }
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized 相当于把临界区中的代码变为串行的
synchronized
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
要保证多个线程对同一个对象使用synchronized
代码演示
package com.test.demo04;
import lombok.extern.slf4j.Slf4j;
/**
* 共享带来的线程安全问题
*
* @author zhangzengxiu
* @date 2022/10/12
*/
@Slf4j(topic = "c.MyTest01")
public class MyTest01 {
static int count = 0;
static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 1; i <= 5000; i++) {
synchronized (obj) {
count++;
}
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 1; i <= 5000; i++) {
synchronized (obj) {
count--;
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count={}", count);
}
}
注意
如果获得到锁的线程,在执行过程中,时间片使用完毕,此时,该线程仍持有该锁,其他线程仍被阻塞在临界区外。被阻塞的线程,CPU不会把时间片分给他们,下次该线程获得时间片时,则可继续执行临界区中的代码,不需要再次抢锁。
当线程执行完成临界区中的代码时,此时才会释放锁,唤醒其他线程去竞争锁。
图示:
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
如果把 synchronized(obj) 放在 for 循环的外面,如何理解?
– 原子性,每次执行完几千次的循环后,才会释放锁资源。
如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?
– 锁对象 ,必须对加同一把锁,不然锁不住。
如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?
– 锁对象,临界区中代码,线程一进行了加锁,线程二没有,就不会被阻塞,会继续运行,则依然线程不安全。
临界区的代码必须对所有线程进行加锁。
面向对象改进
package com.test.demo04;
import lombok.extern.slf4j.Slf4j;
/**
* 共享带来的线程安全问题
* 面向对象思想改进
*
* @author zhangzengxiu
* @date 2022/10/12
*/
@Slf4j(topic = "c.MyTest02")
public class MyTest02 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 1; i <= 5000; i++) {
room.increment();
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 1; i <= 5000; i++) {
room.descrement();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count={}", room.getCount());
}
}
class Room {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public void descrement() {
synchronized (this) {
count--;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
加在成员方法
加在static静态方法
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只能锁对象:
加在成员方法上,锁的是this对象;
加在static静态方法上,锁的是类对象
情况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();
}
成员变量和静态变量是否线程安全?
局部变量是否线程安全?
局部变量线程安全分析
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:![在这里插入图片描述](https://img-blog.csdnimg.cn/ad84da368586472cbd845ac4ba49f563.png#pic_center)
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)
分析:
将 list 修改为局部变量
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);
}
}
那么就不会有上述问题了
分析:
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
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 提供【安全】的意义所在,请体会开闭原则中的【闭】
常见线程安全类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
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 类的实现
Java 对象头
注意:
- synchronized必须是进入同一个对象的monitor才会有上述的效果
- 不加synchronized的对象不会关联监视器,不遵从以上原则
obj锁对象时会与monitor关联。
Thread-1的markword与monitor进行关联,若没有owner,则thread-1与Owner进行关联,并可以执行临界区中的代码;
Thread-2线程进来之后,也会与monitor进行关联,查看owner是否已经有线程关联,若有则与monitor中的EntryList进行关联,进入阻塞队列中,并成为阻塞状态;
当Owner被释放之后,会唤醒阻塞队列中的线程,去竞争owner资源,这是非公平的。
锁对象必须是同一个,否则不是同一个monitor对象。一个Java对象对应一个monitor
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args){
synchronized(lock){
counter++;
}
}
monitorexit是将hashcode等信息重置回去的过程。
Monitor是由操作系统提供的, 每次进入synchronized时,都要获取Monitor锁,成本较高, 会影响程序性能。Java6之后,从使用Monitor锁,改为了使用轻量级锁、偏向锁来优化。
轻量级锁:操作共享资源时,无线程竞争会变为轻量级锁,当出现竞争时,会变为Monitor锁
偏向锁:偏向某个线程使用;
偏向锁被批量撤销时,达到阈值时,默认40次,偏向锁变为轻量级锁,
使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间时错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized
假设两个有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}
Object reference:对象指针,记录锁住对象的地址
lock record 地址 00:用来存储要加锁对象的mark word
交换锁记录里的数据和mark word里的数据,用来表示加锁
01:表示无锁状态
00:表示轻量级锁状态
锁记录内会临时的将对象的hash码等其他信息,解锁时恢复回去。
如果交换的时候时01:表示无锁,则可以替换成功,如果Mark word已经被其他线程改掉了,改成了00,则此次交换失败。
锁重入:自己线程又一次对同一个线程进行加锁。
上图中,上方的Lock Record要交换Mark word里的数据时,交换失败,因为另一个Lock Record已经交换过了,锁对象已经变为了00,但是依然会创建这个Lock Record作重入计数,解锁时,解锁一次去掉一个锁记录。