预见猿份
主题
首页面试题在线工具关于我们老苗一对一私教学员评价
实战项目
项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通Java数据结构与算法Java 并发编程(JUC)实战课程老苗一对一私教学员评价blog
blog
  • Java 并发编程(JUC)实战课程

    • 课程介绍
    • 01 多线程入门
    • 02 线程的常用方法
    • 03 线程池
    • 04 synchronized教程
    • 05 ReentrantLock教程
    • 06 读写锁
    • 07 JMM教程
    • 08 CAS教程
    • 09 wait&notify
    • 10 常用的并发集合
    • 11 异步任务控制
    • 12 常用并发工具







----- 到底线了 -----

第4章 synchronized教程 ​

2.1 synchronized ​

2.1.1 什么是线程安全 ​

思考程序中的问题 ​

要理解线程安全首先来看一个程序,思考它的输出结果是什么?

案例:

共100个线程向同一个sum变量执行sum++累加操作,预期程序执行后sum的值为100,如下图:

ScreenShot_2026-09-20_102136_547.png

程序如下:

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {
	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);
	//共享变量
	static int sum=0;
	//测试线程安全问题
	public static void test_threadsafe1(){
		ExecutorService threadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				sum++;
			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
	//等待线程池中的线程完成
	private static void shutdown(ExecutorService threadPool) {
		//线程池结束
		threadPool.shutdown();
		while (!threadPool.isTerminated()) {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		test_threadsafe1();
	}


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

按照预期执行程序后sum的值是100,执行结果和预期不一致。

什么是线程安全 ​

多线程执行程序,结果与预期不一致,程序是线程不安全的。

多线程执行程序,结果与预期一致,程序是线程安全的。

分析线程不安全的原因 ​

为什么多个线程访问同一个变量就出现问题了?下边我们分析问题的原因。

并发执行状态下CPU时间片在多个线程上来回切换,每个线程的执行顺序没有规律,sum++ 虽然是一条语句但它却对应了多条字节码指令,如下:

1、取出当前sum的值

2、计算sum+1的值

3、将sum+1的值赋值给sum

假设线程1执行完第1步被线程2抢过CPU开始执行,此时线程1和线程2取到的sum值都为0,线程1完成一次累加后将累加结果赋值给sum变量,线程2完成一次累加后也将累加结果赋值给sum变量,此时线程2覆盖了线程1的累加和。

如下图:

ScreenShot_2026-09-20_102334_553.png

根据上边的分析,出现线程不安全的原因:多线程向共享变量并发写操作。

什么是共享变量?

共享变量顾名思义是可以被多线程共享的变量,多线程共享堆内存,所以共享变量是指堆中存储的对象成员变量、静态变量、数组元素等。关于共享变量的详细理解请参考”变量对线程安全的影响“章节。

2.1.2 synchronized入门 ​

如何消除竞态条件 ​

出现线程不安全的原因:

  1. 多线程向共享变量写操作。
  2. 多线程向共享变量并发写或叫异步写。

这里有一个重要的概念就是竞态条件,多线程对共享资源存在竞争的写操作对写的顺序敏感这表示存在竞态条件,由于存在竞态条件会导致线程不安全,消除竞态条件即可解决线程不安全的问题。

如何来解决问题呢?

1)不访问共享资源

如果改为多线程不访问同一个sum变量则还需要修改程序设计。

2)异步改为同步

我们可以让多线程异步访问共享变量改为同步访问共享变量也可以解决这个问题。

“多线程同步访问共享资源” 中同步是什么意思?同步是指当一个线程正在访问共享资源,其它线程需要等待其访问完成后再去访问共享资源,同步访问也可以理解为串行访问,即一次只允许一个线程访问共享资源,如下图:

ScreenShot_2026-09-20_102413_972.png

上图中,线程1和线程2虽然访问了共享资源,但是它们是同步访问(也即串行访问),不会出现对共享资源的竞争写操作。

synchronized实现同步 ​

存在竞态条件的代码叫临界区,sum++为临界区代码,将synchronized标记在临界区,这样临界区的代码可实现同步执行。

synchronized语法格式如下:

synchronized(锁){
    // 临界区代码
}
1
2
3

1、多线程执行临界区代码,需要获取锁方可执行临界区代码。

2、假设线程1先获取了锁,执行临界区完成后释放锁,其它线程才可以去争抢锁。

3、假设线程2抢到了锁,执行临界区完成后释放锁,其它线程才可以去争抢锁。

以上过程说明synchronized实现了同步执行。

代码如下:

//定义一个锁对象
Object lock = new Object();
//多线程执行sum++需要获取lock锁,大括号内的是同步代码块
synchronized (lock){
	sum++;
}
1
2
3
4
5
6

多线程同步执行sum++,消除了竞态条件,实现了线程安全。

1、多线程试图执行sum++,需要获取lock锁方可执行。

2、假设线程1先获取了lock,执行sum++完成后释放锁,其它线程才可以去争抢锁。

3、假设线程2抢到了lock锁,执行sum++完成后释放锁,其它线程才可以去争抢锁。

完整代码如下:

//synchronized入门程序
	public static void test_synchronized_first(){
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//定义一个锁
		Object lock = new Object();
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				//多线程执行sum++需要获取lock锁
				synchronized (lock){
					sum++;
				}
			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		logger.debug("sum="+sum);

	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行程序,预期sum=100,结果sum=100,线程安全。

关于synchronized工作原因稍后介绍。

2.1.3 synchronized工作原理 ​

锁与对象的关系 ​

synchronized是如何实现同步执行临界区代码的?

synchronized的语法如下:

synchronized(锁){
    // 临界区代码
}
1
2
3

同步执行即串行执行,即一个线程执行完成后另一个线程去执行,线程需要获取锁才能执行临界区的代码,多个线程不能同时获取同一个锁,一个线程获取了锁,另一个线程必须等待锁释放了才有机会获取锁,这样就保证了临界区的代码被同步执行。

同步执行的关键在于多线程不能同时获取同一个锁。

锁是操作系统层面的东西,在Java中一个对象会绑定一个锁,在对象的头部记录锁信息,它们是一对一关系,所以在synchronized中指定锁绑定的对象(即锁对象),如下代码所示:

synchronized(锁对象){
    // 临界区代码
}
1
2
3

如何知道多线程去争抢同一个锁?

凡是多线程要执行的临界区使用的是同一个锁对象则说明多线程去竞争的是同一个锁。

下边的代码中线程t1和t2争抢同一个lock锁。

		//定义一个锁对象
		Object lock = new Object();
		new Thread(()->{
			synchronized (lock){
					sum++;
			}
		},"t1").start();
		new Thread(()->{
			synchronized (lock){
					sum++;
			}
		},"t2").start();
1
2
3
4
5
6
7
8
9
10
11
12

下边的代码中线程t1和t2没有争抢同一个锁。

    //定义一个锁对象
    Object lock = new Object();
    //定义另一个锁对象
    Object lock2 = new Object();
	new Thread(()->{
			synchronized (lock){
					sum++;
			}
		},"t1").start();
		new Thread(()->{
			synchronized (lock2){
					sum++;
			}
		},"t2").start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Monitor监视器 ​

多线程执行临界区代码根据锁对象找到锁,此时于操作系统交互尝试获取锁,下图显示了线程1、线程2、线程3、线程4、线程5竞争了同一个锁。

ScreenShot_2026-09-20_102505_023.png

Monitor为监视器或叫管程,Monitor就是锁,在obj对象头中记录了Monitor的地址。

在Monitor中记录了当前获取锁的线程以及当前等待获取锁的线程,在Owner中记录了当前获取锁的线程且只能记录一个,假设线程1获取到了锁那么在Owner中记录线程1的标识,线程2、线程3、线程4、线程5在等待获取锁,在等待队列里记录了等待获取锁的线程。

所以Monitor监视器控制着多线程不能同时获取同一个锁。

多线程竞争锁案例 ​

知道了锁和对象的关系以及Monitor的作用,下边看再入门程序就知道了其它奥秘。

		//定义一个锁
		Object lock = new Object();
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				//多线程执行sum++需要获取lock锁
				synchronized (lock){
					sum++;
				}
			});
		}
1
2
3
4
5
6
7
8
9
10

上边的代码用下图表示:

ScreenShot_2026-09-20_102539_153.png

流程如下:

1、100个线程执行临界区代码sum++,谁拥有了该lock锁谁执行临界区代码。

2、线程1获取到了锁,其它线程去获取锁被阻塞,被阻塞线程的状态为BLOCKED。

3、获取锁的线程执行临界区的代码,正常访问共享资源。

4、线程执行临界区代码完成自动释放锁。

5、锁被释放后其它线程开始竞争同一个锁,获取到锁的线程执行临界区的代码。

注意:锁被释放后等待锁的线程都会去竞争该锁,具体由操作系统调度由哪个线程获取锁。

分析下边的代码是否存在线程不安全的问题。

		//定义一个锁对象
		Object lock = new Object();
		Object lock2 = new Object();
		for (int i = 0; i < 100; i++) {
			final int y = i;
			threadPool.execute(()->{
				if(y % 2 ==0){
					synchronized (lock){
						sum++;
					}
				}else{
					synchronized (lock2){
						sum++;
					}
				}

			});
		}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通过代码可知,50个线程竞争同一个锁,另外50个线程竞争另一个锁,见下图所示:

wechat_longscreenshot_2026-09-20_102634_923.png

多线程向同一个共享变量写操作,由于多线程不是竞争的同一个锁就会存在并发写操作,产生了竞态条件,存在线程不安全的问题。

上图对应的测试代码如下:

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {
	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);

	//测试synchronized同步锁
	public static void test_synchronized_first2(){
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//定义一个锁对象
		Object lock = new Object();
		Object lock2 = new Object();
		for (int i = 0; i < 100; i++) {
			final int y = i;
			threadPool.execute(()->{
				if(y % 2 ==0){
					synchronized (lock){
						sum++;
					}
				}else{
					synchronized (lock2){
						sum++;
					}
				}

			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
	public static void main(String[] args) {
		test_threadsafe2();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

执行程序预期sum=100,结果与预期不一致,线程不安全。

如何让100个线程竞争同一个锁则100个线程会同步执行临界区的代码,修改如下:

2.1.4 常见的线程安全类 ​

常见的线程安全类 ​

学习了线程安全的概念,在开发中要有线程安全的意识,下边是一些常用类都是线程不安全的:

StringBulider、ArrayList、LinkedList、HashMap、HashSet、TreeMap、TreeSet

下边的类都是线程安全的:

1、Integer、String: 都是不可变类,它的不可变性体现如下:

​ String类的substring方法不改变原有内容而是返回新的字符串,String类用final修饰保证String类的方法无法被子类重写,属性用final修饰表示不可变。

2、StringBuffer:append方法都用synchronized同步处理。

3、Vector、Stack、Hashtable:add、push、put等写方法都采用synchronized同步处理所以是线程安全的。

4、java.util.concurrent:此包简称为JUC,它是jdk1.5推出的并发编程包,包下的很多类都是线程安全的,这是本课程学习的重点。

随着课程的深入还会学习更多线程安全的类供在开发中使用。

StringBulider与StringBuffer测试 ​

分别用20个线程调用StringBuilder和StringBuffer的append方法追加一个字符。

共两个测试方法,每个测试方法执行10次,正确的结果是每个测试共追加20个字符。

代码如下:

package com.yjoffer.javase.thread.safe;

import com.yjoffer.javase.config.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.OptionalLong;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {

	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);
	//测试StringBuilder
	public static void test_stringbuilder(){
		StringBuilder stringBuilder = new StringBuilder();
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				stringBuilder.append(n);
			});
		}
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				stringBuilder.append(n);
			});
		}
		shutdown(executorService);
		System.out.println(stringBuilder.toString().length());
	}
	//测试StringBuffer
	public static void test_stringbuffer(){
		StringBuffer stringBuffer = new StringBuffer();
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				stringBuffer.append(n);
			});
		}
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				stringBuffer.append(n);
			});
		}
		shutdown(executorService);
		System.out.println(stringBuffer.toString().length());
	}


	public static void main(String[] args) throws InterruptedException {
		System.out.println("==========stringbuilder=========");
		for (int i = 0; i < 10; i++) {
			test_stringbuilder();
		}
		System.out.println("==========stringbuffer=========");
		for (int i = 0; i < 10; i++) {
			test_stringbuffer();
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

输出每次测试追加的字符数 :

==========stringbuilder=========
18
20
19
18
18
20
20
20
20
20
==========stringbuffer=========
20
20
20
20
20
20
20
20
20
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

从输出可见测试stringbuilder存在结果 不正确的现象。

ArrayList与Vector测试 ​

分别启动10个线程每个线程向列表插入100个数,正确的结果是共向列表插入1000个数。

分别使用ArrayList、Vector、 Collections.synchronizedList()三种方法测试,每种方法测试10次。

代码如下:

//测试ArrayList
	public static void test_ArrayList(){
		ArrayList<Integer> list = new ArrayList<>();
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			executorService.execute(()->{
				for (int i1 = 0; i1 < 100; i1++) {
					list.add(i1);
				}

			});
		}
		shutdown(executorService);
		System.out.println(list.size());
	}
	//测试vector
	public static void test_vector(){
		Vector<Integer> list = new Vector<>();
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			executorService.execute(()->{
				for (int i1 = 0; i1 < 100; i1++) {
					list.add(i1);
				}

			});
		}
		shutdown(executorService);
		System.out.println(list.size());
	}
	//测试Collections.synchronizedList
	public static void test_synchronizedList(){
		List<Integer> list = Collections.synchronizedList(new ArrayList<>());
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			executorService.execute(()->{
				for (int i1 = 0; i1 < 100; i1++) {
					list.add(i1);
				}

			});
		}
		shutdown(executorService);
		System.out.println(list.size());
	}
	public static void main(String[] args) throws InterruptedException {
		System.out.println("==========ArrayList=========");
		for (int i = 0; i < 10; i++) {
			test_ArrayList();
		}
		System.out.println("==========Vector=========");
		for (int i = 0; i < 10; i++) {
			test_vector();
		}
		System.out.println("==========synchronizedList=========");
		for (int i = 0; i < 10; i++) {
			test_synchronizedList();
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

输出 每次测试向列表插入的数据个数:

==========ArrayList=========
Exception in thread "pool-1-thread-1" java.lang.ArrayIndexOutOfBoundsException: 10
	at java.util.ArrayList.add(ArrayList.java:463)
	at com.yjoffer.javase.thread.safe.ThreadsafeTest.lambda$test_ArrayList$25(ThreadsafeTest.java:1051)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
901
1000
1000
938
957
1000
1000
997
1000
1000
==========Vector=========
1000
1000
1000
1000
1000
1000
1000
1000
1000
1000
==========synchronizedList=========
1000
1000
1000
1000
1000
1000
1000
1000
1000
1000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

从输出可以看出ArrayList类存在结果不正确的现象,Vector和Collections.synchronizedList()都是线程安全的。

在后边讲解JUC包中的队列时还会讲到其它线程安全的集合。

HashMap与Hashtable测试 ​

分别启动10个线程每个线程向map插入100个KV对,正确的结果是共向列表插入1000个KV对。

分别使用HashMap、Hashtable、 Collections.SynchronizedMap三个类进行测试,每种方法测试10次。

代码如下:

//测试HashMap
	public static void test_HashMap(){
		HashMap<String,Integer> map = new HashMap<>();
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				for (int i1 = 0; i1 < 100; i1++) {
					map.put(n+"_"+i1,i1);
				}

			});
		}
		shutdown(executorService);
		System.out.println(map.size());
	}
	//测试Hashtable
	public static void test_Hashtable(){
		Hashtable<String,Integer> map = new Hashtable<>();
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				for (int i1 = 0; i1 < 100; i1++) {
					map.put(n+"_"+i1,i1);
				}

			});
		}
		shutdown(executorService);
		System.out.println(map.size());
	}
	//测试synchronizedMap
	public static void test_synchronizedMap(){
		Map<String,Integer> map = Collections.synchronizedMap(new HashMap<>());
		ExecutorService executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 10; i++) {
			int n = i;
			executorService.execute(()->{
				for (int i1 = 0; i1 < 100; i1++) {
					map.put(n+"_"+i1,i1);
				}

			});
		}
		shutdown(executorService);
		System.out.println(map.size());
	}
	public static void main(String[] args) throws InterruptedException {
		System.out.println("==========HashMap=========");
		for (int i = 0; i < 10; i++) {
			test_HashMap();
		}
		System.out.println("==========Hashtable=========");
		for (int i = 0; i < 10; i++) {
			test_Hashtable();
		}
		System.out.println("==========synchronizedMap=========");
		for (int i = 0; i < 10; i++) {
			test_synchronizedMap();
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

输出 每次测试向map插入的KV对个数:

==========HashMap=========
965
946
1000
995
993
990
984
931
1000
986
==========Hashtable=========
1000
1000
1000
1000
1000
1000
1000
1000
1000
1000
==========synchronizedMap=========
1000
1000
1000
1000
1000
1000
1000
1000
1000
1000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

从输出可以看出HashMap类存在结果不正确的现象,Hashtable和Collections.SynchronizedMap都是线程安全的。

在后边讲解JUC包中的队列时还会讲到其它线程安全的集合。

2.1.5 理解变量对线程安全的影响 ​

共享变量影响线程安全 ​

根据线程安全的定义可知,共享变量是造成线程不安全的前提条件,当存在多线程可以并发向共享变量写数据时会造成线程不安全。

什么是共享变量?

共享变量顾名思义是可以被多线程共享的变量,它在堆中存储,包括:对象的成员变量、静态变量、数组元素等。

下边拿成员变量举例说明。

成员变量的值有引用类型和基本类型,当多线程访问同一个对象的时会共享成员变量。

ScreenShot_2026-09-20_102742_390.png

多线程访问不同的对象但是引用的是同一个对象此时会共享引用对象的成员变量。

ScreenShot_2026-09-20_102819_123.png

基本类型和引用类型的共享变量都可以导致线程不安全,但造成线程不安全更多的说的是基本类型的成员变量,我们把基本类型的成员变量叫作数据域的成员变量。

如果一个类中有基本类型的成员变量时要去看访问它的方法是否线程安全, 如果不是线程安全则该成员变量所在的类就不是线程安全的。

比如下边的类中,safe()是线程安全的,unsafe()方法是线程不安全的,该类则是线程不安全的。

class Analyzer {
		private int sum=0;
		//此方法线程安全
		public synchronized void safe(){
			sum++;
		}
		//此方法线程不安全
		public void unsafe(){
			sum++;
		}
	...
1
2
3
4
5
6
7
8
9
10
11
12

如果一个类的成员变量是引用类型,要注意该类型是否是线程安全的,如果不是线程安全当多线程共享时导致本类就不是线程安全的。

比如下边的类,成员变量list所属类型不是线程安全的则该类不是线程安全的。

class Analyzer {
		private List<String> list = new ArrayList();
		private StringBuffer stringBuffer = new StringBuffer();
		...
1
2
3
4

只要有共享变量就会线程不安全吗?不是,以下情况共享变量不会造成线程不安全:
1、共享变量没有被多线程共享。

比如有两个共享变量:sumA、sumB,线程A访问sumA,线程B访问sumB,共享变量并没有被多线程共享,不会造成线程不安全。

2、多线程没有并发写共享变量,仅仅是读则不会造成线程不安全。

分析下边的代码中哪些类是线程安全的,哪些是线程不安全的。

示例1:

package com.yjoffer.javase.thread.basic;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class PbController {

    private long ids;

    private List<String> list = new ArrayList();

    private final List<String> list2 = new ArrayList<>();

    private StringBuilder stringBuilder = new StringBuilder();

    //....
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

成员变量为共享变量,且list、list2、stringBuilder都为线程不安全类型,所以PbController类线程不安全。

示例2:

package com.yjoffer.javase.thread.basic;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class PbAction {
    private PbService pbService = new PbService();

    public List queryuser(){
        return pbService.queryuser();
    }
}

class PbService{

    public List queryuser(){
        return new ArrayList();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

PbAction类有引用 类型的成员 变量PbService,但是PbService中并没有数据域成员变量(共享变量),new ArrayList()在方法中属于局部变量,所以PbAction 和PbService是线程安全的。

局部变量影响线程安全 ​

方法中定义的局部变量、方法参数、方法返回值都是局部变量,每个线程都有自己的栈内存,线程执行一个方法在栈内存中开辟一个栈帧,栈帧中存放方法中定义的局部变量(在讲解JMM内存模型时还会详细讲)。

如下图所示,不同线程的栈帧互不影响。

ScreenShot_2026-09-20_102905_463.png

基本类型的局部变量不会造成线程不安全,它们是线程安全的。

当局部变量为引用类型并且它暴露给外部时可能会线程不安全,下边说明这一点。

局部变量为引用类型则它引用了堆中的对象,暴露给外部可能会被多线程访问,这时该局部变量就成了共享变量。

下图显示了栈帧中的局部变量暴露给外部,不同的多个线程栈帧中的局部变量引用 了堆中的同一个对象。

ScreenShot_2026-09-20_102941_911.png

1、 首先举例定义一下引用 类型的局部变量不暴露给外部的情况。

下边代码在safe方法中定义HashMap局部变量,虽然HashMap本身是线程不安全的,但是该局部变量仅在safe方法中使用并且没有被 多线程共享,所以它是线程安全的。

	static class InnerClass1{
		//定义引用类型局部变量不暴露给外部
		public void safe(){
			HashMap<String, Integer> map = new HashMap<>();
			for (int i = 0; i < 1000; i++) {
				map.put(String.valueOf(i),i);
			}
			logger.debug("map.size="+map.size());
		}
	}
	public static void test_InnerClass1(){
        ExecutorService executorService = Executors.newCachedThreadPool();
        InnerClass1 innerClass1 = new InnerClass1();
        for (int i = 0; i < 10; i++) {
            executorService.execute(()->{
                innerClass1.safe();
            });
        }
    }
	public static void main(String[] args) throws InterruptedException {

		test_InnerClass1();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行,输出 map的size为1000。

2、下边再举一个将局部变量暴露给外部的例子。

定义unsafe()方法,此方法将map对象暴露给putmap方法,此时就可能存在线程不安全的问题。

	static class InnerClass1{
		//定义引用类型的局部变量暴露给外部
		public void unsafe(){
			HashMap<String, Integer> map = new HashMap<>();
			putmap(map);
		}
		//修改map,线程安全
		public void putmap(Map<String, Integer> map){
			for (int i = 0; i < 1000; i++) {
				map.put(String.valueOf(i),i);
			}
			logger.debug("map.size="+map.size());
		}
	}
	public static void test_InnerClass1(){
        ExecutorService executorService = Executors.newCachedThreadPool();
		InnerClass1 innerClass1 = new InnerClass1();
		for (int i = 0; i < 10; i++) {
			executorService.execute(()->{
				innerClass1.unsafe();
			});
		}
    }
	public static void main(String[] args) throws InterruptedException {
		test_InnerClass1();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

运行上边的程序并没有出现线程不安全的问题,因为unsafe方法调用putmap方法,在putmap方法中并没有使用多线程访问map对象,所以是线程安全的。

由于putmap是public,当在子类中对此方法进行重写则可能线程不安全,而这是InnerClass1类无法控制的。

//在子类中重写putmap方法,线程不安全
	static class SubInnerClass1 extends InnerClass1{
		//修改map
		public void putmap(Map<String, Integer> map){
			ExecutorService executorService = Executors.newCachedThreadPool();
			for (int i = 0; i < 10; i++) {
				int n = i;
				executorService.execute(()->{
					for (int i1 = 0; i1 < 100; i1++) {
						map.put(n+"_"+i1,i1);
					}

				});
			}
			shutdown(executorService);
			logger.debug("map.size="+map.size());
		}
	}
	public static void main(String[] args) throws InterruptedException {

		ExecutorService executorService = Executors.newCachedThreadPool();
		//这里定义子类对象
		InnerClass1 innerClass1 = new SubInnerClass1();
		for (int i = 0; i < 10; i++) {
			executorService.execute(()->{
				innerClass1.unsafe();
			});
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

输出发现存在map的size不为1000的情况。

所以,对于可能暴露局部变量给外部的方法可以使用private或final控制不允许子类重写方法,或者整个类加final修饰不允许子类更改,从而控制整个类的线程安全。

2.1.6 synchronized同步块的几种形式 ​

同步块的几种形式 ​

什么是同步代码块?

临界区添加synchronized成为同步代码块,多线程执行同步代码块的代码只能同步执行。

下边代码中大括号的内容就是同步代码块。

synchronized(锁){
    // 临界区代码,同步代码块
    //...
}
1
2
3
4

synchronized同步代码块可以是一个实例方法、静态方法、任意代码块,下图是同步代码块的各种形式:

image-20211117091049732

重点关注同步代码块的“锁对象”,要求:对不同的同步代码块判断出多线程是否竞争同一个锁,只有竞争同一个锁多线程才会同步执行,最终保证线程安全。

1、实例方法

对于添加了synchronized的实例方法来说,多线程执行同一个对象的实例方法才会共用一个锁对象,锁对象为当前实例this,这些方法组合为一个大的同步区域。

如下伪代码中,方法1和方法2的锁对象为当前实例this,多线程执行同一个类1对象的方法1、方法2时共用同一个锁对象,多线程同步执行。

public class 类1{
	public synchronized 方法1{
		//同步区域

	}
	public synchronized 方法2{
		//同步区域

	}
	public 方法3{
		//非同步区域

	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

静态方法:对于添加了synchronized的静态方法来说,多线程执行同一个类的静态方法共用一个锁,锁对象为当前类的类对象(类名.class),临界区的范围是整个静态方法。

下边伪代码中方法1和方法2的锁对象为类2的类对象(类2.class),多线程执行类2的静态方法1、方法2时共用同一个锁对象,多线程同步执行。

public class 类2{
	public static synchronized 方法1{
		//同步区域1

	}
	public static synchronized 方法2{
		//同步区域1

	}
	public synchronized 方法3{
		//同步区域2

	}
	public static 方法4{
		//非同步区域
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

任意代码块:对于由synchronized包裹的代码块来说,使用同一个锁对象的代码块多线程同步执行。

如下边伪代码:

public class 类3{
    public static synchronized 方法1{
	    //同步区域1

	}
	//方法2与方法1同步作用一样
	public static 方法2{
	    synchronized(类3.class){
	    	//同步区域1

	    }
	}
	public synchronized 方法3{
		//同步区域2

	}
	//方法4与方法3同步作用一样
	public 方法4{
		synchronized(this){
	    	//同步区域2

	    }
	}
	//自定义的锁对象
	Object obj = new Object();
	public 方法5{
	    //任意代码块,使用自定义锁对象
		synchronized(obj){
	    	//同步区域3

	    }
	    //其它代码
	    synchronized(obj){
	    	//同步区域3

	    }
	    //其它代码
	    synchronized(类3.class){
	    	//同步区域1

	    }
	    //其它代码
	    synchronized(this){
	    	//同步区域2

	    }

	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
实例方法同步块 ​

实例方法同步块是在实例方法上添加synchronized,此时锁对象为当前实例。

语法如下:

方法权限修饰符  synchronized  返回值 方法名(){
	//临界区...
}
1
2
3

下边使用100个线程调用同一个分析器对象的increase()方法实现sum++操作。

1、定义一个分析器类。

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {
	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);
	//共享变量
	static int sum = 0;
	//分析器
	static class Analyzer {
		//递增
		public synchronized void increase(){
			sum++;
		}
		//递减
		public  void decrease(){
			sum--;
		}

	}
	...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

2、测试实例方法同步块

//测试实例方法同步块
	public static void test_syncblock1(){
		//分析器对象实例
		Analyzer analyzer = new Analyzer();
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				analyzer.increase();
			});
		}
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

执行程序,预期sum=100,结果与预期一致,线程不安全。

现在启动100个线程执行increase()递增,再启动100个线程执行decrease()递减,预期结果sum=0,运行下边的程序:

//测试实例方法同步块
	public static void test_syncblock1(){
		//分析器对象实例
		Analyzer analyzer = new Analyzer();
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				analyzer.increase();
			});
		}
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				analyzer.decrease();
			});
		}
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
	public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            sum=0;
            test_syncblock1();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

结果sum不等于0,线程不安全。

因为是increase()方法是同步执行, decrease()并不是同步代码块导致 sum--与sum++并发执行。

将decrease()方法添加synchronized,如下:

//递减
public synchronized void decrease(){
	sum--;
}
1
2
3
4

再次运行test_syncblock1()方法结果与预期一致sum=0。

如果将测试改为如下方式又会出现线程不安全的问题:

public static void test_syncblock1(){
		//分析器对象实例
		Analyzer analyzer = new Analyzer();
		Analyzer analyzer2 = new Analyzer();
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 1000; i++) {
			threadPool.execute(()->{
				analyzer.increase();
			});
		}
		for (int i = 0; i < 1000; i++) {
			threadPool.execute(()->{
				analyzer2.decrease();
			});
		}
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

原因是前1000个线程执行递增和后1000个线程执行递减使用的锁对象不同,它们 操作的是同一个共享变量,由于锁对象不同存在并发执行,所以线程不安全。

总结:多线程通过多个方法访问同一个共享变量一定要保证锁对象的统一。

静态方法同步块 ​

静态方法同步块即在静态方法上添加synchronized,此时锁对象为类对象。

语法如下:

方法权限修饰符  synchronized static 返回值 方法名(){
	//临界区...
}
1
2
3

测试代码如下:

staticincrease()方法为静态方法,staticdecrease()方法为实例方法。

	//分析器
	static class Analyzer {

		//静态方法上添加synchronized,锁对象为类对象Analyzer.class
		public  synchronized static void staticincrease(){
			sum++;
		}
		public  synchronized void staticdecrease(){
			sum--;
		}
		...
	}
//测试静态方法同步块
	public static void test_syncblock2(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		Analyzer analyzer = new Analyzer();
		for (int i = 0; i < 1000; i++) {
			threadPool.execute(()->{
				analyzer.staticincrease();
			});
		}
		for (int i = 0; i < 1000; i++) {
			threadPool.execute(()->{
				analyzer.staticdecrease();
			});
		}
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

执行程序,预期sum=0,结果存在sum不等于0的情况,结果与预期不一致,线程不安全。

解决方法:

需要保证staticincrease()和staticdecrease()两个方法的锁对象相同,要么把staticdecrease()方法改为实例方法,要么把staticdecrease()方法改为静态方法。修改后再次运行test_syncblock2()发现存在sum不等于0的情况,线程安全。

代码块同步块 ​

synchronized可以直接标记在一个代码块上实现同步代码块,多线程执行临界区代码是同步执行,语法如下:

synchronized(锁对象){
    // 临界区代码
}
1
2
3

1、锁对象为当前实例,使用this。

//递增
public synchronized void increase(){
   sum++;
}
//上边的方法等价于下边
public void increase(){
   synchronized (this){
      sum++;
   }
}
1
2
3
4
5
6
7
8
9
10

2、锁对象为类对象

public  synchronized static void staticincrease(){
		sum++;
}
1
2
3

上边的方法等价于下边:

public  static void staticincrease(){
	//锁对象为类对象
    synchronized (Analyzer.class){
        sum++;
    }
}
1
2
3
4
5
6
7

3、锁对象为某个对象

//测试代码同步块
	public static void test_syncblock3(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//锁对象
		Object obj = new Object();
		for (int i = 0; i < 100; i++) {
			threadPool.execute(()->{
				//锁对象为实例对象
				synchronized (obj){
					sum++;
				}
			});
		}
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行程序sum=100,预期和结果一致,线程安全。

如果将代码改成如下方式,50个线程执行sum++,50线程执行sum--,预期sum=0

public static void test_syncblock3(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//锁对象
		Object obj = new Object();
		Object obj2 = new Object();
		for (int i = 0; i < 100; i++) {
			int y=i;
			threadPool.execute(()->{
				if(y % 2==0){
					//锁对象为实例对象
					synchronized (obj){
						sum++;
					}
				}else{
					synchronized (obj2){
						sum--;
					}
				}
			});
		}
		shutdown(threadPool);
		logger.debug("sum="+sum);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

执行程序预期和结果不一致,线程不安全。

原因:50线程用的是obj锁对象,另外50个线程用的是obj2锁对象,由于100个线程操作同一个共享变量但是锁对象不同导致并发写的问题,线程不安全。

解决:更改为同一个锁对象即可。

如下:

threadPool.execute(()->{
				if(y % 2==0){
					//锁对象为实例对象
					synchronized (obj){
						sum++;
					}
				}else{
					synchronized (obj){
						sum--;
					}
				}
			});
1
2
3
4
5
6
7
8
9
10
11
12
尽量缩小临界区范围 ​

用synchronized标注的临界区只能同步执行,synchronized不利于提高系统性能,建议如下。

1)能不加synchronized就不加

如果一块代码不会被多线程执行说明它是一个单线程程序,此时就不要加synchronized关键字。

2)尽量缩小临界区范围

如果临界区的范围越大则同步的范围越大,那么多线程执行性能越低,所以临界区的选择一定要谨慎,尽量缩小临界区的范围。

3)能不公用锁对象就不要公用

当共享资源有多个时,如果共享资源可以不共用锁对象就不要使用同一个。

举例如下:

在分析器上添加put方法,该方法除了计算累加值以外还求偶数的个数,如下:

package com.yjoffer.javase.thread.safe;

import com.yjoffer.javase.config.Logger;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {

	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);
	static long evenCount = 0;
	static int sum = 0;
	//分析器
	static class Analyzer {

		//递增
		public synchronized void increase(){
			sum++;
		}

		public void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				evenCount++;
			}
			increase();
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

根据经验可知分析器中的put方法是线程不安全的,因为evenCount属于共享资源,且evenCount++语句没有同步执行。

测试代码如下,启动1000个线程执行put方法,预期sum=1000,evenCount=500,代码如下:

//测试同步块优化
	public static void test_blockoptimize1(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//分析器
		Analyzer analyzer = new Analyzer();
		//提交1000个任务
		for (int i = 1; i <=1000; i++) {
			final int y = i;
			//提交任务
			threadPool.execute(()->{
				analyzer.put(y);
			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		//取出sum
		System.out.println("sum="+sum);
		//偶数数量
		System.out.println("evenCount="+evenCount);
	}
	//测试,运行10次
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			sum = 0;
			evenCount = 0;
			test_blockoptimize1();
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

经过多次测试发现evenCount的结果 和预期不一致。

如何修改bug?

根据经验可以在put方法上用synchronized标记,如下代码:

		public synchronized void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				evenCount++;
			}
			increase();
		}
1
2
3
4
5
6
7

经过多次测试evenCount值没有出现不正确的情况。

但是仔细分析put方法的代码,存在线程不安全的是下边的语句:

if(value % 2==0) {
    //统计偶数数量
    evenCount++;
}
increase();
1
2
3
4
5

increase();方法上已经标记了synchronized,所以increase();方法是线程安全的。

只要保证evenCount++;;同步执行即可没有必要让整个put方法同步,因为临界区的范围越大则程序的性能越低。

所以,对于一个方法中可能存在线程不安全的危险一定要仔细分析,对于代码块也可以用synchronized标记,代码如下:

		public  void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				synchronized(this){
					evenCount++;
				}

			}
			increase();
		}
1
2
3
4
5
6
7
8
9
10

修改后尝试分析多线程执行put方法,value为偶数进入if,获取锁或者等待锁,如果value为奇数直接执行increase方法,所以当value为奇数时不用等待偶数统计完成,提高了执行效率。

多次测试程序,evenCount和sum没有出现不正确的情况。

公用锁对象好吗? ​

多线程根据是否是同一个锁对象来判断是否对临界区同步执行,当共享资源有多个时,如果共享资源可以不共用锁则锁对象就不要使用同一个。

例如put方法中evenCount++和sum++都是使用analyzer实例,代码如下:

	//分析器
	static class Analyzer {
		//递增
		public synchronized void increase(){
			sum++;
		}
		...

		public  void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				synchronized(this){
					evenCount++;
				}

			}
			increase();
		}
		...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

evenCount++统计偶数数量,sum++统计所有数据的数量。

修改Analyzer类的代码如下,在evenCount++临界区中添加休眠3秒。

//分析器
	static class Analyzer {
		//递增
		public synchronized void increase(){
			sum++;
			logger.debug("sum++");
		}

		public void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				synchronized (this){
					evenCount++;
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					logger.debug("休眠3秒");
				}
			}
			increase();
		}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

下边进行测试:

//测试同步块优化
	public static void test_blockoptimize1(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//分析器
		Analyzer analyzer = new Analyzer();
		//提交5个任务
		for (int i = 1; i <=1000; i++) {
			final int y = i;
			//提交任务
			threadPool.execute(()->{
				analyzer.put(y);
			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		//取出sum
		System.out.println("sum="+sum);
		//偶数数量
		System.out.println("evenCount="+evenCount);
	}
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			sum = 0;
			evenCount = 0;
			test_blockoptimize1();
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

通过测试发现increase()临界区代码的执行受evenCount++临界区休眠3秒的影响。

increase()方法和evenCount++分别访问了不同的共享资源,如果使用不同的锁对象,两个临界区代码不再互相影响。

在Analyzer类定义evenCount的锁对象如下:

//evenLock对evenCount的锁对象
private Object evenLock = new Object();
1
2

修改put方法,修改evenCount++临界区锁对象为evenLock。

public void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				synchronized (evenLock){
					evenCount++;
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					logger.debug("休眠3秒");
				}
			}
			increase();
		}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

再次运行程序,increase方法临界区不再受evenCount++;临界区休眠3秒的影响。

2.1.7 synchronized可重入性 ​

同步锁的可重入性 ​

学习了synchronized我们知道,一个线程获取了锁就可以执行临界区的代码,当这个锁没有释放时再次遇到同一个锁的临界区时不用再次获取锁即可进入该临界区,这个特点叫锁的可重入性。如下图代码:

synchronized(obj){
  //代码...
  synchronized(obj){//相同的锁具有可重入性
  //代码...
  }
  //代码...
}
1
2
3
4
5
6
7

在实际应用中,锁的可重入性是最外层代码获取锁后内层代码可再次获取该对象的锁,synchronized也叫可重入锁,可重入锁可以防止自己等自己的现象。

下边测试锁的可重入性。

本代码使用“代码同步块”章节的代码,实现了5个线程执行采集程序,并调用同一个分析程序进行计算。

1、分析程序:

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {

	//分析器
	static class Analyzer {
		//evenLock对evenCount的锁对象
		private Object evenLock = new Object();
		//递增
		public synchronized void increase(){
			sum++;
		}
		public synchronized void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				synchronized (this){
					evenCount++;
				}
			}
			increase();
		}
	}
	...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

测试程序如下:

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {
	//测试同步块优化
	public static void test_blockoptimize1(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//分析器
		Analyzer analyzer = new Analyzer();
		//提交5个任务
		for (int i = 1; i <=1000; i++) {
			final int y = i;
			//提交任务
			threadPool.execute(()->{
				analyzer.put(y);
			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		//取出sum
		System.out.println("sum="+sum);
		//偶数数量
		System.out.println("evenCount="+evenCount);
	}
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			sum = 0;
			evenCount = 0;
			test_blockoptimize1();
		}
	}


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

测试多次所得结果正确,即sum为1000、evenCount为500。

由于synchronized具有可重入性,进入put方法获取锁后进入synchronized(this)则不需要再次获取锁,进入increase()方法也不需要再次获取锁。

每个synchronized对象锁会维护一个加锁次数,获取锁时加1,释放锁时减1,直到为0表示该锁空闲。结合上边的代码分析如下:

1、进入put方法获取实例对象锁,加1

2、进入同步代码块synchronized(this)获取实例对象锁,加1

3、离开同步代码块synchronized(this)释放实例对象锁,减1

4、进入increase方法获取实例对象锁,加1

5、离开increase方法释放实例对象锁,减1

6、离开put方法释放实例对象锁,减1

7、锁空闲

sleep释放锁吗? ​

如果在临界区添加sleep,它除了使线程休眠以外会释放锁吗?

比如下边的代码:

public synchronized void put(long value){
			if(value % 2==0) {
				//统计偶数数量
				synchronized(this){
					even_count++;
					try {
						TimeUnit.MILLISECONDS.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}

			}
			increase();
		}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

按照synchronized锁维护的过程,结束临界区代码后会释放锁,同时加锁次数减1,如果执行sleep后释放锁则不符合此规则。

sleep的作用是使线程休眠,暂时放弃CPU使用权并不会释放锁。

子类调父类是否具有可重入性? ​

在生产中很多时候会继承一个现有的类,当子类中的一个同步方法去调用父类中的同步方法是否具有可重入性,测试下边的代码:

1、定义分析器,它是现有分析器的子类:

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {
	//分析器
	static class AnalyzerTemp extends Analyzer {

		public synchronized void put(long value) {
			super.put(value);

		}
	}
	...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

测试程序如下:

//测试同步块优化
	public static void test_blockoptimize2(){
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//分析器
		AnalyzerTemp analyzer = new AnalyzerTemp();
		//提交5个任务
		for (int i = 1; i <=1000; i++) {
			final int y = i;
			//提交任务
			threadPool.execute(()->{
				analyzer.put(y);
			});
		}
		//如果所有任务没有完成继续等待
		shutdown(threadPool);
		//取出sum
		System.out.println("sum="+sum);
		//偶数数量
		System.out.println("evenCount="+evenCount);
	}
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			sum = 0;
			evenCount = 0;
			test_blockoptimize2();
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

多次测试,结果正确,输出如下:

sum=1000
evenCount=500
1
2

这说明 子类调父类的同步方法具有锁重入性。

2.1.8 synchronized案例 ​

账户转账需求 ​

在金融项目中账户之间进行转账是核心需求,比如:张三向李四转账100元,张三原余额1000元,张三的账户减去100元是900元,李四的账户原余额是1000元,加上100元是1100元,两个账户转账前的总和与转账后的总和是相同的都是2000元。

下图是张三向李四转账的示意图:

设计账户类和转账接口,如下:

再设计一个任务类用于操作转账接口,转账任务由多线程去执行,所以任务类实现了转账接口和Runnable接口,如下:

定义转账接口:

package com.yjoffer.javase.thread.bank;

/**
 * 账户类
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class Account {
  //...稍后实现
}


/**
 * 转账接口
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public interface TransferInterface {

    /**
     * 转账方法
     * @param from 源账户
     * @param target 目标账户
     * @param amount 转账金额
     */
    void transfer(Account from,Account target,float amount);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
代码实现 ​

首先定义Account类,由于存在多线程访问同一个Account实例的余额,这样就产生了竞态条件,所以需要将subtractAmount()扣减方法和addAmount()增加金额方法加synchronized修饰。

package com.yjoffer.javase.thread.bank;

/**
 * 账户类
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class Account {

    /**
     * 账户id
     */
    String id;
    /**
     * 账户名称
     */
    String name;
    /**
     * 账户余额
     */
    float balance;

    public  Account(String id,String name,float balance){
        this.id = id;
        this.name = name;
        this.balance = balance;
    }

    //增加余额
    public synchronized void addAmount(float amount){
        balance +=amount;
    }

    //减去余额
    public synchronized void subtractAmount(float amount){
        balance -=amount;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public float getBalance() {
        return balance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

定义任务类,虽然多线程执行转账任务,但是TransferTask实例不是共享资源,因为每个线程运行自己的TransferTask实例,所以TransferTask里边的实例方法不用加synchronized修饰。

package com.yjoffer.javase.thread.bank;

/**
 * 转账任务类
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class TransferTask implements Runnable,TransferInterface {

    /**
     * 源账户
     */
    private Account from;

    /**
     * 目标账户
     */
    private Account target;

    /**
     * 转账金额
     */
    private float amount;

    public TransferTask(Account from,Account target,float amount){
        this.from = from;
        this.target = target;
        this.amount = amount;
    }


    @Override
    public void run() {
        //转账
        transfer(from,target,amount);
    }

    @Override
    public void transfer(Account from, Account target, float amount) {
        //源账户扣钱
        from.subtractAmount(amount);
        //目标账户加钱
        target.addAmount(amount);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
测试 ​

测试类:

package com.yjoffer.javase.thread.bank;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 转账测试
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class TranserTest {

    //转账100次,每次10元
    public static void test1(){
        //源账户
        Account from = new Account("01", "张三", 1000);
        //目标账户
        Account target = new Account("02", "李四", 1000);
        //线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //启动100个线程转账,每个线程转一次
        for (int i = 0; i < 100; i++) {
            //每次转10元
            threadPool.execute(new TransferTask(from,target,10));
        }

        shutdown(threadPool);
        System.out.println("from:"+from.getBalance());
        System.out.println("target:"+target.getBalance());
    }
    public static void main(String[] args) {
        test1();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

输出:

from:0.0
target:2000.0
1
2

原始账户的金额分别是1000元,共转账100次,每次10元,通过输出可知:转账前和转账后的总和是相等的都是2000元。

转账异常处理 ​

下边测试转账101次,更改代码如下:

   //转账101次,每次10元
    public static void test2(){
        //源账户
        Account from = new Account("01", "张三", 1000);
        //目标账户
        Account target = new Account("02", "李四", 1000);
        //线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //启动101个线程转账,每个线程转一次
        for (int i = 0; i < 101; i++) {
            //每次转10元
            threadPool.execute(new TransferTask(from,target,10));
        }

        shutdown(threadPool);
        System.out.println("from:"+from.getBalance());
        System.out.println("target:"+target.getBalance());
    }
    public static void main(String[] args) {
        test2();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行,输出如下:

from:-10.0
target:2010.0
1
2

当转账101次时虽然转账前和转账后的总和不变,但是余额出现负数,这是不正确的。

下边对余额扣减的方法进行修改,如下:

当扣减金额大于余额时则抛出异常。

    //减去余额
    public synchronized void subtractAmount(float amount){
        if(balance<amount){
            throw new RuntimeException("余额不足");
        }
        balance -=amount;
    }
1
2
3
4
5
6
7

一旦异常抛出则整个转账方法将终止。

再次运行程序,输出如下:

from:0.0
Exception in thread "pool-1-thread-28" java.lang.RuntimeException: 余额不足
target:2000.0
	at com.yjoffer.javase.thread.bank.Account.subtractAmount(Account.java:37)
	at com.yjoffer.javase.thread.bank.TransferTask.transfer(TransferTask.java:41)
	at com.yjoffer.javase.thread.bank.TransferTask.run(TransferTask.java:35)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
1
2
3
4
5
6
7
8
9

思考:

账户扣减时添加了异常,如果在转账方法中先向账户添加金额再扣减金额可行吗?代码如下:

public void transfer(Account from, Account target, float amount) {
        //目标账户加钱
        target.addAmount(amount);
        //源账户扣钱
        from.subtractAmount(amount);
    }
1
2
3
4
5
6

可在“预见猿份”公众号中交流。

2.1.9 synchronized优化机制 ​

synchronized优化介绍 ​

JDK6之前Synchronized性能低下,每次访问临界区代码都需要获取Monitor锁,Monitor的底层是操作系统的Mutex Lock(互斥锁),我们写的程序是用户程序,操作系统是内核程序,每次获取锁、释放锁都需要与操作系统交互,进入内核需要保存用户程序的上下文,内核执行结束再到用户程序需要恢复用户程序的上下文,这个过程即用户态到内核态的切换,切换频繁从而影响性能。

JDK6对Synchronized进行优化,引入了偏向锁、轻量级锁,最初会使用偏向锁,当有偏向线程以外的其它线程执行临界区时则用轻量级锁,当升级为轻量级锁失败时(即CAS更新失败)表示存在多线程竞争,此时使用Monitor重量级锁。

对象头信息 ​

如何知道当前的synchronized使用了哪种锁呢?synchronized在使用时需要指定锁对象,当初学习Monitor重量级锁时知道一个Monitor绑定一个对象,即锁对象。在对象的头部会记录当前的加锁类型,是偏向锁还是轻量级锁或是重量级锁。

一个对象包括对象头和对象体,以64位虚拟机为例,如下图:

Klass word存储了对象所属Class的元信息地址,Class信息在JVM的方法区存储。

如果对象是数组还需要存储数组的长度。

下图显示了Mark Word的信息结构:

最后两位标记了锁的状态,01表示无锁,00表示轻量级锁,10表示重量级锁,11表示GC标记。

倒数第3位标记的”是否偏向锁“,1表示开启偏向锁,0表示未开启偏向锁。

分代年龄表示对象的年龄,用于垃圾回收。

unused:未使用

hashcode: 对象的hashcode

thread:存储偏向锁偏向线程的标记

epoch: 验证偏向锁的时间戳

ptr_to_lock_record: 轻量锁的锁记录指针

ptr_to_heavyweight_monitor:重量锁monitor指针

偏向锁 ​

对象默认开启偏向锁,当线程进入临界区后在对象头中更新当前线程的标识,此时对象偏向于该线程,该线程再次进入临界区发现对象头是自己的标识则直接进入临界区执行代码。偏向锁好比一个人的专属座位,座位上写上人名,人就是线程,座位就是对象,座位上的人名就是对象头中的线程标识。

一个对象默认开启了偏向锁,由于存在延迟,几秒后偏向标记位为1,下边的代码打印了对象头信息。

首先在工程中创建lib目录,将拷贝jol-core-0.12.jar到lib目录下:

image-20211217084334485

右键lib目录,点击”Add as Library...“

image-20211217084626729

代码如下:

package com.yjoffer.javase.thread.basic;

import com.yjoffer.javase.config.Logger;
import org.openjdk.jol.info.ClassLayout;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 测试锁优化
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class LockOptimizerTest {

    private static Logger logger = Logger.getLogger(LockOptimizerTest.class);

    //输出对象头信息
    public static void test_optimizer1(){
        logger.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        try {
            Thread.sleep(5000);//5秒后再看对象的头信息,偏向标记位为1
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.debug(ClassLayout.parseInstance(new Object()).toPrintable());

    }
    public static void main(String[] args) {
        test_optimizer1();
     }
     ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

输出如下 :

image-20211217102607509

8个字节是Mark Word信息,每8位为一组,排在第一的是低8位。

Klass信息被压缩为32位。

由于偏向锁存在延时最初打印的头信息后3位为001,偏向标记位为0表示不偏向,5秒后再次打印后3位为101,偏向标记位为1表示可偏向。

为了测试程序方便取消偏向锁延迟,设置VM参数:-XX:BiasedLockingStartupDelay=0

image-20211217104300102

下边测试线程进入synchronized临界区后在对象头中更新偏向线程的标识:

public static void test_optimizer2(){
        logger.debug(ClassLayout.parseInstance(new Object()).toPrintable());

        //进入临界区对象头记录线程的标识
        Object lock = new Object();
        synchronized (lock){
            logger.debug("线程标识更新至对象头");
            logger.debug(ClassLayout.parseInstance(lock).toPrintable());
        }
        logger.debug("解锁后偏向的线程标识仍然存在");
        logger.debug(ClassLayout.parseInstance(lock).toPrintable());
    }
1
2
3
4
5
6
7
8
9
10
11
12

输出 如下:

image-20211217105107117

红色部分是更新至对象头中的线程标识,当前线程为main主线程,此标识为主线程的标识。

解锁后偏向线程的标识依然在对象中存在,这说明对象偏向了main主线程

image-20211217105159996

轻量级锁 ​

一个对象偏向一个线程,当该线程执行完成代码后另一个线程去获取锁就会撤销偏向锁升级为轻量级锁,升级为轻量级锁变为不可偏向。注意:此时虽然多个线程用了同一个对象但并没有竞争同一个锁。

代码如下:

//撤销偏向锁
    public static void test_optimizer3() {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                logger.debug("t1线程标识更新至对象头");
                logger.debug(ClassLayout.parseInstance(lock).toPrintable());
            }
        }, "t1");
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.debug("t1完成后运行主线程");
        synchronized (lock) {
            logger.debug("撤销偏向,升级为轻量级锁");
            logger.debug(ClassLayout.parseInstance(lock).toPrintable());
        }

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

输出 :

image-20211217154322754

从输出可以看出升级为轻量级锁后Mark Word的最后两位为00,表示轻量级锁,其余存放锁记录的地址。

轻量级锁使用CAS技术将对象头的Mark Word信息与线程栈中的锁记录进行交换,交换成功表示加锁成功。CAS可以保证操作的原子性,是一种乐观锁技术,稍后会详细学习。

轻量级锁相比Monitor锁效率高,使用自旋技术不断重试CAS操作,直到更新成功为止,所以轻量级锁也叫自旋锁。

注意:自旋锁会占用CPU进行CAS重试,所以它不适合在单核CPU环境运行。

批量重偏向 ​

对象被撤销偏向后升级为轻量级锁,一般情况是无法再偏向的,当存在同一个类的多个对象被撤销偏向的次数达到20次时会执行批量重偏向,此时将偏向到一个新的线程。

注意:重偏向一次后不允许再重偏向了。

下边是测试代码:

1、t1线程获取30个对象的偏向锁。

2、主线程对偏向t1线程的30个对象撤销偏向。

从第20个对象开始批量重偏向到主线程。

//批量重偏向
    public static void test_optimizer4() {
        ArrayList<Book> objects = new ArrayList<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                Book lock = new Book();
                objects.add(lock);
                synchronized (lock) {
                    logger.debug(i+"t1线程标识更新至对象头");
                    logger.debug(ClassLayout.parseInstance(lock).toPrintable());
                }
            }
        }, "t1");

        t1.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.debug("t1完成后运行主线程");
        for (int i = 0; i < 19; i++) {
            Book lock = objects.get(i);
            synchronized (lock) {
                logger.debug(i+"撤销偏向,升级为轻量级锁");
                logger.debug(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
        //从本类第20个对象开始批量重偏向到main线程
        for (int i = 19; i < 30; i++) {
            Book lock = objects.get(i);
            synchronized (lock) {
                logger.debug(i+"从本类第20个对象开始批量重偏向到main线程");
                logger.debug(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

输出 :

前19个对象撤销偏向后升级为轻量级锁,从第20个对象开始执行批量重偏向,第20个往后的对象重偏向到了新的线程。

image-20211217162146342

批量撤销偏向 ​

当执行了批量重偏向后,又有新的线程去撤销偏向,当撤销偏向达到40次时JVM就认为该类的偏向线程是不稳定的,此时会执行批量撤销偏向,该类的所有对象都不支持偏向。批量撤销与批量重偏向是对类的一种优化,和具体的对象无关。

测试代码如下:

1、t1线程获取50个对象的偏向锁。

2、主线程对偏向t1线程的50个对象撤销偏向。

3、t2线程对这50个对象再次撤销,当撤销偏向达到40次时此时会执行批量撤销,再创建该类的新对象都不支持偏向。

//批量撤销
    public static void test_biased_lock5() {
        ArrayList<Book> objects = new ArrayList<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                Book lock = new Book();
                objects.add(lock);
                synchronized (lock) {
                    logger.debug(i+"t1线程标识更新至对象头");
                    logger.debug(ClassLayout.parseInstance(lock).toPrintable());
                }
            }
        }, "t1");

        t1.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.debug("t1完成后运行主线程");
        for (int i = 0; i < 19; i++) {
            Book lock = objects.get(i);
            synchronized (lock) {
                logger.debug(i+"撤销偏向,升级为轻量级锁");
                logger.debug(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
        //从第20个开始批量重偏向到main线程
        for (int i = 19; i < 50; i++) {
            Book lock = objects.get(i);
            synchronized (lock) {
                logger.debug(i+"批量重偏向");
                logger.debug(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 39; i++) {
                Book lock = objects.get(i);
                synchronized (lock) {
                    logger.debug(i+"t2撤销偏向,升级为轻量级锁");
                    logger.debug(ClassLayout.parseInstance(lock).toPrintable());
                }
            }
            //从第40个开始新创建的对象为不可偏向状态
            Book obj1 = new Book();
            synchronized (obj1) {
                logger.debug("从第40个开始新创建的对象为不可偏向状态");
                logger.debug(ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "t2");

        t2.start();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

输出 :

image-20211217165024980

← 03 线程池05 ReentrantLock教程 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系我们
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节