1.线程同步的必要性
- 同时运行的线程需要共享数据,需要考虑其他线程的状态和行为
- 示例,两个人同时在一个银行账户上取款
//银行账户类
public class Account {
//balance 为余额,即账户上现有金额
private int balance=500;
public int getBalance(){
return balance;
}
//取款的方法
public void withdraw(int amount){
//amount 为取款的金额
//要是余额足够的话,余额减去现取款的金额
balance=balance-amount;
}
}
//取款线程类
public class WithdrawThread implements Runnable{
//所有的取款线程对象,都从同一个账户中取款
private Account account=new Account();
public void run(){
//每个线程运行5次,即取款5次
for(int i=0;i<5;i++){
//运行取款方法
makeWithdraw(100);
if(account.getBalance()<0){
System.out.println("账户已透支。");
}
}
}
private void makeWithdraw(int amount){
//如果账户上的余额>=取款金额,就可以取款
if(account.getBalance()>=amount){
System.out.println(Thread.currentThread().getName()+"准备取款啦!");
try{
Thread.sleep(500);//500毫秒,即0.5秒后实现取款
}catch (InterruptedException e){}
account.withdraw(amount);
System.out.println(Thread.currentThread().getName()+"取款"+amount+"元,已完成。");
}else {
System.out.println("余额不足支付"+Thread.currentThread().getName()+"的取款。余额为:"+account.getBalance()+"元。");
}
}
}
//测试类
public class WithdrawTest {
public static void main(String[] args){
//创建两个线程
WithdrawThread wt= new WithdrawThread();
Thread t1=new Thread(wt);
Thread t2=new Thread(wt);
t1.setName("李雷");
t2.setName("韩梅梅");
//启动线程
t1.start();
t2.start();
}
}
运行结果:
韩梅梅准备取款啦!
李雷准备取款啦!
韩梅梅取款100元,已完成。
李雷取款100元,已完成。
李雷准备取款啦!
韩梅梅准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
韩梅梅取款100元,已完成。
韩梅梅准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
韩梅梅取款100元,已完成。
韩梅梅准备取款啦!
李雷取款100元,已完成。
账户已透支。
余额不足支付李雷的取款。余额为:-100元。
账户已透支。
韩梅梅取款100元,已完成。
账户已透支。
余额不足支付韩梅梅的取款。余额为:-100元。
账户已透支。
- 虽然在程序中对余额做了判断,但是还是出现了透支的情况。为了避免这种情况,就需要线程同步。
2.实现线程同步
- 在多个线程需要访问同一资源时,需要以某种顺序来确保该资源在某一时刻只能被一个线程使用的方式称为线程同步
- 采用线程同步来控制线程的执行有两种方式,都使用 synchronized 关键字实现
- 同步方法
- 同步代码块
(1)同步方法
- 在方法声明中加入 synchronized 关键字来声明同步方法
- 控制对类成员变量的访问
- 每个类实例对应一把锁,方法一旦执行,就独占该锁,知道该方法返回时才将锁释放
- 此后被阻塞的线程方能获得该锁,重新进入可执行状态
- 这种机制确保了同一时刻对应每一个实例
- 其所有声明为 synchronized 的方法只能有一个处于可执行状态,有效避免类成员变量的访问冲突
- 示例,使用同步方法的方式解决两人同在一个账户上取款时,账户透支的情况
//在取款的方法前加上 synchronized 关键字
private synchronized void makeWithdraw(int amount){}
//其余代码省略
运行结果:
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
- 当李雷的线程在使用 makeWithdraw() 取款的方法时,该线程就得到了当前对象的锁
- 当该方法执行完毕后 ,才会释放锁。
- 在释放锁之前,别的线程是无法同时执行此对象的 makeWithdraw() 取款的方法。
- 同步方法的缺陷:如果将一个运行时间比较长的方法声明成 synchronized 将会影响效率
(2)同步代码块
- synchronized 块中的代码必须获得对象 syncObject 的锁才能执行
- 示例,使用同步代码块的方式解决两人同在一个账户上取款时,账户透支的情况
private void makeWithdraw(int amount){
//同步查询和取款的代码块,需要获得银行账户类对象的锁才能执行
synchronized (account){
//如果账户上的余额>=取款金额,就可以取款
if(account.getBalance()>=amount){
System.out.println(Thread.currentThread().getName()+"准备取款啦!");
try{
Thread.sleep(500);//500毫秒,即0.5秒后实现取款
}catch (InterruptedException e){}
account.withdraw(amount);
System.out.println(Thread.currentThread().getName()+"取款"+amount+"元,已完成。");
}else {
System.out.println("余额不足支付"+Thread.currentThread().getName()+"的取款。余额为:"+account.getBalance()+"元。");
}
}
}
运行结果:
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
李雷准备取款啦!
李雷取款100元,已完成。
韩梅梅准备取款啦!
韩梅梅取款100元,已完成。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付韩梅梅的取款。余额为:0元。
余额不足支付李雷的取款。余额为:0元。
(3)死锁
- 多线程在使用同步机制时,会存在“死锁"的潜在危险
- 如果多个线程都处于等待状态而无法唤醒时,就构成了死锁 Deadlock
- 此时处于等待状态的多个线程占用系统资源,但无法运行,因此不会释放自身的资源
- 避免死锁的有效方法
- 线程因某个条件未满足而受阻,不能让其继续占有资源
- 如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁