目录
一、线程的上下文切换
CPU会给每个线程分配时间片,当前线程执行完后会切换到下一个线程,在切换前会保存上一个线程的状态,以便下次切换回这个线程时,可以再次加载这个线程的状态,从线程保存到再加载的过程就是一次上下文切换。
二、什么情况会出现线程安全问题
1.什么是线程安全问题
在单线程中是不会出现线程安全问题的,但是在多线程中,CPU会在多个线程中间进行切换,可能导致某些重要的指令不能完整执行或不同线程访问相同资源导致最后的结果与实际上的愿望相违背或者直接导致程序出错,出现数据的问题。
2.出现线程安全的三个条件
- 多个线程
- 同一时间执行
- 执行同一段指令或修改同一个变量
我们来看下面这个例子:
public class Test {
// 模拟十个账户
int[] accounts = new int[10];
{
// 给每个账户加10000
for (int i = 0; i < accounts.length; i++) {
accounts[i] = 10000;
}
}
public static void main(String[] args) {
Random random = new Random();
Test test = new Test();
// 进行十次模拟转账
for (int i = 0; i < 10; i++) {
new Thread(() -> {
int from = random.nextInt(10);
int to = random.nextInt(10);
int money = random.nextInt(1000);
test.transfer(from,to,money);
}).start();
}
}
// 模拟转账
public void transfer(int from, int to, int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足!");
}
accounts[from] -= money;
System.out.printf("从%d转钱%d到%d,",from,money,to);
accounts[to] += money;
System.out.println("银行总余额是:" + totalBalance());
}
// 计算总余额
public int totalBalance(){
int sum = 0;
for (int i = 0; i < accounts.length; i++) {
sum += accounts[i];
}
return sum;
}
}
代码的运行效果:
出现上面情况的原因是因为当前线程执行转账方法时抢占的时间片用完,所以会切换到下一个抢占到时间片的线程,上一次线程的accounts[to] += money还没有执行就切换到了下一个线程,所以会出现上面的情况。
三、线程安全问题的解决方法
1.同步方法
实现方式:给方法添加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到%d,",from,money,to);
accounts[to] += money;
System.out.println("银行总余额是:" + totalBalance());
}
添加synchronized关键字后的运行效果:
2.同步代码块
实现方式:synchronized(锁对象){代码},给一段代码上锁。
锁对象,可以对当前线程进行控制,如:wait等待、notify通知;任何对象都可以作为锁,对象不能是局部变量。
// 锁对象
Object lock = new Object();
// 模拟转账
public void transfer(int from, int to, int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足!");
}
// 同步代码块
synchronized (lock){
accounts[from] -= money;
System.out.printf("从%d转钱%d到%d,",from,money,to);
accounts[to] += money;
System.out.println("银行总余额是:" + totalBalance());
}
}
3.同步锁
实现方式:
-
定义同步锁对象(成员变量)
-
上锁(lock())
-
释放锁(unlock())
// 锁对象
Lock lock = new ReentrantLock();
// 模拟转账
public void transfer(int from, int to, int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足!");
}
// 上锁
lock.lock();
try {
accounts[from] -= money;
System.out.printf("从%d转钱%d到%d,",from,money,to);
accounts[to] += money;
System.out.println("银行总余额是:" + totalBalance());
}finally {
// 释放锁
lock.unlock();
}
}
synchronized关键字与Lock锁方式的区别:
-
两者都可以解决线程安全问题
-
synchronized关键字既可以修饰代码块又可以修饰方法;而Lock锁方式只可以修饰代码块
-
synchronied关键字修饰的代码块或方法在运行结束后,会自动释放锁;而Lock锁方式需手动为代码块加锁并释放锁
-
从性能上,Lock锁方式优于synchronized关键字