# 线程同步
[TOC]
## 多线程运行问题
在之前的学习中,我们发现在线程的运行过程中,线程什么时候运行是不确定的。
**多线程运行问题总结:**
* 各个线程是通过竞争cpu时间来获取运行机会的;
* 各线程什么时候得到cpu时间占用多久,是不可预测的;
* 一个正在运行着的线程在什么时候被暂停是不确定的。
多线程的这些问题,在具体的开发中也会带来不可预测的结果。
**场景:** 公司有一个公共的银行账号,在这个账号中经常进行存取款操作。由于存取款操作可能同时进行,就有可能发生一定的风险。使用以下代码模拟这样一个过程
~~~java
public class Bank {
private String account;//账户
private int balance;//余额
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public Bank(String account, int balance) {
super();
this.account = account;
this.balance = balance;
}
public Bank() {
super();
}
@Override
public String toString() {
return "Bank [账户=" + account + ", 余额=" + balance + "]";
}
/**
* 存款方法
*/
public void saveAccount() {
//可以在不同的地方添加sleep()方法
//获取当前账户余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改存款
balance += 100;
//修改账户余额
setBalance(balance);
//输出修改后的账户余额
System.out.println("输出存款后的账户余额" + balance);
}
/**
* 取款方法
*/
public void drawAccount() {
//可以在不同的地方添加sleep()方法
//获取当前的账户余额
int balance = getBalance();
//修改余额
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
setBalance(balance);
//输出
System.out.println("取款后的账户余额" + balance);
}
}
~~~
~~~java
/**
* 存款
* @author LiXinRong
*
*/
public class SaveAccount implements Runnable {
Bank bank;
public SaveAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.saveAccount();
}
}
~~~
~~~java
/**
* 取款
* @author LiXinRong
*
*/
public class DrawAccount implements Runnable{
Bank bank;
public DrawAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.drawAccount();
}
}
~~~
~~~java
public class SaveAndDrawDemo {
public static void main(String[] args) {
//创建账户,给定账户余额
Bank bank = new Bank("1001",1000);
//创建线程对象,并启动线程
SaveAccount sa = new SaveAccount(bank);
DrawAccount da = new DrawAccount(bank);
Thread save = new Thread(sa);
Thread draw = new Thread(da);
save.start();
draw.start();
try {
//这里设置join方法,是为了先执行存取款线程,最后执行代码末尾的打印银行语句
save.join();
draw.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bank);
}
}
~~~
当没有对Bank类中的进行数据及时更新时,就有可能会造成运行的错误。尤其是我们今后在设计有关钱的代码时,需要慎之又慎。
## 线程同步(线程互斥)处理
造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体的锁定,即当判断数据和修改数据时,只允许一个线程进行处理,而其他线程需要等待当前线程执行完毕后才可以继续执行,这样就使得同一个时间段内,只允许一个线程执行操作,从而实现同步的处理。
![](https://img.kancloud.cn/c5/2f/c52f298f3d0997d1b438d068376450b6_1002x237.png)
>[success]就相当于,我们给CPU加了一把锁
**使用synchronized关键字,可以实现同步处理。**
同步的关键是要为代码加上“锁”,对于锁的操作程序有两种:同步代码块,同步方法(成员方法、静态方法);
**同步方法与同步代码块**
~~~java
/**
* 存款方法
*/
public synchronized void saveAccount() {
//可以在不同的地方添加sleep()方法
//获取当前账户余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改存款
balance += 100;
//修改账户余额
setBalance(balance);
//输出修改后的账户余额
System.out.println("输出存款后的账户余额" + balance);
}
/**
* 取款方法
*/
public void drawAccount() {
synchronized(this) {
//可以在不同的地方添加sleep()方法
//获取当前的账户余额
int balance = getBalance();
//修改余额
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
setBalance(balance);
//输出
System.out.println("取款后的账户余额" + balance);
}
}
~~~
当使用同步代码块的时候,当多个线程并发执行时,只允许一个线程执行此部分内容,从而实现同步处理操作。
同步代码块可以直接定义在某个方法中,使得方法的部分操作进行同步处理,但是如果某个方法中的全部操作都需要进行同步处理,则可以采用同步方法的形式进行定义。
>[warning]同步会造成处理性能下降
同步操作的本质在于同一个时间段内只允许有一个线程运行,所以在此线程对象未执行完的过程中其他线程对象将处于等待状态,这样就会造成程序处理性能下降。但是同步也带来一些优点:数据的线程访问安全。
## 线程死锁
死锁是在多线程开发中较为常见的一种不确定出现的问题,其所带来的影响计时导致程序出现“假死”状态。
同步保证了一个线程要等待另外一个线程执行完毕才会继续执行,虽然在一个程序中,使用同步可以办证资源共享操作的正确性,但是过多的同步也会产生问题。
**例如:**
>[info]现在张三想要李四的画,李四想要张三的书,那么张三对李四说:“把你的画给我,我就给你书”。李四也对张三说:“把你的书给我,我就给你画”。此时,张三在等李四的回答,李四在等张三的回答。最终的结果,就是张三得不到李四的画,李四得不到张三的书。
所谓死锁,是指两个线程都在等待对方先完成,造成程序的停滞状态,一般程序的死锁都是在程序运行时出现的。
在开发过程中回避线程的死锁问题,是设计的难点。