一、共享带来的问题
1. 小故事
- 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
- 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
- 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
- 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
- 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
- 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘
- 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
- 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上
- 计算流程是这样的
- 但是由于分时系统,有一天还是发生了事故
- 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
- 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地到一边待着去了(上下文切换)
- 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
- 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本
- 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0
java实现
- 两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 10000 次,结果是 0 吗?
@Slf4j(topic = "c.Test17_1")
public class Test17_1 {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}
18:01:04.759 c.Test17_1 [main] - 3091
问题分析
- 以上的结果可能是正数、负数、零。为什么呢?因为 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. 临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区 例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
3. 竞态条件 Race Condition
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
二、synchronized 解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
1. synchronized 解决临界区的竞态条件发生
- 本次使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】。
- 它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
- 这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
2. synchronized 的同步与互斥
-
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。 同步是由于线程执行的先后顺序不同,需要一个线程等待其它线程运行到某个点。
-
同步与互斥的应用 互斥:使用 synchronized 或 ReentrantLock 来达到共享资源互斥效果 同步:使用 wait/notify 或 ReentrantLock 的条件变量 来达到线程间通信效果
3. synchronized 语法
- synchronized 加在普通方法中时,锁住的是 this 对象
// 当线程1获取到锁后, 线程2会阻塞住(BLOCKED)
class Test{
public synchronized void test() {
临界区
}
}
等价于
class Test{
public void test() {
// synchronized 加在普通方法中时,锁住的是 this 对象
synchronized(this) {
临界区
}
}
}
- synchronized 加在静态方法中时,锁住的是 类对象
// 当线程1获取到锁后, 线程2会阻塞住(BLOCKED)
class Test{
public synchronized static void test() {
临界区
}
}
等价于
class Test{
public static void test() {
// synchronized 加在普通方法中时,锁住的是 类对象
synchronized(Test.class) {
临界区
}
}
}
synchronized 解决方案代码
@Slf4j(topic = "c.Test17_1")
public class Test17_1 {
static int counter = 0;
// 锁对象
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}
21:12:00.343 c.Test17_1 [main] - 0
4. synchronized 的类比理解
- synchronized(对象锁) 中的对象锁,可以想象为一个房间(room),有唯一入口(门),房间一次只能进入一人进行计算,线程 t1,t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
- 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 房间(room)出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 房间(room),锁住门,执行它的 count-- 代码
5. synchronized 加锁后的时序图
- synchronized 实际是用【对象锁】保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
6. 面向对象思想改进 synchronized 加锁
@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
// 把需要保护的共享变量放入一个类
class Room {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
public synchronized int getCounter() {
return counter;
}
}
21:43:22.876 c.Test17 [main] - 0
7. synchronized 之 “线程八锁”
- 其实就是考察 synchronized 锁住的是哪个对象
1. 不同线程 锁 同一个 this 对象(n1)
- 打印结果:
- 先1后2
- 先2后1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
2. 不同线程 锁 同一个 this 对象(n1),并且其中一个方法加 sleep
- 打印结果:
- 1s 后打印1、2
- 先打印2,再1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
3. 不同线程 锁 同一个 类对象(Test8Locks .class),并且其中一个方法加 sleep
- 打印结果:
- 1s 后打印1、2
- 先打印2,再1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
// 加了static,锁的是 类对象
public static synchronized void a() {
sleep(1);
log.debug("1");
}
// 加了static,锁的是 类对象
public static synchronized void b() {
log.debug("2");
}
}
4. 不同线程 锁 同一个 类对象(Test8Locks .class),并且其中一个方法加 sleep,再多创建一个 n2 对象
- 打印结果:
- 1s 后打印1、2
- 先打印2,再1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
// 再多创建一个 n2 对象
Number n2 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n2.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
// 加了static,锁的是 类对象
public static synchronized void a() {
sleep(1);
log.debug("1");
}
// 加了static,锁的是 类对象
public static synchronized void b() {
log.debug("2");
}
}
5. 不同线程 锁 不同对象(2个普通对象 n1 、n2),并且其中一个方法加 sleep
- 打印结果:
- 先打印2,1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n2.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
6. 不同线程 锁 不同对象(1个普通对象 n1,1个静态对象 Test8Locks .class),并且其中一个方法加 sleep
- 打印结果:
- 先打印2,1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
7. 不同线程 锁 不同对象(1个普通对象 n1,1个静态对象 Test8Locks .class),并且其中一个方法加 sleep,再多创建一个 n2 对象
- 打印结果:
- 先打印2,1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n2.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
8. 不同线程 锁 同一个 this 对象,并且其中一个方法加 sleep,再加一个没有加 synchronized 的普通方法
- 打印结果:
- 先打印3,1s 后打印1、2
- 先打印3、2 ,再1s 后打印1
- 先打印2、3, 再1s 后打印1
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
new Thread(() -> {
log.debug("begin");
n1.c();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
// 没有加 synchronized 的普通方法
public void c() {
log.debug("3");
}
}
三、 线程安全分析
1. 成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
2. 局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
3. 成员变量与局部变量的线程安全分析
public static void test1() {
int i = 10;
i++;
}
- 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
(1)案例一,不安全的成员变量
public class TestThreadSafe {
// 创建2个线程
static final int THREAD_NUMBER = 2;
// 循环200次
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).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 就会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n4.ThreadUnsafe.method3(TestThreadSafe.java:33)
at cn.itcast.n4.ThreadUnsafe.method1(TestThreadSafe.java:24)
at cn.itcast.n4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
(2)案例二,安全的局部变量
- private 或 final 可以防止修改,符合开闭原则中的【闭】
public class TestThreadSafe {
// 创建2个线程
static final int THREAD_NUMBER = 2;
// 循环200次
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
// final防止子类继承,重写该方法
public final void method1(int loopNumber) {
// 将 list 修改为局部变量
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
// 将list作为参数传递,private防止子类继承重写该方法
private void method2(ArrayList<String> list) {
list.add("1");
}
// 将list作为参数传递,private防止子类继承重写该方法
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
(3)案例三,方法访问修饰符暴露引用带来的线程安全问题
- 如果把 method2 和 method3 的方法修改为 public,并且出现以下情况, 就会有线程安全问题:
- 情况1:有其它线程调用 method2 和 method3
- 情况2:在 情况1 的基础上,添加一个子类,子类覆盖 method2 或 method3 方法,并且在子类方法中再开一个线程
public class TestThreadSafe {
// 创建2个线程
static final int THREAD_NUMBER = 2;
// 循环200次
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
// 将 list 修改为局部变量
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
// 将list作为参数传递,把 method2 和 method3 的方法修改为 public
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) {
System.out.println(2);
new Thread(() -> {
list.remove(0);
}).start();
}
}
Exception in thread "Thread-384" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n4.ThreadSafeSubClass.lambda$method3$0(TestThreadSafe.java:62)
at java.lang.Thread.run(Thread.java:748)
4. 常见线程安全的类
一、常见线程安全类
-
String
-
Integer
-
StringBuffer
-
Random
-
Vector
-
Hashtable
-
java.util.concurrent 包下的类
-
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
-
也可以理解为它们的每个方法是原子的。
注意:它们多个方法的组合不是原子的。
二、线程安全类方法的组合,不是线程安全的
Hashtable table = new Hashtable();
// 线程1,线程2,同时执行时,会有线程安全问题
if( table.get("key") == null) {
table.put("key", value);
}
- 虽然Hashtable的get、set方法都是线程安全的,但是get、put之间,可能有其他线程也在操作,并且操作的还是同一个共享变量table,这样会导致线程安全问题。
三、不可变类线程安全性
- String、Integer、StringBuffer都是不可变类,因为其内部的属性(状态)不可以改变,因此它们的方法都是线程安全的。
- String 虽然有 replace,substring 等方法【可以】改变值,其实是new了一个新对象,修改的是拷贝对象的值。
String 源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 截取字符串的方法
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 以当前字符串的value,再创建一个新的字符串,原有的字符串并没有改变
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 新创建的字符串会对原有的字符串进行拷贝
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
}
四、实例分析,代码是否线程安全
- 不安全,虽然D2的引用地址不能变,但是Date对象里的其他属性会改变,从而造成线程不安全。
// 成员变量
final Date D2 = new Date();
- 不安全,UserServiceImpl里的成员变量会被多线程共享。
public class MyServlet extends HttpServlet {
// 不安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
- 不安全,在Spring中单例bean会被共享,类里面的成员变量也会被共享,可以加一个环绕通知将成员变量作为局部变量
// Spring AOP,例如:自定义一个统计时间的切面类
@Aspect
@Component
public class MyAspect {
// 成员变量
private long start = 0L;
// 前置通知,记录开始时间
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
// 后置通知,记录结束时间
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
}
- 安全,conn是局部变量,userService、userDao是私有变量,不会被重写
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
- 不安全,conn是成员变量可以多线程共享
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
- 安全,虽然conn是成员变量,但是userDao是局部变量,每次调用UserServiceImpl时,都会创建新对象,但不建议这样写
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
- 不安全,抽象方法foo会被重写, foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
// 抽象方法,会被重写, foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
// 局部变量sdf的引用会被暴露出去
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
// 例如:重写抽象方法,再创建线程修改共享变量,导致不安全
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();
}
}
四、多线程习题
1. 卖票案例
- 不对sell()方法加锁时,会有线程安全问题
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟1000人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计,Vector是线程安全的集合
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) { // 2000个线程
Thread thread = new Thread(() -> {
// 这里会发生竞态条件
// 每个人随机买票数量
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}
// 等待所有线程执行完
for (Thread thread : threadList) {
thread.join();
}
// 统计卖出的票数和剩余票数
// 统计卖出的票数和剩余票数
System.out.println("余票:" + window.getCount());
System.out.println("卖出的票数:" + amountList.stream().mapToInt(i-> i).sum());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 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;
}
}
}
测试脚本
- 在 Windows 下 使用 CMD 语法写测试脚本,进行批处理循环测试
参数 /L (该集表示以增量形式从开始到结束的一个数字序列。可以使用负的 Step)
---
格式:FOR /L %variable IN (start,step,end) DO command [command-parameters]
该集表示以增量形式从开始到结束的一个数字序列。可以使用负的 Step
从1开始,每次增加1,循环5次 Java 代码
for /L %n in (1,1,5) do java cn.itcast.n4.exercise.ExerciseSell
步骤
- 先编译 cn.itcast.n4.exercise.ExerciseSell 类,cn.itcast.n4.exercise.ExerciseSell 类会被放在target的classes目录下
- 在该目录下输入脚本命令
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>for /L %n in (1,1,5) do java cn.itcast.n4.exercise.ExerciseSell
- 以下是循环测试的结果
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1010
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1000
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1001
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1000
D:\学习资料\资料-并发编程\并发编程代码\concurrent\case_java8\target\classes>java cn.itcast.n4.exercise.ExerciseSell
余票:0
卖出的票数:1000
分析
- 临界区:多个线程对共享变量进行读写操作
// 模拟1000人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计,Vector是线程安全的集合
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) { // 2000个线程
Thread thread = new Thread(() -> {
// 每个人随机买票数量
// 此处是临界区,window是共享变量,sell方法有读写操作,所以需要对sell方法进行加锁
int amount = window.sell(random(5));
// 统计买票数
// 此处是临界区,amountList是共享变量,但是amountList是Vector集合本身是线程安全的,这里不用再处理
amountList.add(amount);
// 注意:window、amountList属于不同的共享变量,所以不用考虑这两行代码组合时的线程安全问题
});
// threadList是共享变量,但在这里只会被main线程使用,所以在这里不用考虑线程安全问题
threadList.add(thread);
thread.start();
}
- 综上分析,最终只需在sell方法加锁
// 售票,对修改共享变量进行加锁
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
2. 转账案例
@Slf4j(topic = "c.ExerciseTransfer")
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(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
分析
public void transfer(Account target, int amount) {
// 这里的对象锁不能是synchronized (this),因为该方法涉及到2个共享变量 this、target,
// 这2个共享变量都是Account类型,所以需要加Account.class对象锁
synchronized (Account.class) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}