前面我们学习了什么是线程,今天我们学习一下线程的安全问题
提示:以下是本篇文章正文内容,下面案例可供参考
线程的上下文切换有一个前提条件:一个CPU的内核一个时间只能运行一个线程中的一个指令。
我们都知道CPU内核会在多个线程中来回切换来达到同时运行的效果,所以多线程创建到
目录
前言
一、线程的上下文切换
1.线程切换回来后,如何在上次执行的指令后执行?
2.线程执行随时都会切换,如何保证重要的指令能全部完成?
二、线程安全(同步)问题
CPU在多个线程中来回切换,可能导致某些重要的指令不能完整执行
三、线程安全问题的解决方法
总结
切换到另一个线程的过程叫做上下文切换。
既然是在多个线程中来回切换的就会出现几个问题:
这是通过程序计数器来实现的,java虚拟机中有一个程序计数器,每个线程都有他私有的程序计数器,生命周期与线程的生命周期保持一致。
这就是我下面要讲到的线程安全问题了
出现线程安全的问题需要满足一些个条件,即多个线程在同一时间执行同一段指令或修改同一个变量。
举个例子:银行向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.锁的范围