目录
一、线程的上下文切换
在上一节我们讲到,当线程处于运行状态时,被其他线程得到CPU资源后,会回到准备状态,那么当这条线程再次处于运行状态时,是如何知道自己原先是处在哪一行呢?
原因就是程序计数器,程序计数器是每一个线程都会拥有的一个东西,它的作用就是记录上次代码执行的行数。
二、线程的安全问题
上面提到一条线程可能被另一条线程抢占CPU二回到准备阶段,那么当这条线程作为一个事务如何保证能够将这条线程的关键代码全部执行完毕,而不影响后面的结果呢?
当然,单线程是不会出现这种线程安全问题。
因此出现这种安全问题的前提条件有:
- 多线程
- 处于同一个时间
- 在同一个时间调用并修改同一个变量
下面我举一个简单的银行转账例子。大家都知道,银行转账必须一个的钱要扣除,另一个人的钱要上涨,是不可分离的,当中间环节出错了,那整个环节就会出错。比如说:
import java.util.Random;
public class ThreadDemo {
private int[] account = new int[10];
{
for (int i = 0; i < account.length; i++) {
account[i] = 10000;
}
}
//转账
public void transform(int from, int to, int money){
if (account[from] < money){
throw new RuntimeException("余额不足");
}
account[from] -= money;
System.out.println(from + "给"+ to + "转账" + money);
account[to] += money;
System.out.println(to + "收到"+ from + "转账" + money);
System.out.println("总账有:"+ getTotal());
}
//总账
public int getTotal(){
int sum = 0;
for (int i = 0; i < account.length; i++) {
sum += account[i];
}
return sum;
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Random random = new Random();
for (int i = 0; i < 5; i++) {
new Thread(()->{
int from = random.nextInt(10);
int to = random.nextInt(10);
int money = random.nextInt(2000);
threadDemo.transform(from,to,money);
}).start();
}
}
}
出现了这种情况
正常来讲总账应该是100000不变,为什么会出现小于这个数字的情况出现。
我自己理解到的原因是:
当线程进行到account[from] -= money;这一行时,此时该线程CPU被其他线程占用在其他线程完成了全部的代码,得到了总账的数据是错误的,之后回到原来的线程再次执行后来的代码回到了正轨。
作为一个事务来说,这是不行的,因为作为整体需要将他们一起全部执行,因此需要一个方法将它们锁住,这就是线程锁。
三、线程安全问题的解决方法
给程序上锁,让当前线程完整执行一段指令,执行完释放锁,其它线程再执行。
几种不同的上锁方法:
- 添加synchronized关键字
- 同步锁
1、在方法上添加synchronized关键字
作用是给整个方法上锁
//转账
public synchronized void transform(int from, int to, int money){
if (account[from] < money){
throw new RuntimeException("余额不足");
}
account[from] -= money;
System.out.println(from + "给"+ to + "转账" + money);
account[to] += money;
System.out.println(to + "收到"+ from + "转账" + money);
System.out.println("总账有:"+ getTotal());
}
2、在代码块前添加synchronized关键字
synchronized(锁对象){
代码
}
//转账
public void transform(int from, int to, int money){
if (account[from] < money){
throw new RuntimeException("余额不足");
}
synchronized (lock){
account[from] -= money;
System.out.println(from + "给"+ to + "转账" + money);
account[to] += money;
System.out.println(to + "收到"+ from + "转账" + money);
System.out.println("总账有:"+ getTotal());
}
}
锁对象,可以对当前线程进行控制,如:wait等待、notify通知;
任何对象都可以作为锁,但是对象不能是局部变量
3、同步锁
在java.concurrent并发包中的 Lock接口
基本方法:
-
lock() 上锁
-
unlock() 释放锁
//成员变量 Lock lock = new ReentrantLock(); //转账 public void transform(int from, int to, int money){ if (account[from] < money){ throw new RuntimeException("余额不足"); } lock.lock(); try { account[from] -= money; System.out.println(from + "给"+ to + "转账" + money); account[to] += money; System.out.println(to + "收到"+ from + "转账" + money); System.out.println("总账有:"+ getTotal()); }finally { lock.unlock(); } }
由于这两种必须绑定在一起,因此使用finally将这条线程解锁,否则会导致死锁。
4、区别
-
在编程上:synchronized关键字比Lock锁编程简单
-
性能方面:Lock锁的性能更为优秀
-
作用范围方面:synchronized关键字可以作用在方法上,也可以作用于代码块;而Lock锁只能作用于代码块