0%

多线程并发问题与解决方法

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。使用多线程可能遇到的问题:

并发问题

并发:同一个对象被多个线程同时操作。

例子一买票

问题呈现:

创建类TestThread4,实现Runnable接口。同时创建三个线程对象(小明、Pete、钢铁侠)同时操作该类的实例化对象ticker。

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
public class TestThread4 implements Runnable{
private int ticketNums = 10;//票数
@Override
public void run() {
while (true){
if (ticketNums <= 0){
break;
}
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread().getName()方法获取线程名字
System.out.println(Thread.currentThread().getName()+"----拿到了第"+ticketNums--+"张票~");
}
}

public static void main(String[] args) {
TestThread4 ticker = new TestThread4();

new Thread(ticker,"小明").start();
new Thread(ticker,"Pete").start();
new Thread(ticker,"钢铁侠").start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Pete----拿到了第9张票~
小明----拿到了第8张票~
钢铁侠----拿到了第10张票~
Pete----拿到了第6张票~
钢铁侠----拿到了第5张票~
小明----拿到了第7张票~
Pete----拿到了第4张票~
钢铁侠----拿到了第2张票~
小明----拿到了第3张票~
Pete----拿到了第1张票~
钢铁侠----拿到了第1张票~
小明----拿到了第0张票~

Process finished with exit code 0

观察程序运行结果,我们发现有同一张票被两个人抢到的情况。多个线程操作同一个资源时,线程不安全,数据紊乱。

例子二银行取钱

账户类Account,记录卡名和余额。

银行类Drawing,模拟取钱操作,操作对象为账户。

添加线程休眠Thread.sleep(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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class UnSafeBank {
public static void main(String[] args) {
//账户
Account account = new Account(100,"农业银行");

Drawing pete = new Drawing(account,50,"pete");
Drawing peteGirlfriend = new Drawing(account,100,"peteGirlfriend");

pete.start();
peteGirlfriend.start();
}
}
//账户
class Account{
int money;//卡内余额
String name;//卡名
public Account(int money,String name){
this.money = money;
this.name = name;
}
}
//银行
//模拟取款
class Drawing extends Thread{
Account account;//账户
int drawingMoney;//取了多少钱
int nowMoney;//现在手里多少钱
public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//判断账户还有没有钱
if(account.money - drawingMoney < 0){
System.out.println("--------余额不足,取款失败!--------");
return;
}
try {
Thread.sleep(1000);//放大问题的发生性
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;

//Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"取了"+nowMoney+"元");

System.out.println(account.name+"余额为:"+account.money+"\n");
}
}

运行结果:

同一个资源被多个线程同时操作,导致数据紊乱。语句System.out.println("--------余额不足,取款失败!--------");并没有正常执行。

1
2
3
4
5
6
7
pete取了50
农业银行余额为:50

peteGirlfriend取了100
农业银行余额为:-50

Process finished with exit code 0

例子三不安全的集合

启用线程将线程名添加到到集合,操作对象为集合。for循环,建立10万条线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UnSafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("集合的大小:"+list.size());
}
}

运行结果:

使用10万条线程添加数据到集合中,最后结果应该是集合中有10万条数据才对。

1
2
3
集合的大小:99998

Process finished with exit code 0

要解决这个问题,可以给使用下边的线程同步解决方法或者使用JDK中提供的线程安全类集合CopyOnWriteArrayList(底层使用了Lock锁)。

解决方法

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

synchronized关键字

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

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

实现线程同步,使用synchronized关键字。用法:synchronized方法与synchronized块。

synchronized方法

synchronized方法也可以叫做同步方法,就跟static关键字一样加在方法的前边用来修饰方法即可。同步方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺点:若将一个大的方法申明为synchronized将会影响效率。方法里面需要修改的内容才需要锁,锁的太多会浪费资源。

synchronized块

synchronized块也可以叫做同步块synchronized( Obj ){ }

Obj称之为同步监视器

  • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器。(共享资源可以理解为多个线程共同操作的对象
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class。

同步监视器执行过程

  1. 第一个线程访问,锁定同步监视器,执行其中代码。
  2. 第二个线程访问,发现同步监视器被锁定,无法访问。
  3. 第一个线程访问完毕,解锁同步监视器。
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

代码演示

使用synchronized关键字对前边的银行取钱例子进行修改:

注意synchronized关键字需要锁的是共享资源即多个线程共同操作的对象(同步监视器),如果这里使用同步方法即关键字加在取钱的run方法上是不行的。因为同步方法的同步监视器是this即银行取钱类Drawing。

1
2
3
4
5
6
@Override
public synchronized void run() {
//判断账户还有没有钱
...
System.out.println(account.name+"余额为:"+account.money+"\n");
}

取钱操作是对账户中的钱进行增删改查,锁的对象应该是account,应使用同步块

1
2
3
4
5
6
7
8
@Override
public void run() {
synchronized(account){
//判断账户还有没有钱
...
System.out.println(account.name+"余额为:"+account.money+"\n");
}
}

完整代码:

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
public class UnSafeBank {
public static void main(String[] args) {
//账户
Account account = new Account(100,"农业银行");

Drawing pete = new Drawing(account,50,"pete");
Drawing peteGirlfriend = new Drawing(account,100,"peteGirlfriend");

pete.start();
peteGirlfriend.start();
}
}
//账户
class Account{
int money;//卡内余额
String name;//卡名
public Account(int money,String name){
this.money = money;
this.name = name;
}
}
//银行
//模拟取款
class Drawing extends Thread{
Account account;//账户
int drawingMoney;//取了多少钱
int nowMoney;//现在手里多少钱
public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
synchronized(account){
//判断账户还有没有钱
if(account.money - drawingMoney < 0){
System.out.println("--------余额不足,取款失败!--------");
return;
}
try {
Thread.sleep(1000);//放大问题的发生性
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;

//Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"取了"+nowMoney+"元");
System.out.println(account.name+"余额为:"+account.money+"\n");
}
}
}

运行结果:

1
2
3
4
5
6
pete取了50
农业银行余额为:50

--------余额不足,取款失败!--------

Process finished with exit code 0

LOCK锁

  • 从JDK5.0开始,java提供了更强大的线程同步机制:通过显式定义同步锁对像来实现同步。(同步锁对像即Lock对像)(显式定义–区别于前边的synchronized关键字)

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对像加锁,线程开始访问共享资源之前应先获得Lock对像

  • ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。(synchronized关键字都是离开synchronized块或方法的范围自动释放锁)(ReentrantLock可重入锁

代码演示

使用ReentrantLock对像对前边的买票例子进行修改:

创建ReentrantLock对象mylock,用于加锁和解锁。

1
private final ReentrantLock mylock = new ReentrantLock();

注意:使用try-finally结构,加锁放在try中,解锁放在finally中。

1
2
3
4
5
6
7
8
try{
mylock.lock();

//此处为需要保证线程安全的代码

}finally {
mylock.unlock();
}

对前边的买票例子中的run方法中操作资源部分进行修改,放在try-finally结构中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try{
mylock.lock();



if (ticketNums <= 0){
break;
}
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread().getName()方法获取线程名字
System.out.println(Thread.currentThread().getName()+"----拿到了第"+ticketNums--+"张票~");



}finally {
mylock.unlock();
}

完整代码:

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
import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
public class TestLock implements Runnable{
private int ticketNums = 10;//票数

//定义lock锁
private final ReentrantLock mylock = new ReentrantLock();//可重入锁

@Override
public void run() {
while (true){

try{
mylock.lock();


if (ticketNums <= 0){
break;
}
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread().getName()方法获取线程名字
System.out.println(Thread.currentThread().getName()+"----拿到了第"+ticketNums--+"张票~");


}finally {
mylock.unlock();
}

}
}

public static void main(String[] args) {
TestLock ticker = new TestLock();

new Thread(ticker,"小明").start();
new Thread(ticker,"Pete").start();
new Thread(ticker,"钢铁侠").start();
}
}

运行结构:

1
2
3
4
5
6
7
8
9
10
11
12
小明----拿到了第10张票~
小明----拿到了第9张票~
小明----拿到了第8张票~
小明----拿到了第7张票~
小明----拿到了第6张票~
小明----拿到了第5张票~
小明----拿到了第4张票~
小明----拿到了第3张票~
小明----拿到了第2张票~
小明----拿到了第1张票~

Process finished with exit code 0

总结

synchronized关键字与Lock接口的对比。

  • Lock锁显式锁(手动开启和关闭锁,别忘记了关闭锁),synchronized是隐式锁,出了作用域自动释放。
  • Lock只有代码块锁,synchronized有代码块和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)。(建议使用synchronized,因为是隐式锁,避免出现不必要的问题。)

死锁问题

什么是死锁

多个线程各自占有一些共享资源,并且互相对待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系。

如何避免死锁:上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

代码演示

使用代码演示化妆时,多人需要使用同一个化妆品时产生的死锁问题。

创建两个资源类:口红Lipstick、镜子Mirror

一个调用资源的类:化妆Makeup(通过继承Thread类来实现多线程)

1
2
3
4
5
6
7
8
9
10
11
//口红
class Lipstick{
}

//镜子
class Mirror{
}

//化妆,调用口红、镜子等资源
class Makeup extends Thread{
}

死锁

Makeup类具体代码如下:

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
//化妆,调用口红、镜子等资源
class Makeup extends Thread{

//需要的资源只有一份,用static来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();

int choice;//选择
String girlName;//使用化妆品(资源)的人

Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {

//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}

}
//化妆,互相持有对方的锁,需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0){
synchronized (lipstick){//获得口红的锁(拿口红来用)
System.out.println(this.girlName + "拿了口红");
Thread.sleep(1000);
synchronized (mirror){//一秒钟后想获得镜子的锁(此时还没放下口红,想继续拿镜子用)
System.out.println(this.girlName + "拿了镜子");
}
}
}else {
synchronized (mirror){//获得镜子的锁(拿镜子来用)
System.out.println(this.girlName + "拿了镜子");
Thread.sleep(2000);
synchronized (lipstick){//一秒钟后想获得口红的锁(因为synchronized是嵌套在一起的,所以此时还没放下镜子,想继续拿口红用)
System.out.println(this.girlName + "拿了口红");
}
}
}
}
}

先模拟会出现死锁的情况。两个synchronized块嵌套在一起,模拟拿起了某样资源还未放下时又去拿另一样资源。(里头使用了线程休眠sleep,是为了放大问题的发生性。)

写一个测试类DeadLock:
两个Makeup对象模拟两个准备要化妆的人,灰姑娘与白雪公主。灰姑娘先拿口红再去拿镜子,白雪公主先拿镜子再去拿口红。(两个人取东西顺序是反过来的)

1
2
3
4
5
6
7
8
9
10
11
//死锁:多个线程相互抱着对方需要的资源,然后形成僵持
public class DeadLock {

public static void main(String[] args) {
Makeup girl01 = new Makeup(0,"灰姑娘");
Makeup girl02 = new Makeup(1,"白雪公主");

girl01.start();
girl02.start();
}
}

运行结果:

灰姑娘与白雪公主是拿不到她们想要的东西的,因为灰姑娘先拿口红再去拿镜子,白雪公主先拿镜子再去拿口红。她们都是拿起了某样资源还未放下时又去拿另一样资源。

1
2
灰姑娘拿了口红
白雪公主拿了镜子

改进

对run方法内执行的makeup方法进行改进,将嵌套的synchronized拆开,模拟需要去拿另一个资源时,先将当前使用的资源释放掉再去拿另一样资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (choice == 0){
synchronized (lipstick){//获得口红的锁(拿口红来用)
System.out.println(this.girlName + "拿了口红");
Thread.sleep(1000);
}
synchronized (mirror){//一秒钟后想获得镜子的锁(此时放下了口红,想拿镜子用)
System.out.println(this.girlName + "拿了镜子");
}
}else {
synchronized (mirror){//获得镜子的锁(拿镜子来用)
System.out.println(this.girlName + "拿了镜子");
Thread.sleep(2000);
}
synchronized (lipstick){//一秒钟后想获得口红的锁(此时放下了镜子,想拿口红用)
System.out.println(this.girlName + "拿了口红");
}
}

运行结果:

1
2
3
4
5
6
灰姑娘拿了口红
白雪公主拿了镜子
灰姑娘拿了镜子
白雪公主拿了口红

Process finished with exit code 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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.study.多线程.demo3;

//死锁:多个线程相互抱着对方需要的资源,然后形成僵持
public class DeadLock {

public static void main(String[] args) {
Makeup girl01 = new Makeup(0,"灰姑娘");
Makeup girl02 = new Makeup(1,"白雪公主");

girl01.start();
girl02.start();
}
}
//口红
class Lipstick{

}

//镜子
class Mirror{

}

//化妆,调用口红、镜子等资源
class Makeup extends Thread{

//需要的资源只有一份,用static来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();

int choice;//选择
String girlName;//使用化妆品(资源)的人

Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {

//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}

}
//化妆,互相持有对方的锁,需要拿到对方的资源
private void makeup() throws InterruptedException {
// if (choice == 0){
// synchronized (lipstick){//获得口红的锁(拿口红来用)
// System.out.println(this.girlName + "拿了口红");
// Thread.sleep(1000);
// synchronized (mirror){//一秒钟后想获得镜子的锁(此时还没放下口红,想继续拿镜子用)
// System.out.println(this.girlName + "拿了镜子");
// }
// }
// }else {
// synchronized (mirror){//获得镜子的锁(拿镜子来用)
// System.out.println(this.girlName + "拿了镜子");
// Thread.sleep(2000);
// synchronized (lipstick){//一秒钟后想获得口红的锁(因为synchronized是嵌套在一起的,所以此时还没放下镜子,想继续拿口红用)
// System.out.println(this.girlName + "拿了口红");
// }
// }
// }

//改进,避免死锁
if (choice == 0){
synchronized (lipstick){//获得口红的锁(拿口红来用)
System.out.println(this.girlName + "拿了口红");
Thread.sleep(1000);
}
synchronized (mirror){//一秒钟后想获得镜子的锁(此时放下了口红,想拿镜子用)
System.out.println(this.girlName + "拿了镜子");
}
}else {
synchronized (mirror){//获得镜子的锁(拿镜子来用)
System.out.println(this.girlName + "拿了镜子");
Thread.sleep(2000);
}
synchronized (lipstick){//一秒钟后想获得口红的锁(此时放下了镜子,想拿口红用)
System.out.println(this.girlName + "拿了口红");
}
}


}
}

学习自B站遇见狂神说

若图片不能正常显示,请在浏览器中打开

欢迎关注我的其它发布渠道