学习内容:
1、 线程的上下文切换
2、 线程的安全(同步)问题
3、 解决线程安全问题方法及代码演示
线程的上下文切换
前提:一个CPU的内核一个时间只能运行一个线程中的一个指令
线程并发:CPU内核会在多个线程间来回切换运行,切换速度非常快,达到同时运行的效果
切换的话就会有三个问题
线程切换回来后,如何从上次执行的指令后执行?
程序计数器(每个线程都有,用于记录上次执行的行数)
线程执行会随时切换,如何保证重要的指令能完全完成?
线程安全问题
CPU进行上下文切换的过程中,性能会降低。
1、ALU:是能实现多组算术运算和逻辑运算的组合逻辑电路,简称ALU。
2、CU:是Control Unit控制单元,是CPU的一部分,用于执行计算机指令或者Client Unit 监控系统的监控客户端单元的一个计算机学名词
线程的安全(同步)问题
解释:当cpu在线程里来回切换,可能会导致丢失一些操作使整个执行流程缺失步骤,从而会影响到最终的数据问题
那么出现线程安全问题就需要三个条件
- 多个线程
- 同一个时间
- 执行同一段指令或修改同一个变量
下面是一段银行转账案例
/**
* 银行转账的案例
*/
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关键字作用是给整个方法上锁当前线程调用方法后,方法上锁,其它线程无法执行,调用结束后,释放锁
/**
* @Authoe: smz
* @Description: 同步方法
* @Date: 10:58 2021-12-08
*/
public synchronized void trade(int from,int to,int num){
if (arr[from]<num){
throw new RuntimeException("余额不足了哦!");
}
//转出的减钱
arr[from]-=num;
System.out.println(from+"向"+to+"转出了"+num);
//转入的加钱
arr[to]+=num;
System.out.println(to+"接收了"+from+"的"+num);
}
同步代码块
任何对象都可以作为锁,对象不能是局部变量
/**
* @Authoe: smz
* @Description: 同步块
* @Date: 11:15 2021-12-08
*/
synchronized (this){
//转出的减钱
arr[from]-=num;
System.out.println(from+"向"+to+"转出了"+num);
//转入的加钱
arr[to]+=num;
System.out.println(to+"接收了"+from+"的"+num);
System.out.println(getTotal());
}
同步锁
在java.concurrent并发包中的
Lock接口
基本方法:
- lock() 上锁
- unlock() 释放锁
常见实现类
- ReentrantLock 重入锁
- WriteLock 写锁
- ReadLock 读锁
- ReadWriteLock 读写锁
使用方法:
- 定义同步锁对象(成员变量)
- 上锁
- 释放锁
private Lock lock = new ReentrantLock();
public void trade(int from,int to,int num){
lock.lock();
if (arr[from]<num){
throw new RuntimeException("余额不足了哦!");
}
try {
//转出的减钱
arr[from]-=num;
System.out.println(from+"向"+to+"转出了"+num);
//转入的加钱
arr[to]+=num;
System.out.println(to+"接收了"+from+"的"+num);
System.out.println(getTotal());
}finally {
lock.unlock();
}
}
三种锁对比:
- 粒度(锁的范围)
同步代码块/同步锁 < 同步方法 - 编程简便
同步方法 > 同步代码块 > 同步锁 - 性能
同步锁 > 同步代码块 > 同步方法 - 功能性/灵活性
同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法
锁对象:
- 非静态方法 --> this
- 静态方法 —> 当前类.class
synchronized的基本的原理
一旦代码被synchronized包含,JVM会启动监视器(monitor)对这段指令进行监控
线程执行该段代码时,monitor会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放
如果锁没有其它线程持有,当前线程就持有锁,执行代码