前言
前面我们学习了什么是线程,今天我们学习一下线程的安全问题
提示:以下是本篇文章正文内容,下面案例可供参考
一、线程的上下文切换
线程的上下文切换有一个前提条件:一个CPU的内核一个时间只能运行一个线程中的一个指令。
我们都知道CPU内核会在多个线程中来回切换来达到同时运行的效果,所以多线程创建到
目录
CPU在多个线程中来回切换,可能导致某些重要的指令不能完整执行
切换到另一个线程的过程叫做上下文切换。
既然是在多个线程中来回切换的就会出现几个问题:
1.线程切换回来后,如何在上次执行的指令后执行?
这是通过程序计数器来实现的,java虚拟机中有一个程序计数器,每个线程都有他私有的程序计数器,生命周期与线程的生命周期保持一致。
2.线程执行随时都会切换,如何保证重要的指令能全部完成?
这就是我下面要讲到的线程安全问题了
二、线程安全(同步)问题
CPU在多个线程中来回切换,可能导致某些重要的指令不能完整执行
出现线程安全的问题需要满足一些个条件,即多个线程在同一时间执行同一段指令或修改同一个变量。
举个例子:银行向100个账户转账,我们来看看银行的总账
import java.util.Random;
/**
* 银行转账的案例
*/
public class BankDemo {
//模拟100个银行账户
private int[] accounts = new int[100];
{
//初始化账户
for (int i = 0; i < accounts.length; i++) {
accounts[i] = 10000;
}
}
/**
* 模拟转账
*/
public void transfer(int from,int to,int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足");
}
accounts[from] -= money;
System.out.printf("从%d转出%d%n",from,money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
/**
* 计算总余额
* @return
*/
public int getTotal(){
int sum = 0;
for (int i = 0; i < accounts.length; i++) {
sum += accounts[i];
}
return sum;
}
public static void main(String[] args) {
BankDemo bank = new BankDemo();
Random random = new Random();
//模拟多次转账过程
for (int i = 0; i < 50; i++) {
new Thread(() -> {
int from = random.nextInt(100);
int to = random.nextInt(100);
int money = random.nextInt(2000);
bank.transfer(from,to,money);
}).start();
}
}
}
我们可以看到每次银行的总账都是不一样的,这就是线程不安全所导致的。
三、线程安全问题的解决方法
要解决上面的问题就是给程序上锁,让当前线程完整执行一段指令,执行完后释放锁,再让其他线程运行
给程序上锁有几种方式
(一)同步方法
同步方法就是在方法上添加synchronized关键字,作用是给整个方法上锁,当前线程调用方法后,方法上有锁,其他线程就无法执行,等此方法执行完释放锁后再执行。
咱们给上面的方法上个锁
/**
* 模拟转账
*/
public synchronized void transfer(int from,int to,int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足");
}
accounts[from] -= money;
System.out.printf("从%d转出%d%n",from,money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
我们可以看到上锁后每次银行的总账都没有出现问题了。
(二)同步代码块
使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块。多个同步代码块如果使用相同的锁对象,那么他们就是同步的。如果是非静态方法就用this,如果是静态方法就用当前类.class。
synchronized(锁对象){
代码
}
锁对象可以对当前线程进行控制如(wait 等待、notify 通知等),任何对象都可以作为锁但对象不能是局部变量。
咱们再用同步代码块的方法给上面例子加个锁:
//同步代码块
synchronized (lock) {
accounts[from] -= money;
System.out.printf("从%d转出%d%n", from, money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
(三)同步锁
同步锁的使用就是定义一个同步锁对象(成员变量)然后在方法内部上锁,最后释放锁
//成员变量
Lock lock = new ReentrantLock();
//方法内部上锁
lock.lock();
try{
代码...
}finally{
//释放锁
lock.unlock();
}
同样拿上面的案例来试试
Lock lock = new ReentrantLock();
private void transfer(int from,int to,int money) {
lock.lock();
try {
if (accounts[from] < money) {
throw new RuntimeException("余额不足");
}
accounts[from] -= money;
System.out.printf("从%d转出%d%n", from, money);
accounts[to] += money;
System.out.printf("向%d转入%d%n", to, money);
System.out.println("银行总账是:" + getTotal());
}finally {
lock.unlock();
}
}
总结
三种锁的对比:
粒度:同步代码块/同步锁 < 同步方法
编程简便:同步方法 > 同步代码块 > 同步锁
性能:同步锁 > 同步代码块 > 同步方法
功能性/灵活性:同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法
加锁牢记两点:1.锁的对象2.锁的范围