1.pm.xml依赖如下
<properties>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
dependencies>
2.logback.xml 配置如下
<configuration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback ">
<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>
Java中,线程作为最小调度单位,进程作为资源分配的最小单位
。在winodw中进程是不活动的,只是作为线程的容器。进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同一台计算机的进程通信称为 IPC
(Inter-process communication)不同计算机之间的进程通信,需要通过网络,并遵守共同的协议
,例如 HTTP线程通信相对简单,因为它们共享进程内的内存
,一个例子是多个线程可以访问同一个共享变量
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并行:
小海王左手牵着A女友同时右手牵着B女友。
并发:
老海王分时间安排和不同的女友见面,两个女友看不出任何破绽。时间管理大师!!!
单核 cpu 下,线程实际还是 串行执行
的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片
(windows
下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的
。总结为一句话就是: 微观串行,宏观并行 。
一般会将这种 线程轮流使用 CPU
的做法称为并发, concurrent
多核 cpu下,每个 核(core)
都可以调度运行线程,这时候线程可以是并行的。
concurrent
)是同一时间应对(dealing with)多件事情的能力parallel
)是同一时间动手做(doing)多件事情的能力从方法的角度来讲,如果
注意: 同步子啊多线程中还有另外一层意思,是让多个线程步调一致。
- 设计
多线程可以让方法执行变为异步
的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
- 结论
视频文件需要转换格式等操作比较费时
,这时开一个新线程处理视频转换,避免阻塞主线程。(视频文件格式转换)servlet
也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程。(tomcat)避免阻塞 ui 线程
。(ui)充分利用多核 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 个注意: 需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
- 搭建测试环境: 分别在单核和多核下测试
1.向pom.xml中导入我们的JHM检测包工具
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-coreartifactId>
<version>1.23version>
dependency>
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-generator-annprocessartifactId>
<version>1.23version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.5version>
dependency>
2.编写测试的类
package com.jsxs.sample;
/**
* @Author Jsxs
* @Date 2023/9/29 13:45
* @PackageName:com.jsxs.sample
* @ClassName: MyBenchmark
* @Description: TODO
* @Version 1.0
*/
import java.util.Arrays;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3,time = 1,timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {
static int[] ARRAY = new int[1000_000_00];
static {
Arrays.fill(ARRAY, 1);
}
@Benchmark
public int c() throws Exception {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 0; i < 250_000_00; i++) {
sum += array[0 + i];
}
return sum;
});
FutureTask<Integer> t2 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 0; i < 250_000_00; i++) {
sum += array[250_000_00 + i];
}
return sum;
});
FutureTask<Integer> t3 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 0; i < 250_000_00; i++) {
sum += array[500_000_00 + i];
}
return sum;
});
FutureTask<Integer> t4 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 0; i < 250_000_00; i++) {
sum += array[750_000_00 + i];
}
return sum;
});
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
new Thread(t4).start();
return t1.get() + t2.get() + t3.get() + t4.get();
}
@Benchmark
public int d() throws Exception {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(() -> {
int sum = 0;
for (int i = 0; i < 1000_000_00; i++) {
sum += array[0 + i];
}
return sum;
});
new Thread(t1).start();
return t1.get();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
- 结论
多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换
,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活。多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
1.创建线程:
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
2.使用线程列子
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) {
// 1.构造方法的参数指定名字,推荐
Thread thread = new Thread("t1") {
@Override
public void run() {
log.debug("hello");
}
};
// 2. 开始交给任务处理器支配
thread.start();
}
}
3.输出:
把【线程】和【任务】(要执行的代码)分开
1.创建任务并执行线程
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
2.执行线程
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) {
// 1.创建执行任务,因为是一个接口需要实现方法
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("hello-Runnable");
}
};
// 2.启动线程,并且给线程指定名字
new Thread(runnable,"Runnable").start();
}
}
使用lamda表达式的条件为: 函数式接口(接口中只有一个方法)
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) {
// 假如函数式接口中的方法没有参数,那么直接用()表示,返回值类型是void直接直接写函数的语句就行
new Thread(()->log.debug("hello-lamda"),"lamda").start();
}
}
1. 两者实际上都是使用的Thread的run()方法进行操作的
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况:
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.编写执行任务,并设置此线程执行完毕后的返回值
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("hello-futureTask");
Thread.sleep(1000); //释放资源但不释放锁
return 100;
}
});
// 2.启动线程
new Thread(task,"t3").start();
// 3.调用get()方法会造成_主线程阻塞,同步等待 task 执行完毕的结果⭐⭐
Integer integer = task.get();
// 4. {}占位符,第一个占位符对应第一个参数....
log.debug("结果是:{}",integer);
}
}
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(()->{while (true)log.debug("running1...","t1");}).start();
new Thread(()->{while (true)log.debug("running2...","t2");}).start();
}
}
ps -ef
查看所有的进程信息。ps -ef -p
查看某个进程pid的所有线程。netstat -anp|grep 端口号
查看某一个端口号是否开启。kill
杀死进程。top
按大写H切换是否显示线程。top -HP
查看某个进程pid的所有线程。Java Virtual Machine Stacks (Java 虚拟机栈
)
我们都知道 JVM 中由堆、栈、方法区
所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
方法
调用时所占用的内存
- 单线程_栈与栈帧
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws ExecutionException, InterruptedException {
method1(10);
}
public static void method1(int x){
int y= x+1;
Object o = method2();
System.out.println(o);
}
public static Object method2(){
Object n = new Object();
return n;
}
}
一个线程对应一个栈内存,一个栈内存又多个栈帧组成!
当我们结果运行完毕后,栈帧和栈内存会被释放!!!
- 类加载过程
类加载过程示意图:
①.首先通过一个类的全限定名获取该类的二进制;然后将该二进制流中静态存储结构转化为方法区运行时结构;最后在栈内存中生成该类的class对象,作为该类数据的访问入口。
②.验证文件格式(字节流是否符合、主次版本号是否在虚拟机范围内、常量池中的常量是否右不被支持的类型)、验证元数据(对字节码描述的信息进行语义分析是否有父类)、验证字节码(验证数据流和控制流程序语义是否正确)、验证符号引用(主要是为了确保解析动作能正确执行)。
③.为类中的静态变量分配内存并初始化默认值,这些内存都是在方法区进行分配的;准备阶段不分配类中实列变量的内存
,实列变量将会在对象实列化时随着对象一起分配到堆中。
④.主要完成符号引用到直接引用的转换动作
⑤.初始化时类加载的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码。
- 栈帧图解
①通过二进制流转换成方法区运行时结构;②当我们启动项目之后会获取到时间片就会调用main线程(main内存);③main线程会生成main栈帧;④程序计算器负责指挥先main方法执行然后method1最后method2,⑤当程序运行结束后,清理顺序是先methos2、methods1最后是main方法。
- 多线程下_栈与栈帧
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.t1线程
new Thread("t1"){
@Override
public void run() {
method1(20);
}
}.start();
// 2.main线程
method1(10);
}
public static void method1(int x){
int y= x+1;
Object o = method2();
System.out.println(o);
}
public static Object method2(){
Object n = new Object();
return n;
}
}
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码。
cpu
时间片用完。垃圾回收
。优先级
的线程需要运行。sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法。当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。Java 中对应的概念就是程序计数器(Program Counter Register)
,它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。
程序计数器
、虚拟机栈中每个栈帧的信息
,如局部变量
、操作数栈
、返回地址
等。package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception{
Thread thread = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
// 当线程t1被时间片获取的时候,就会执行打印
log.debug("running...");
// 这里模拟读取文件需要十秒种
Thread.sleep(10000);
}
};
System.out.println(thread.getState()); //查看线程状态
// thread.run(); //假如我们使用直接调用run,也能够启动线程并打印run方法里面的数据,但是不会生成t1线程,所有的操作都是main线程在操纵 (也就是说同步等待)
thread.start(); // 使用这个开启线程之后,会生成一个t1线程去执行 run方法;main线程去执行其他的事情 (也就是说会异步)
log.debug("do other things .....");
}
}
结论:
sleep
会让当前线程从 Running
进入 Timed Waiting
状态(阻塞)interrupt
方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
未必会立刻
得到执行TimeUnit 的 sleep
代替 Thread 的 sleep
来获得更好的可读性
- sleep 进入阻塞状态…
1.调用sleep()会进入阻塞状态.....
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception{
Thread thread = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
// 当线程t1被时间片获取的时候,就会执行打印
log.debug("running...");
// 这里模拟读取文件需要十秒种
Thread.sleep(2000);
}
};
thread.start(); // 开启t1线程
log.debug("t1 state:{}",thread.getState()); // 获取t1线程的状态
Thread.sleep(500); // 主线程阻塞 500毫秒
log.debug("t1 state:{}",thread.getState()); // 再次获取t1线程的状态 RUNNABLE
}
}
- sleep打断 _interrupt()
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception{
Thread thread = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
log.debug("进入睡眠 .... ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// 当线程睡眠被打断之后,会报异常....
log.debug("wake up ....");
e.printStackTrace();
}
}
};
thread.start();
Thread.sleep(1000); //主线程睡眠 1s
log.debug("开始唤醒.....");
thread.interrupt(); // 主线程睡眠1s之后,执行interrupt打断t1线程进入 唤醒状态
}
}
Running
进入 Runnable
就绪状态,然后调度执行其它线程
- 验证yield() 礼让
正常情况下我们不加优先级和yeild等做干涉,那么count1 和 count 2的最后结果将会相差不大。
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception {
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.start();
t2.start();
}
}
- 优先级设置
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception {
Runnable task1 = () -> {
int count = 0;
for (; ; ) { // 死循环
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (; ; ) {
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();
}
}
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用 yield 或 sleep
来让出cpu的使用权给其他程序。
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception {
while (true) {
try {
Thread.sleep(50); // 加时间进行间隔,减少一直对cpu的占用
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在单核cpu下,不加等待时间的cpu占用率达到了98%,如果加上了我们的时间做间隔限制,我们的cpu资源占用率降低到了3%。
为什么需要join
下面的代码执行,打印r是什么?
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
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("开始");
try {
Thread.sleep(1); // 这里需要进行睡眠的操作
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
},"t1");
t1.start(); // 开启
log.debug("结果为:{}", r); // 结果为0,因为t1先睡眠了一会,所以结果为0。
log.debug("结束");
}
}
t1
是并行执行的,t1 线程需要 1 秒之后才能算出 r=10。所以只能打印出 r=0
。解决方法
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
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("开始");
try {
Thread.sleep(1); // 这里需要进行睡眠的操作
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
},"t1");
t1.start(); // 开启
t1.join(); // 同步等待阻塞当这个线程阻塞结束之后,后面的才能运行
log.debug("结果为:{}", r); // 因为当阻塞释放后才会运行,所以结果为10
log.debug("结束");
}
}
以调用方角度来讲,如果
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
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(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
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);
}
}
线程1和线程2同时开启线程,首先线程1会停顿1秒,然后线程2会停顿两秒,又因为不是在主线程进行停顿,所以当线程1停顿一秒的时候,线程2也已经停顿一秒了,再只需要等待一秒我们就可以成功了。所以总功用时2秒。
- 打断 sleep 的线程, 会清空打断状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例.
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000); //
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(100);
t1.interrupt();
log.debug(" 打断状态: {}", t1.interrupted()); // interrupted() 会清空打断状态 true变成false
}
}
使用interrupted() 打断非运行(Waitting)的线程那么会抛出异常且清空状态!!!;假如使用Interrupt那么会爆出异常但不会清空状态。
- 打断正常运行的线程
打断正常运行的线程, 不会清空打断状态 ( IsInterrupted() 不会清空状态)
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
test2();
}
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; // 我们被打断之后,这个while并不是说不再运行了
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt(); //执行打断 不清空状态
}
}
使用Interrupt()打断正常的运行不会触发异常的操作,也不会清空打断状态!!!!;假如使用interrupted()不会出现异常但是会清空打断状态。
在一个线程T1种如何优雅终止线程T2? 这里的优雅指的是给T2一个料理后事的机会。
错误思路
使用线程对象的 stop() 方法停止线程
使用System.exit(int) 方法停止线程
- 两阶段终止_interrupt分析
- 两阶段终止_interrupt 异常处理
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination termination = new TwoPhaseTermination();
termination.start(); //开启监控
Thread.sleep(100); // 休眠
termination.stop(); // 停止监控
}
}
@Slf4j(topic = "c.Sync")
class TwoPhaseTermination{
private Thread monitor;
// 启动监控线程
public void start(){
monitor=new Thread(){
@Override
public void run() {
while (true){
Thread thread = Thread.currentThread(); //获取当前线程对象
if (thread.isInterrupted()) {
log.debug("料理后事.....");
break;
}
try {
Thread.sleep(10000); //1. 非正常打断,清空运行状态。 比如说:打断状态为真清空后变成假。⭐
log.debug("执行监控功能...."); // 2. 正常打断,不清空运行状态⭐⭐
} catch (InterruptedException e) {
log.debug("被interrupt给打断");
currentThread().interrupt(); // 非正常打断的情况下,再次进行打断⭐⭐
e.printStackTrace();
}
}
}
};
monitor.start();
}
// 停止监控线程
public void stop(){
monitor.interrupt(); //使用的是 interrupt() 不会清空状态
}
}
假如我们使用两阶段终止的情况下,直接打断正常运行(RUNNING)的线程不会清空状态;假如遇到了非正在运行(Waitting)的线程那么会清空状态。
- 一个 park 在线程中
打断 park 线程, 不会清空打断状态
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
test3();
}
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();
Thread.sleep(500);
t1.interrupt();
}
}
- 两个park 在线程中
如果打断标记已经是 true, 则 再调用一个park 会失效(就是变成未被打断所以一直运行)。
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park(); // 进行打断处理
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().interrupted()); // interrupted 返回状态且清空打断状态(true->false;false->true), isInterrupted 返回状态不清空打断状态
LockSupport.park(); // 会造成失效也就是 打断+打断=未打断。假如使用interrupted会先清空打断状态,然后再打断。
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
}
}
interrupted 返回状态且清空打断状态(true->false;false->true), isInterrupted 返回状态不清空打断状态。正常情况下会继续打印第二个park()下的打印的;但是我们使用interrupted之后就不会打印了。
还有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁。
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
。
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception {
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
try {
Thread.sleep(2000); // 守护线程设置两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000); // 主线程睡眠1s
log.debug("运行结束...");
}
}
注意:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的
Acceptor
和Poller
线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理当前请求。
NEW
线程刚被创建,但是还没有调用 start() 方法RUNNABLE
当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED
, WAITING
, TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述TERMINATED
当线程代码运行结束.package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread("t1") { // 1. NEW状态
@Override
public void run() {
log.debug("running..");
}
};
// 2.阻塞状态
Thread t2 = new Thread("t2") { //2. Runnable (可运行、运行、阻塞)
@Override
public void run() {
while (true) {
}
}
};
t2.start();
Thread t3 = new Thread("t3") { //3. Runnable (运行状态)
@Override
public void run() {
log.debug("running..");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Sync.class){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (Sync.class){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state : {}",t1.getState()); //打印NEW,因为没有启动线程
log.debug("t2 state : {}",t2.getState()); //打印Runnable,因为处于运行
log.debug("t3 state : {}",t3.getState()); //打印Terminate,因为主线程先比t3线程先执行完毕
log.debug("t4 state : {}",t4.getState()); //打印TIMED_WAITING,因为有时限的在休眠
log.debug("t5 state : {}",t5.getState()); //打印WAITING,因为一直在等待
log.debug("t6 state : {}",t6.getState()); //打印BLOCKED,因为获取不到锁资源,t4一直在占用
}
}
阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示
附:华罗庚《统筹方法》
统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?
- 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
- 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
- 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,可以引出生产管理等方面有用的方法来。
水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:
从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做。
是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。
洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:
看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。
这里讲的主要是时间方面的事,但在具体生产实践中,还有其他方面的许多事。这种方法虽然不一定能直接解决所有问题,但是,我们利用这种方法来考虑问题,也是不无裨益的。
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread("user1") {
@Override
public void run() {
log.debug("洗水壶.... 2s");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("烧水壶...3s");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
new Thread("user2"){
@Override
public void run() {
log.debug("洗茶壶.... 1s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("洗茶杯.... 2s");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("拿茶叶.... 1s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t1.join(); // 等待t1线程结束之后,进行泡茶
log.debug("user2 泡茶");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
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 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
但多线程下这 8 行代码可能交错运行:
出现负数的情况
首先线程2获取静态变量并减1,这时候还没写入主存呢,然后遇到了上下文的切换,线程1读取的数据是0,然后赋值为1并写入主存。这时候线程一的时间片用完了,轮到线程2进行操作了。继续接着执行线程2写入主存的操作,也就是说会将原本主存中的1覆盖为-1.
出现正数的情况: (线程1会覆盖线程2)
如果存在对共享资源的多线程读写操作
,称这段代码块为临界区static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
本次课使用阻塞式的解决方案:synchronized
,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意:
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized: 释放资源但不释放锁!!!
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
解决
package com.jsxs.utils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.io.FileReader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
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);
}
}
- 理论概述
对象
) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人t1 进入了这个房间,并锁住了门拿走了钥匙
,在门内执行count++ 代码t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他
。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码.注意: 当临界区的代码执行完毕之后才会释放锁。
- 用图来理解
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题
把需要保护的共享变量放入一个类
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
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());
}
static class Room {
int value = 0;
public void increment() {
synchronized (this) { // 改进锁对象,锁的是同一个类
value++;
}
}
public void decrement() {
synchronized (this) { //改进锁对象,锁的是同一个类
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
}
添加成员方法上,相当于锁的是this对象
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) {
}
}
}
不加 synchronzied
的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)。没有办法保证他的原子性的。
起始就是考察 synchorized 锁住的是哪个对象。
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
static class Number {
public synchronized void a() { // 锁的是: Number对象⭐
log.debug("1");
}
public synchronized void b() { // 锁的是: Number对象⭐
log.debug("2");
}
}
}
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
static class Number {
public synchronized void a() { // 锁的是: Number对象
try {
Thread.sleep(1000); // ⭐
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() { // 锁的是: Number对象
log.debug("2");
}
}
}
①: 1秒后输出 1 2 。 ②: 0秒输出2然后1s后输出1
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
new Thread(() -> {
n1.c();
}).start();
}
static class Number {
public synchronized void a() { // 锁的是: Number对象
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() { // 锁的是: Number对象
log.debug("2");
}
public void c() { // 未加锁 ⭐
log.debug("3");
}
}
}
输出结果: ①.0秒后输出3,然后1秒后输出1 2。②.0秒后输出3和2然后1秒后输出0
不存在互斥的情况....
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number(); // 锁的是 n1 ⭐
Number n2 = new Number(); // 所得是 n2 ⭐
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
static class Number {
public synchronized void a() { // 锁的是: Number对象
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() { // 锁的是: Number对象
log.debug("2");
}
}
}
锁静态方法相当于锁住的是整个类
类锁和对象锁不互斥
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
static class Number {
public static synchronized void a() { // 锁的是: Number对象 ⭐
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() { // 锁的是: Number对象
log.debug("2");
}
}
}
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n1.b();
}).start();
}
static class Number {
public static synchronized void a() { // 锁的是: Number对象 ⭐
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public static synchronized void b() { // 锁的是: Number对象 ⭐
log.debug("2");
}
}
}
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
static class Number {
public static synchronized void a() { // 锁的是: Number对象
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() { // 锁的是: Number对象
log.debug("2");
}
}
}
一定先是 先2然后1秒后1
两个类锁,所以互斥
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws InterruptedException {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
static class Number {
public static synchronized void a() { // 锁的是: Number对象
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public static synchronized void b() { // 锁的是: Number对象
log.debug("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 // 赋值为10
2: istore_0
3: iinc 0, 1 // 做自增1
6: return // 返回结果
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
- 局部变量的引用_堆中同一个实列 (成员变量)
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) throws InterruptedException {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
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);
}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list成员变量
- method3 与 method2 分析相同
- 局部变量的引用_堆中不同实列
引用堆中各自的引用对象。而不是共享的
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) throws InterruptedException {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
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 中传递过来的,与 method中引用同一个对象
- method3 的参数分析与 method2 相同
- 方法访问修饰符带来的思考
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) throws InterruptedException {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list); //用的是同一个局部变量
method3(list); //
}
}
public void method2(ArrayList<String> list) { // ⭐
list.add("1");
}
public void method3(ArrayList<String> list) { //⭐
list.remove(0);
}
}
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class Sync {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) throws InterruptedException {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public 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
或 final
提供【安全】的意义所在,请体会开闭原则中的【闭】。因为被 private 修饰的方法不能被子类继承,所以不会出现重写method3的情况了。假如子类写了同名的method3,根据双亲委派机制也不会执行子类的method3
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
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);
}
squenceDiagram
participant t1 as 线程1
participant t1 as 线程2
participant table
t1 ->> table : get("key")==null
t2 ->> table : get("key")==null
t2 ->> table : put("key",v2)
t1 ->> table : put("key",v1)
#这是后会产生覆盖,也就是线程安全问题。v1覆盖v2
String、Integer 等都是不可变类,因为其内部的状态(属性)不可以改变,因此它们的方法都是线程安全的,有同学或许有疑问,String 有 replace,substring
等方法【可以】改变值啊。实质上是NEW了一个新的对象,那么这些方法又是如何保证线程安全的呢?
replace和substring实质上就是NEW新建了一个对象。
- 例1: Controller层
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Author Jsxs
* @Date 2023/9/29 13:20
* @PackageName:com.jsxs.utils
* @ClassName: Sync
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.Sync")
public class MyServlet extends HttpServlet {
// 是否安全? -> 不是 (因为HashMap不是线程安全的类)
Map<String, Object> map = new HashMap<>();
// 是否安全? -> 是 (因为是线程安全类)
String S1 = "...";
// 是否安全? -> 是 (因为是线程安全类) (被final修饰的引用变量。引用值(S1)不能改变 S2=S2.replace('g','1')->报错; 但是值可以改变 S2.replace('g','1') ->不报错)
final String S2 = "...";
// 是否安全? -> 不是 (因为不是线程安全类)
Date D1 = new Date();
// 是否安全? -> 不是 (因为不是线程安全类) (被final修饰的引用变量。引用值(d2)不能改变,但是值可以变)
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 调用上面的方法
}
}
例2: Serverimpl层
public class MyServlet extends HttpServlet {
// 是否安全? ->不是线程安全因为server层存在不安全 ⭐⭐
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++;
}
}
- 列三: 切面的时候
@Aspect
@Component
public class MyAspect {
// 是否安全? 不安全,因为Spring默认切面是单列的,然后里面的成员变量就会被共享。
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));
}
}
- 列四 : MVC ✅
public class MyServlet extends HttpServlet {
// 是否安全 -是安全的因为private UserDao userDao = new UserDaoImpl();安全的 ⭐⭐⭐
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 -虽然是成员变量,但是因为底部的Conn是安全的, ⭐⭐
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: MVC
public class MyServlet extends HttpServlet {
// 是否安全: 不安全因为server层
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全: 不安全因为dao层
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();
}
}
- 列六 MVC✅
public class MyServlet extends HttpServlet {
// 是否安全 : 不安全因为server层消除了安全隐患 ⭐
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 {
// 是否安全? 这里会出现安全问题,但是经过server层就不会了 ⭐
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() {
// 是否安全 : 不能确定是否安全,看foo
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 类的实现。
为什么String被设置成final,主要是为了避免外星方法。保证线程安全。
测试下面代码是否存在线程安全问题,并尝试改正。
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
/**
* @Author Jsxs
* @Date 2023/10/2 16:17
* @PackageName:com.jsxs.utils
* @ClassName: ExerciseSell
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.sell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
TicketWindow ticketWindow = new TicketWindow(1000); //初始化为1000张票
List<Integer> list = new Vector<>(); // 统计所有已经卖出的票
List<Thread> threads = new ArrayList<>(); // 统计所有的线程集合
for (int i = 0; i < 5000; i++) { // 模拟循环2000个人去抢票
Thread thread = new Thread("第" + i + "个顾客") {
@Override
public void run() {
// 顾客买票 : 买票的数目为 0~5
int amount = ticketWindow.sell(ExerciseSell.randomAmount());
try {
Thread.sleep(randomAmount()); // 放大事故发生的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(amount);
}
};
threads.add(thread);
thread.start();
}
// 统计票数之前,先等待线程结束
for (Thread thread : threads) {
thread.join();
}
// 统计卖出的票数和剩余的票数
log.debug("余票: {}",ticketWindow.getCount());
log.debug("卖出去的票数: {}",list.stream().mapToInt(i->i).sum());
}
// 随机 1~5 张
static Random random = new Random();
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<>();
需要在临界区上加锁,就能够解决!
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
/**
* @Author Jsxs
* @Date 2023/10/2 16:17
* @PackageName:com.jsxs.utils
* @ClassName: ExerciseSell
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.sell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
TicketWindow ticketWindow = new TicketWindow(1000); //初始化为1000张票
List<Integer> list = new Vector<>(); // 统计所有已经卖出的票
List<Thread> threads = new ArrayList<>(); // 统计所有的线程集合
for (int i = 0; i < 5000; i++) { // 模拟循环2000个人去抢票
Thread thread = new Thread("第" + i + "个顾客") {
@Override
public void run() {
// 顾客买票 : 买票的数目为 0~5
int amount = ticketWindow.sell(ExerciseSell.randomAmount());
try {
Thread.sleep(randomAmount());
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(amount);
}
};
threads.add(thread);
thread.start();
}
// 统计票数之前,先等待线程结束
for (Thread thread : threads) {
thread.join();
}
// 统计卖出的票数和剩余的票数
log.debug("余票: {}",ticketWindow.getCount());
log.debug("卖出去的票数: {}",list.stream().mapToInt(i->i).sum());
}
// 随机 1~5 张
static Random random = new Random();
public static int randomAmount(){return random.nextInt(5)+1;}
}
// 售票窗口
class TicketWindow {
private int count; // 没有添加static,却也是共享变量因为有 get 或 set 对成员变量
public TicketWindow(int count) {
this.count = count;
}
// 获取余票数量
public int getCount() {
return count;
}
// 售票操作 : 临界区
public synchronized int sell(int amount) { // 对我们的临界区进行加锁的操作
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
/**
* @Author Jsxs
* @Date 2023/10/2 16:17
* @PackageName:com.jsxs.utils
* @ClassName: ExerciseSell
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.sell")
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) {
synchronized (this){
if (this.money > amount) {
this.setMoney(this.getMoney() - amount); //这里有两个受保护的对象
target.setMoney(target.getMoney() + amount); // 这里有两个受保护的对象
}
}
}
}
// 转账操作 : 临界区
public void transfer(Account target, int amount) {
synchronized (Account.class){ // 因为有多个受保护的对象,不是一个所以需要对整个类加锁
if (this.money > amount) {
this.setMoney(this.getMoney() - amount); //这里有两个受保护的对象
target.setMoney(target.getMoney() + amount); // 这里有两个受保护的对象
}
}
}
以32位虚拟机为例
普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 (没有启用偏向锁 ) | 01 | Normal (正常状态) |
|-------------------------------------------------------|--------------------|
| thread:23 (线程id) | epoch:2 | age:4 | biased_lock:1 (启用偏向锁 ) | 01 | Biased (偏向锁状态) |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked (轻量级锁)|
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked (重量级锁) |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
64 位虚拟机 Mark Word
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked (轻量级锁) |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
Monitor 被翻译为监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
Monitor 结构如下
Owner 为 null
。Thread-2
执行 synchronized(obj) 就会将 Monitor
的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
。Thread-3,Thread-4,Thread-5
也来执行 synchronized(obj),就会进入 EntryList BLOCKED
(阻塞队列)Thread-2
执行完同步代码块的内容,然后唤醒 EntryList
中等待的线程来竞争锁,竞争时是非公平的。注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
对应的字节码为
public static void main(java.lang.String[]);
descriptor:([Ljava/lang/String;)V
flags:ACC_PUBLIC,ACC_STATIC
Code:
stack=2,locals=3,args_size=1
0:getstatic #2 // <- lock引用 (synchronized开始)
3:dup
4:astore_1 // lock引用 -> slot 1
5:monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6:getstatic #3 // <- i
9:iconst_1 // 准备常数 1
10:iadd // +1
11:putstatic #3 // -> i
14:aload_1 // <- lock引用
15:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16:goto 24
19:astore_2 // e -> slot 2
20:aload_1 // <- lock引用
21:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22:aload_2 // <- slot 2 (e)
23:athrow // throw e
24:return
Exception table:
from to target type
6 16 19any
19 22 19any
LineNumberTable:
line 8:0
line 9:6
line 10:14
line 11:24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0args[Ljava/lang/String;
StackMapTable:number_of_entries=2
frame_type=255 /* full_frame */
offset_delta=19
locals=[class "[Ljava/lang/String;",
class java/lang/Object]
stack=[
class java/lang/Throwable]
frame_type=250 /* chop */
offset_delta=4
故事角色
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字.
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Jsxs
* @Date 2023/10/2 16:17
* @PackageName:com.jsxs.utils
* @ClassName: ExerciseSell
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.sell")
public class ExerciseTransfer {
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块 B
}
}
}
Mark Word
。Object reference
指向锁对象,并尝试用 cas
替换 Object 的 Mark Word
,将 Mark Word
的值存入锁记录。如果 cas 失败,有两种失败情况
其它线程已经持有了该 Object 的轻量级锁 也就是(00)
,这时表明有竞争,进入锁膨胀过程。自己执行了 synchronized 锁重入(锁中套锁)
,那么再添加一条 Lock Record 作为重入的计数。synchronized
代码块(解锁时)如果有取值为 null
的锁记录,表示有重入。这时重置锁记录,表示重入计数减一;也就是恢复。cas 将 Mark Word 的值恢复给对象头
原本是01,就恢复成01。
总结: 轻量级锁实质上就是交换对象头中的 Mark Word 的操作。
如果在尝试加轻量级锁的过程中,CAS 操作无法成功
,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
Object
对象申请 Monitor
锁,让 Object 指向重量级锁地址 (10)
。Monitor
的 EntryList BLOCKED
(阻塞队列)Thread-0
退出同步块解锁时,使用 cas
将 Mark Word
的值恢复给对象头,会出现失败(因为在恢复前Thread -1就让object成为01了
)。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋重试成功的情况
- 自旋重试失败的情况
也就是说线程2一直在自旋但是线程一一直不释放锁,最终自旋失败只能进入阻塞队列中去。
Java 6
之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7
之后不能控制是否开启自旋功能轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6
中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
列如:
重复加锁,每次锁重入的时候都要进行锁重入CAS的操作
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized (obj) { // 同步块 C
}
}
回忆一下对象头格式
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 (没有启用偏向锁 ) | 01 | Normal (正常状态) |
|-------------------------------------------------------|--------------------|
| thread:23 (线程id) | epoch:2 | age:4 | biased_lock:1 (启用偏向锁 ) | 01 | Biased (偏向锁状态) |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked (轻量级锁)|
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked (重量级锁) |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
一个对象创建时:
如果开启了 偏向锁(默认开启101),那么对象创建后,markword 值为 0x05 即最后 3 位为 101
,这时它的 thread、epoch、age 都为 0
。
偏向锁是默认是延迟(默认两秒)
的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数- XX:BiasedLockingStartupDelay=0
来禁用延迟。
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001
,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值。
- 正常情况下: 我们不能查看消息头
1.我们需要先导入依赖
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.10version>
dependency>
2.测试如下
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* @Author Jsxs
* @Date 2023/10/3 11:41
* @PackageName:com.jsxs.utils
* @ClassName: TestBiased
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test")
public class TestBiased {
public static void main(String[] args) {
// 使用jol-core的包获取整个消息头的信息
System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());
try {
Thread.sleep(4000); // 因为偏向锁具有延迟性,所以我们先延迟四秒再看
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用jol-core的包获取整个消息头的信息
System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());
}
}
class Dog {
}
- 测试加锁前后: 我们对虚拟机参数做了修改
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* @Author Jsxs
* @Date 2023/10/3 11:41
* @PackageName:com.jsxs.utils
* @ClassName: TestBiased
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test")
public class TestBiased {
public static void main(String[] args) {
// 使用jol-core的包获取整个消息头的信息
Dog d = new Dog();
System.out.println("加锁前: ");
System.out.println(ClassLayout.parseInstance(d).toPrintable());
synchronized (d){
System.out.println("加锁时: ");
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
System.out.println("解锁后: ");
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
}
class Dog {
}
注意: 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
- 测试禁用
在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking
禁用偏向锁。
优先级: 有偏向锁优先偏向锁,否则第二就是轻量级锁
输出
11:13:10.018 c.TestBiased [t1] - synchronized 前 (正常状态)
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中 (轻量级锁)
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
- 测试 hashCode
调用了对象的 hashCode
,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
.
hashCode
Monitor
中记录 hashCode
在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前 (正常锁)
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中 (轻量级锁)
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后 (正常锁)
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
使用hashCode之后,偏向锁会失效。
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
加等待的主要原因是先不存在竞争让线程1解锁之后,再进行唤醒线程2.
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
* @Author Jsxs
* @Date 2023/10/3 11:41
* @PackageName:com.jsxs.utils
* @ClassName: TestBiased
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
synchronized (d) { //这里加的时偏向锁
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
synchronized (TestBiased.class) {
TestBiased.class.notify(); // 在这里等待唤醒
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class) {
try {
TestBiased.class.wait(); // 在这里进行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
}, "t2");
t2.start();
}
}
class Dog {
}
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 (偏向锁)
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 (偏向锁)
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000 (轻量级锁)
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (正常锁)
会造成 偏向级锁转换为重量级锁
public static void main(String[] args) throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
try {
d.wait(); //调用睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t1");
t1.start();
new Thread(() -> {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify(); // 调用唤醒
}
}, "t2").start();
}
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 偏向锁
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 偏向锁
[t2] - notify 重量级锁
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2
,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过 20 次
后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.Vector;
/**
* @Author Jsxs
* @Date 2023/10/3 11:41
* @PackageName:com.jsxs.utils
* @ClassName: TestBiased
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
// 将30个偏向锁偏向t1
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
synchronized (list) {
list.notify(); // 最后唤醒 t2
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait(); // 存在这个撤销锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
// 将锁偏向给t2,t1的偏向锁相当于全部撤销
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t2");
t2.start();
}
}
class Dog {
}
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============> 以上偏向于t1 ⭐
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
======>以上经过20论反复反复撤销,达到20的阈值就会出现重现偏移锁 ⭐
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
当撤销偏向锁阈值超过 40次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
package com.jsxs.utils;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.Vector;
import java.util.concurrent.locks.LockSupport;
/**
* @Author Jsxs
* @Date 2023/10/3 11:41
* @PackageName:com.jsxs.utils
* @ClassName: TestBiased
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test")
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
test4();
}
static Thread t1, t2, t3;
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
// 线程1循环39次,加 39个线程偏向锁
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
// 线程2会发生批量重偏向(前20个撤销) 20~39进行批量重偏向 ⭐
t2 = new Thread(() -> {
LockSupport.park(); // 等待
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
// 这里会继续撤销20个重偏向锁。⭐
t3 = new Thread(() -> {
LockSupport.park(); // 等待
log.debug("===============>3 ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t3");
t3.start();
t3.join();
// 第41个偏向锁.
log.debug(ClassLayout.parseInstance(new Dog()).toPrintable()); // 我们再打印不应该是偏向锁了。应该是01重量级
}
}
class Dog {
}
锁消除:
package com.jsxs.sample;
/**
* @Author Jsxs
* @Date 2023/9/29 13:45
* @PackageName:com.jsxs.sample
* @ClassName: MyBenchmark
* @Description: TODO
* @Version 1.0
*/
import java.util.Arrays;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++; // 不加锁进行 ++
}
@Benchmark
public void b() throws Exception {
Object object= new Object(); // 局部变量object
synchronized (o) { // 加锁后进行 ++
x++;
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
我们发现加锁和不加锁相差不大! 主要原因是因为JVM进行了优化加锁,
JIT: 会对我们的字节码进行即时编译进行优化,因为分析道局部变量object逃离不掉作用于的范围,也就是说是私有的,所以加锁会没有任何意义,所以JIT会给我们优化掉。
-XX:-EliminateLocks 关掉JIT即时编译功能
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。