线程的同步
# 问题的提出
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
比如:当你和媳妇去取钱时,2个线程都是取2000元,但是账户里面只有3000,这时如果多线程安全问题不解决,就会出现2个人都能取钱成功。
程序执行是需要时间的,虽然这个时间极短,当一个线程判断为true时,还未执行到扣钱的时候,另一个线程同事也达到了判断条件,并且判断true时,那么2个线程都能取钱,这就是线程安全问题。
包括之前的3个窗口卖票的场景,也会出现线程安全问题。
class Window2 implements Runnable {
private int ticket = 100;
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
// System.out.println(Thread.currentThread().getName() + "抢不到票,卖光了!");
break;
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 w1 = new Window2();java
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
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
窗口2:卖票,票号为:100
窗口1:卖票,票号为:100
窗口3:卖票,票号为:100
窗口3:卖票,票号为:97
窗口2:卖票,票号为:97
窗口1:卖票,票号为:97
窗口2:卖票,票号为:94
窗口1:卖票,票号为:94
窗口3:卖票,票号为:94
窗口2:卖票,票号为:91
窗口3:卖票,票号为:91
窗口1:卖票,票号为:91
窗口3:卖票,票号为:88
窗口1:卖票,票号为:88
窗口2:卖票,票号为:88
窗口3:卖票,票号为:85
窗口1:卖票,票号为:85
窗口2:卖票,票号为:85
窗口3:卖票,票号为:82
窗口2:卖票,票号为:82
窗口1:卖票,票号为:82
窗口2:卖票,票号为:79
窗口3:卖票,票号为:79
窗口1:卖票,票号为:79
窗口1:卖票,票号为:76
窗口2:卖票,票号为:76
窗口3:卖票,票号为:76
窗口1:卖票,票号为:73
窗口2:卖票,票号为:73
窗口3:卖票,票号为:73
窗口2:卖票,票号为:70
窗口3:卖票,票号为:70
窗口1:卖票,票号为:70
窗口1:卖票,票号为:67
窗口3:卖票,票号为:67
窗口2:卖票,票号为:67
窗口3:卖票,票号为:64
窗口2:卖票,票号为:64
窗口1:卖票,票号为:64
窗口3:卖票,票号为:61
窗口1:卖票,票号为:61
窗口2:卖票,票号为:61
窗口2:卖票,票号为:58
窗口1:卖票,票号为:58
窗口3:卖票,票号为:58
窗口2:卖票,票号为:55
窗口1:卖票,票号为:55
窗口3:卖票,票号为:55
窗口2:卖票,票号为:52
窗口3:卖票,票号为:52
窗口1:卖票,票号为:52
窗口2:卖票,票号为:49
窗口1:卖票,票号为:49
窗口3:卖票,票号为:49
窗口3:卖票,票号为:46
窗口2:卖票,票号为:46
窗口1:卖票,票号为:46
窗口1:卖票,票号为:43
窗口2:卖票,票号为:43
窗口3:卖票,票号为:43
窗口3:卖票,票号为:40
窗口2:卖票,票号为:40
窗口1:卖票,票号为:40
窗口1:卖票,票号为:37
窗口2:卖票,票号为:37
窗口3:卖票,票号为:37
窗口2:卖票,票号为:34
窗口3:卖票,票号为:34
窗口1:卖票,票号为:34
窗口3:卖票,票号为:31
窗口2:卖票,票号为:31
窗口1:卖票,票号为:31
窗口2:卖票,票号为:28
窗口1:卖票,票号为:28
窗口3:卖票,票号为:28
窗口1:卖票,票号为:25
窗口2:卖票,票号为:25
窗口3:卖票,票号为:25
窗口3:卖票,票号为:22
窗口2:卖票,票号为:22
窗口1:卖票,票号为:22
窗口1:卖票,票号为:19
窗口3:卖票,票号为:19
窗口2:卖票,票号为:19
窗口2:卖票,票号为:16
窗口3:卖票,票号为:16
窗口1:卖票,票号为:16
窗口1:卖票,票号为:13
窗口2:卖票,票号为:13
窗口3:卖票,票号为:13
窗口1:卖票,票号为:10
窗口3:卖票,票号为:10
窗口2:卖票,票号为:10
窗口3:卖票,票号为:7
窗口2:卖票,票号为:7
窗口1:卖票,票号为:7
窗口2:卖票,票号为:4
窗口3:卖票,票号为:4
窗口1:卖票,票号为:4
窗口3:卖票,票号为:1
窗口1:卖票,票号为:1
窗口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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
这里不但会出现重复票的事情,还有可能是负数。
sleep
只是为了模拟线程的阻塞状态,目的是提高安全隐患爆发的概率,提升出现0或者1出现的概率。
不能因为概率小就不会出现,不去解决,这是错误的,在程序中,就算是0.01%也要去解决。
否则在现实业务场景中,产生问题造成的经济损失无法估量。可能你不理解这句话的意思,打个夸张的比喻,当坐飞机,每一万次航班出现一次故障,那么它的后果就非常恐怖了。
# 问题的原因
那么一定会出现线程安全问题吗?
也不一定,但是只要操作同一个共享数据时,就可能会发生,比如火车站抢票、明星演唱会抢票、电商网站秒杀活动等等。这些都会存在安全隐患。
就拿上面卖票的案例,票数应该是递减才对,不会出现重票或错票。是的在理想状态下是这样,但是如果是极端状态,别的线程也一起进去操作数据时,就会产生线程安全问题。
理想的状态
极端状态
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
# 解决办法
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
那么怎么实现呢?就是加锁。
# Synchronized关键字
Java对于多线程的安全问题提供了专业的解决方式:同步机制。
Synchronized
有两种方式来实现同步机制:
- 同步代码块
- 同步方法
同步代码块:
synchronized (同步监视器){
// 需要被同步的代码;
}
2
3
- 需要被同步的代码:是指操作共享数据的代码,即为需要被同步的代码。
- 共享数据:多个线程共同操作的变量。比如:本案例中的
ticket
变量。
- 共享数据:多个线程共同操作的变量。比如:本案例中的
- 同步监视器:俗称:锁。任何一个类的对象,都可以充当锁。
- 要求:多个线程必须要公用同一把锁。
同步方法
synchronized
还可以放在方法声明中,表示整个方法为同步方法
例如:
public synchronized void show (String name){
....
}
2
3
在同步方法中,它的同步监视器其实是this
# 分析同步原理
t1、t2、t3、看谁先抢到同步锁,抢到了同步锁的线程,进入代码块内,操作数据,其他线程等待,等操作完毕,线程将同步锁释放,3个线程又重新开始抢同步锁,一直循环……
在Java中,通过同步机制,来解决了线程的安全问题。但是操作同步代码时,只能有一个线程参与,其他线程等待。相当于一个单线程的过程,效率变低了。
使用同步代码块解决案例线程安全问题
class Window implements Runnable {
//共享数据
private int ticket = 100;
//监视器/锁
private static final Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
// System.out.println(Thread.currentThread().getName() + "抢不到票,卖光了!");
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
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
输出结果是递减了,虽然很多次都是窗口1,但是只是概率问题,全部被t1
抢到了同步锁,有时还是能有其他线程能同步抢到了。
使用同步方法解决实现Runnable案例线程安全问题
其实这个案例不太适合使用同步方法,因为while
也是在操作共享数据时的必要条件。如果使用同步方法,`会导致一个窗口一直售票,其他窗口都拿不到锁。所以还需要定义另一个方法。
class Window2 implements Runnable {
//共享数据
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
this.show();
}
}
private synchronized void show() {
if (ticket > 0) {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 w1 = new Window2();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
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
使用同步方法处理继承Thread类的方式中的线程安全问题
class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
show();
}
}
private static synchronized void show() {//同步监视器为Window4.class
// private synchronized void show() 同步监视器为t1,t2,t3,此种解决方式是错误的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
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
# 关于同步方法的总结
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
- 同步方法的锁:
- 静态方法(
类名.class
),当前类本身 - 非静态方法(
this
),当前对象
- 静态方法(
- 同步代码块:自己指定,很多时候也是指定为
this
或类名.class
注意
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(
类名.class
),所有非静态方法共用同一把锁(this
),同步代码块(指定需谨慎)需要谨慎的原因是,如果是继承方式实现多线程,是多个对象,
this
是指当前对象,肯定不是同一把锁,所以需要用类名.class
类名.class
,类也是对象。而且也是唯一的对象,因为类只会加载一次。那么就可以成为共用的一把锁。Class clazz = Window.class;
1
# 同步范围
1、如何找问题,即代码是否存在线程安全?(非常重要)
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中。
3、切记
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
# 释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到
break
、return
终止了该代码块、该方法的继续执行。 - 当前线程在同步代码块、同步方法中出现了未处理的
Error
或Exception
,导致异常结束。 - 当前线程在同步代码块、同步方法中执行了线程对象的
wait()
方法,当前线程暂停,并释放锁。
# 不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行。 - 线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放锁(同步监视器)。- 应尽量避免使用
suspend()
和resume()
来控制线程8.4 线程的同步
- 应尽量避免使用
# 懒汉式线程安全问题
使用同步机制,将单例模式中的懒汉式改写为线程安全的。
回顾懒汉式实现步骤:
1、私有化构造器
2、声明当前类的对象
3、提供公共的静态方法返回类的对象
线程安全有问题的写法:
public class Bank{
//1,私有化构造器
private Bank() {
}
//2,声明当前类的对象
private static Bank instance = null;
//3,提供公共的静态方法返回类的对象
public static Bank getInstance() {
if (instance == null) {
instance = new Bank();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
同步方法解决方案
public class Bank {
//1,私有化构造器
private Bank() {
}
//2,声明当前类的对象
private static Bank instance = null;
//3,提供公共的静态方法返回类的对象
public static synchronized Bank getInstance() {
if (instance == null) {
instance = new Bank();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
同步代码块解决方案
public class Bank {
//1,私有化构造器
private Bank() {
}
//2,声明当前类的对象
private static Bank instance = null;
//3,提供公共的静态方法返回类的对象
public static Bank getInstance() {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这种方式效率稍差,为什么?
当对象未创建时,可以用synchronized
来确保只有一个对象被创建,不会产生线程安全问题。
但是对象如果已经被创建后,其实可以直接返回对象了,return
不是操作共享数据的,当对象已经创建时,所有线程还是要排队一个个地拿到同步锁,才能获取对象,所以效率稍差。
# 死锁
线程的死锁
产生原因:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
导致问题:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,程序无法继续。
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
演示线程的死锁问题
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
//提高占用时间,提高死锁概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
//提高占用时间,提高死锁概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
}
}
}
}).start();
}
}
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
如果不调用线程的sleep,这个死锁概率很低,都能正常输出,但是现在同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,程序无法继续。
我们使用同步锁时,要避免死锁。
演示死锁问题
package com.saul.deadlock;
class A {
public synchronized void foo(B b) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {java
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
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
输出结果:
当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 主线程 企图调用B实例的last方法
当前线程名: 副线程 企图调用A实例的last方法
2
3
4
程序到这里就无法继续了
我们先来看主线程:
当main
方法作为主线程启动后,主线线程调用init
方法,然后调用调用a
对象的foo
方法,foo
是一个同步方法,里面又调用b
对象的last
方法,last
方法也是一个同步方法,只有执行完last
方法才算执行完。
那么主线程要用到的锁有:
- 1、
A.class
- 2、
B.class
现在来看副线程:
main
方法启动后,创建一个DeadLock
对象,启动线程对象的strat
方法,自动调用run
方法,里面是调用b
对象的bar
方法,bar
方法是一个同步方法,里面又调用a
对象的last
方法,last
方法也是一个同步方法,只有执行完last
方法才算执行完。
那么分线程要用到的锁有:
- 1、
B.class
- 2、
A.class
那么跟上面的案例很类似,是现在同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。所以程序无法继续了。
这些案例调用Thread.sleep
方法只是提高死锁的概率,就算没这个概率,程序还是存在安全隐患,所以我们写程序要避免死锁。
# Lock
(锁)
- 从
JDK 5.0
开始,Java
提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock
对象充当。 - **
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。**锁提供了对共享资源的独占访问,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应先获得Lock
对象。 ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁。
实现步骤
1、实例化ReentrantLock
对象
2、调用锁定方法lock()
3、把代码块放入try{}
当中
4、调用解锁方法unlock()
.
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}finally{
lock.unlock();
}
}
}
2
3
4
5
6
7
8
9
10
11
这样说比较抽象,解析来用案例来说明:
就以之前卖票的案例来:
/**
* 解决线程安全问题的方式三:Lock锁 -- JDK5.0新增
*
* @author Saul.J.Wu
* @date 2020/12/20
*/
class Window implements Runnable {
private int ticket = 100;
//实例化`ReentrantLock` 对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//调用`ReentrantLock` .`lock`锁定方法
lock.lock();
try {
if (ticket > 0) {
Thread.sleep(1);
//提高安全隐患概率
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//调用解锁方法`unlock`.
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
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
这样就解决了线程安全问题,不会出现重票或者错票。
# synchronized
与Lock
的对比
synchronized
与Lock
都是用来解决线程安全问题Lock
是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized
是隐式锁,出了作用域自动释放。Lock
只有代码块锁,synchronized
有代码块锁和方法锁使用
Lock
锁,JVM
将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
在开发当中,对于解决线程安全问题,优先使用顺序:Lock
>>>同步代码块(已经进入了方法体,分配了相应资源)>>>>同步方法(在方法体之外)
虽然很多源码都是用synchronized
,那是因为那些源码是很久以前写的。
# 练习
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1,明确哪些代码是多线程运行代码,须写入run()方法
2,明确什么是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
【分析】
1、是否是多线程问题?是,两个储户
2、是否有共享数据?有,账户(或账户余额)
3、是否有线程安全问题?有
4、需要考虑解决线程安全问题?同步机制:有三种方式。
- Lock关键字
synchronized
同步代码块synchronized
同步方法
未解决线程安全问题
class Account {
private double balacne;
public Account(double balacne) {
this.balacne = balacne;
}
/**
* 存钱
*
* @param amt 要存的金额
*/
public void deposit(double amt) {
double temp = balacne;
balacne += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存入" + amt + ",余额:" + temp + "=>" + balacne);
}
}
class Customer extends Thread {
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account account = new Account(0);
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
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
- 输出结果:
甲存入1000.0,余额:0.0=>2000.0
乙存入1000.0,余额:1000.0=>2000.0
甲存入1000.0,余额:2000.0=>4000.0
乙存入1000.0,余额:3000.0=>4000.0
乙存入1000.0,余额:5000.0=>6000.0
甲存入1000.0,余额:4000.0=>6000.0
2
3
4
5
6
未解决线程安全问题,明显存在问题。
这里我只使用
synchronized
同步方法就解决了
因为此时只有一个账户,直接使用synchronized
同步方法就解决了。直接在操作共享数据的方法上加上关键字synchronized
。
public synchronized void deposit(double amt) {
double temp = balacne;
balacne += amt;
try {
Thread.sleep(1000java);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存入" + amt + ",余额:" + temp + "=>" + balacne);
}
2
3
4
5
6
7
8
9
10
输出结果:
甲存入1000.0,余额:0.0=>1000.0
甲存入1000.0,余额:1000.0=>2000.0
甲存入1000.0,余额:2000.0=>3000.0
乙存入1000.0,余额:3000.0=>4000.0
乙存入1000.0,余额:4000.0=>5000.0
乙存入1000.0,余额:5000.0=>6000.0
2
3
4
5
6
这样就保证线程安全问题了。