JavaSE 进阶(十九)

线程同步

并发:多个线程同时操作同一个对象。
线程同步:其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了冲突问题,为了保证数据被操作时的正确性,在操作时加入锁机制(synchronized)。当一个线程要操作一个对象时,会获得这个对象的排它锁,会独占此对象的资源,其它线程必须等待,使用完后就会释放锁。但会存在以下问题:

  • 一个线程持有锁会导致其它所有需要此锁的线程挂起,引起性能问题。
  • 在多线程竟争下,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

不安全案例

买票案例

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
package ml.guest997;

public class BuyTickets implements Runnable {
private int tickets = 10;

@Override
public void run() {
try {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- + "张票");
Thread.sleep(1000); //模拟延时能够扩大问题的发生性
continue;
}
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
BuyTickets buyTickets = new BuyTickets();
new Thread(buyTickets,"01").start();
new Thread(buyTickets,"02").start();
new Thread(buyTickets,"03").start();
}

}
/*结果为
01拿到了第10张票
02拿到了第9张票
03拿到了第8张票
01拿到了第7张票
03拿到了第5张票
02拿到了第6张票
02拿到了第4张票
03拿到了第4张票
01拿到了第3张票
03拿到了第2张票
02拿到了第2张票
01拿到了第2张票
02拿到了第1张票
01拿到了第0张票
*/

可以从结果看到,有的票被重复拿到,甚至拿到了不存在的0票。

取钱案例

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
package ml.guest997;

public class WithDraw {
public static void main(String[] args) {
Account account = new Account("测试账户", 1000);
new Thread(new Bank(account, 500, "guest")).start();
new Thread(new Bank(account, 1000, "admin")).start();
}
}

class Account {
private String name;
private int money;

public Account(String name, int money) {
this.name = name;
this.money = money;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getMoney() {
return money;
}

public void setMoney(int money) {
this.money = money;
}
}

class Bank implements Runnable {

private Account account;
private int getMoney;
private int nowMoney;
private String username;

public Bank(Account account, int getMoney, String name) {
this.account = account;
this.getMoney = getMoney;
this.username = name;
}

@Override
public void run() {
if (account.getMoney() < getMoney) {
System.out.println("取钱失败,余额不足");
} else {
try {
Thread.sleep(1000);
nowMoney = account.getMoney() - getMoney;
account.setMoney(nowMoney);
System.out.println(username + " 成功取钱:" + getMoney + ",账户余额为:" + nowMoney);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/*结果为
admin 成功取钱:1000,账户余额为:0
guest 成功取钱:500,账户余额为:-500
*/

可以从结果看到,账户余额变成了负数。

集合案例

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

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

public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
//Thread.sleep(3000);
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}).start();
}
}
}
/*结果为
[null, d4743923]
[null, d4743923, 8cd9e495]
[null, d4743923]
*/

正常来说,打印出来的集合元素应该分别有1、2、3个,但是出现上面的结果的原因是 CPU 切换太快了,读写的顺序乱了。如果使上面的线程休眠语句生效的话,会发现直接报错了:ConcurrentModificationException(并发修改异常),原因就是一个线程正在写入,另外一个线程抢夺执行权,导致数据不一致。

同步方法

使用 synchronized 关键字修饰的方法控制对象的操作,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺点:方法里可能有只读代码,但是只有修改代码才需要锁,因此锁得太多就会降低效率。

同步块

同步块: synchronized (obj) { }
obj:同步监视器,可以是任何对象,但是推荐使用共享资源作为同步监视器。

同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this,就是这个对象本身或者是 class。

改造不安全案例

买票案例

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
package ml.guest997;

public class BuyTickets implements Runnable {
private int tickets = 10;

@Override
public void run() { //不能锁 run(),因为只有当循环结束才会释放锁,所以第一个得到锁的对象会把全部票拿走。
try {
while (true) { //不能锁在 while 前,理由跟上面相同。
synchronized (this) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- + "张票");
Thread.sleep(1000); //模拟延时能够扩大问题的发生性
continue;
}
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
BuyTickets buyTickets = new BuyTickets();
new Thread(buyTickets,"01").start();
new Thread(buyTickets,"02").start();
new Thread(buyTickets,"03").start();
}

}
/*结果为
01拿到了第10张票
03拿到了第9张票
03拿到了第8张票
02拿到了第7张票
03拿到了第6张票
01拿到了第5张票
01拿到了第4张票
03拿到了第3张票
02拿到了第2张票
02拿到了第1张票
*/

多次运行之后会发现,票数一定是从10开始减到1,并且票不会被重复拿到。

每次判断是否加锁,也会有性能的消耗,进入之后再出来,哪怕什么也不做,也是消耗。其实可以在加锁的前面再加一重判断,那么之后就没必要再判断是否加锁了。

取钱案例

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
package ml.guest997;

public class WithDraw {
public static void main(String[] args) {
Account account = new Account("测试账户", 1000);
new Thread(new Bank(account, 500, "guest")).start();
new Thread(new Bank(account, 1000, "admin")).start();
}
}

class Account {
private String name;
private int money;

public Account(String name, int money) {
this.name = name;
this.money = money;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getMoney() {
return money;
}

public void setMoney(int money) {
this.money = money;
}
}

class Bank implements Runnable {

private Account account;
private int getMoney;
private int nowMoney;
private String username;

public Bank(Account account, int getMoney, String name) {
this.account = account;
this.getMoney = getMoney;
this.username = name;
}

@Override
public void run() {
if (account.getMoney() == 0) {
return;
}
synchronized (account) {
if (account.getMoney() < getMoney) {
System.out.println("取钱失败,余额不足");
} else {
try {
Thread.sleep(1000);
nowMoney = account.getMoney() - getMoney;
account.setMoney(nowMoney);
System.out.println(username + " 成功取钱:" + getMoney + ",账户余额为:" + nowMoney);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

注意:两个实例对象开启了两条线程,每条线程用的锁对象都是当前实例对象,锁对象不同是无法实现同步的。所以这里要锁的是 account 对象,因为要操作的资源就在它这,否则是没有用的。

集合案例

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
package ml.guest997;

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

public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (list) {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}
}).start();
}
}
}
/*结果为
[d551b4a4]
[d551b4a4, 9c66a664]
[d551b4a4, 9c66a664, 6960482c]
*/