线程同步

当多个线程都需要修改同一个资源时,就可能出现错误,不同步的状态。需要使用同步机制使它们正确工作。使用关键词synchronized来进行同步。

同步代码块的语法格式为:

synchronized(obj){

​ 同步代码块

obj叫做同步监视器,线程要执行同步代码块之前必须要获得对同步监视器的锁定。即可以理解为对obj上锁。任何时刻,只能有一个线程拥有obj的锁。

应该使用两个或多个线程共享的资源来作为同步监视器。

例子:

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
public class Main {

public static void main(String[] args) throws InterruptedException {

Accout acc = new Accout("123456w", 200);

Thread thread1 = new TakeMoney("甲", acc, 200);
Thread thread2 = new TakeMoney("乙", acc, 200);

thread1.start();
thread2.start();

}

}

class TakeMoney extends Thread {

private Accout accout;
private double money;

public TakeMoney(String name, Accout accout, double money) {
super(name);
this.accout = accout;
this.money = money;
}

@Override
public void run() {

//在这里加上同步代码块,一个线程取钱操作完成后,另一个线程才可以执行
synchronized (accout) {
if (accout.getBalance() >= money) {

System.out.println(this.getName() + "取钱成功,取出" + money);
accout.setBalance(accout.getBalance() - money);
System.out.println("当前余额为:" + accout.getBalance());
} else {
System.out.println("余额不足,取钱失败");
}
}

}
}

class Accout {
//省略...
}

与同步代码块相对应的,Java提供了同步方法,同步方法就是使用synchronized关键词修饰某个方法,对于synchronized修饰的实例方法,线程在执行该方法时,取得的是调用该方法的对象的锁,即相当于synchronized(this)。

使用同步方法可以方便实现线程安全的类,线程安全类具体以下特征:

  • 该类对象可以被多个线程安全访问
  • 每个线程调用该对象任意方法后都将得到正确的结果
  • 每个线程调用该对象的任意方法后,该对象状态保持合理状态

上个例子可以使用同步方法来做:

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
public class Main {

public static void main(String[] args) throws InterruptedException {

Accout acc = new Accout("123456w", 200);

Thread thread1 = new TakeMoney("甲", acc, 200);
Thread thread2 = new TakeMoney("乙", acc, 200);

thread1.start();
thread2.start();

}

}

class TakeMoney extends Thread {

private Accout accout;
private double money;

public TakeMoney(String name, Accout accout, double money) {
super(name);
this.accout = accout;
this.money = money;
}

@Override
public void run() {

accout.takeMoney(this, accout, money);

}
}

class Accout {

//省略...
//使用同步方法来解决
public synchronized void takeMoney(Thread thread, Accout accout, double money) {

if (accout.getBalance() >= money) {
System.out.println(thread.getName() + "取钱成功,取出" + money);
accout.setBalance(accout.getBalance() - money);
System.out.println("当前余额为:" + accout.getBalance());
} else {
System.out.println("余额不足,取钱失败");
}

}

}

释放同步监视器的锁定

任何线程进入同步代码块,同步方法之前,要获得同步监视器的锁定,然后线程会在几种情况释放该锁

  • 执行到方法、同步代码块结束,线程释放监视器
  • 在执行时遇到break、return终止该代码块
  • 在执行时遇到了未处理的Error或者Exception,导致异常结束
  • 当前线程执行时,程序执行了同步监视器对象的wait方法,当前线程暂停,并且释放监视器

同步锁

Java提供了一种更强大的线程同步机制------通过显式定义同步锁对象来实现同步。这种机制下,同步锁使用Lock对象充当。

Lock是控制多个线程对共享资源进行访问的工具,线程开始访问共享资源时,应先获得Lock对象。

在实现线程安全控制中,比较常用的是ReentrantLock(可重入锁),使用该对象可以显式的加锁、释放锁,通常使用该锁的代码格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class X {
private final ReentrantLock lock = new ReentrantLock();
//定义需要保证线程安全的方法

public void m() {
lock.lock();
try {
//需要保证线程安全的代码
} finally {
//即使遇到异常也能释放锁
lock.unlock();
}
}


}

将之前例子中需要线程安全的方法改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void takeMoney(Thread thread, Accout accout, double money) {
lock.lock();
try {
if (accout.getBalance() >= money) {
System.out.println(thread.getName() + "取钱成功,取出" + money);
accout.setBalance(accout.getBalance() - money);
System.out.println("当前余额为:" + accout.getBalance());
} else {
System.out.println("余额不足,取钱失败");
}
} finally {
lock.unlock();
}

}

同样可以实现线程安全

使用Lock对象和使用synchronized方法类似,只不过一个是显式使用Lock对象,另一个是隐式使用当前对象作为同步监视器。

ReentrantLock具有可重入性,意思是多个线程可以在已被加锁的ReentrantLock对象再次加锁,即可执行多次lock方法,ReentrantLock对象会维持一个计数器来追踪对lock对象的嵌套调用。线程在每次调用lock加锁后,必须调用unlock释放锁。

避免死锁的策略

当两个或多个线程在互相等待对方释放同步监视器时就会发生死锁。Java没有检测,也不会解决死锁。需要人为干预避免死锁出现。

可以使用几个方法来避免

  • 避免多次锁定,尽量避免同一个线程对多个同步监视器进行锁定
  • 具有相同加锁顺序:如果多个线程需要对多个同步监视器进行锁定,则应该保证它们以相同顺序请求加锁
  • 使用定时锁,程序调用Lock对象的tryLock方法可以指定time和unit参数,当超过指定时间就会自动释放对Lock的锁定
  • 死锁检测:依靠算法来实现的死锁预防机制

线程同步
https://zhaoquaner.github.io/2022/05/11/Java/多线程/线程同步/
更新于
2022年5月22日
许可协议