定义
指在同一时刻只能有一个线程去访问共享资源。 简单来说,即采用排他式的操作保证同一时刻只能有一个线程访问共享资源。 一般通过 synchronized 关键字实现。
案例
老生常谈的银行取钱案例 银行
public class Bank {
private int totalMoney;
public Bank(int totalMoney) {
this.totalMoney = totalMoney;
}
public void withdraw(int money, String name) {
if (totalMoney >= money) {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
totalMoney = totalMoney - money;
System.out.println(name + "已取款" + money + ",当前余额:" + totalMoney);
} else {
throw new RuntimeException(name + "尝试取钱" + money + "余额不足,当前余额为:" + totalMoney);
}
}
}
取款线程
public class OperateThread extends Thread {
private Bank bank;
private final int withDrawMoney;
private String name;
public OperateThread(Bank bank, int withDrawMoney, String name) {
this.bank = bank;
this.withDrawMoney = withDrawMoney;
this.name = name;
}
@Override
public void run() {
bank.withdraw(withDrawMoney, name);
}
}
测试类
public class Test {
public static void main(String[] args) {
Bank bank = new Bank(600);
new OperateThread(bank, 300, "张三").start();
new OperateThread(bank, 400, "李四").start();
}
}
张三和李四两人同时取款,结果是余额里面出现了负数。
- 不合理之处自然是因为两个操作线程共用一个bank类,两个线程均存在对共享变量的读取与修改。
- 由于CPU时间片的切换,可能线程执行到某一阶段时,共享变量就被别的线程修改了。
Single Thread Execution提出的理念正是基于此,既然多个线程访问共享变量,会出现安全问题。那么在进行共享变量的操作时,任意时刻只允许一个线程进行,其他线程等待该线程操作完毕后才能操作,便能解决这个问题。
代码层面上可以给withdraw方法添加synchronized关键字,或者使用synchronized(this)代码段等等。
特点
Single Thread Execution 的着重点在于排他性,独占性。 采用这种方式会对程序性能产生影响
- 线程的加锁(获取锁)与解锁(释放锁)会占据一部分的时间耗费;
- 加锁部分由于只能一个线程执行,其他线程的等待自然也会降低性能;
死锁
在Single Thread Execution 的思想下需要谨慎考虑死锁出现的可能性。即可能出现线程1持有A锁并同时等待B锁释放,而线程2持有B锁却同时等待A锁释放。在这种情况下双方互相持有对方需要的锁,又等待对方释放锁,最终线程阻塞,出现死锁。
在这些情况下,我们需要考虑死锁出现的可能性:
- 线程的执行过程中出现多把锁
- 在持有A锁的时候,尚未释放便去获取B锁
- 线程在执行过程中获取多个锁的顺序不同
破坏上面任意一点即可避免死锁。 通常情况下出现多个锁的原因是因为存在多个共享变量,而在编写程序时采用synchronized(共享变量){}的方式,最终便形成了多个锁。所以可能考虑将多个共享变量合为一个,即采用一个类来包括多个共享变量。将多个锁简化为一个锁。
另外如果确实需要多个锁,那么则尽量避免锁嵌套。多个锁之间独立使用,那么也能避免死锁。
继承异常
当将某个类设计为线程安全的类后,如果通过子类继承该类,然后通过子类来修改共享变量,并且没有安全措施来保证子类执行的安全性,那么便会破坏类的线程安全性。这种情况称之为继承异常。 所以在多线程中的继承时需要考虑子类可能会出现的线程安全问题。
|