线程安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
线程安全问题出现的原因:
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
模拟实现:共同的账户,余额是 10 万元,模拟 2 人同时去取钱 10 万
- 需要提供一个账户类,创建一个账户对象代表 2 个人的共享账户。
- 需要定义一个线程类,线程类可以处理账户对象。
- 创建 2 个线程对象,传入同一个账户对象。
启动 2 个线程,去同一个账户对象中取钱 10 万。
@Data@AllArgsConstructor@NoArgsConstructorpublic class Account {private String cardId;private double money; // 账户的余额public void drawMoney(double money) {// 0、先获取是谁来取钱,线程的名字就是人名String name = Thread.currentThread().getName();// 1、判断账户是否够钱if (this.money >= money) {// 2、取钱System.out.println(name + "来取钱成功,吐出:" + money);// 3、更新余额this.money -= money;System.out.println(name + "取钱后剩余:" + this.money);} else {// 4、余额不足System.out.println(name + "来取钱,余额不足!");}}}
/**取钱的线程类*/public class DrawThread extends Thread {// 接收处理的账户对象private Account acc;public DrawThread(Account acc,String name){super(name);this.acc = acc;}@Overridepublic void run() {// 小明 小红:取钱acc.drawMoney(100000);}}
public static void main(String[] args) {// 1、定义线程类,创建一个共享的账户对象Account acc = new Account("ICBC-111", 100000);// 2、创建 2 个线程对象,代表小明和小红同时进来了。new DrawThread(acc, "小明").start();new DrawThread(acc, "小红").start();}
线程同步
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的核心思想:加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来方式一:同步代码块
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。public void drawMoney(double money) {// 0、先获取是谁来取钱,线程的名字就是人名String name = Thread.currentThread().getName();// 同步代码块synchronized ("halo") {// 1、判断账户是否够钱if (this.money >= money) {// 2、取钱System.out.println(name + "来取钱成功,吐出:" + money);// 3、更新余额this.money -= money;System.out.println(name + "取钱后剩余:" + this.money);} else {// 4、余额不足System.out.println(name + "来取钱,余额不足!");}}}
IDEA 快捷创建:选中代码块 Ctrl + Alt + T 后选择 9
锁对象用任意唯一的对象好不好呢?不好,会影响其他无关线程的执行。
锁对象的规范要求:
- 规范上:建议使用共享资源作为锁对象。
- 对于实例方法建议使用 this 作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
public void drawMoney(double money) {// 0、先获取是谁来取钱,线程的名字就是人名String name = Thread.currentThread().getName();// 同步代码块synchronized (this) {// 1、判断账户是否够钱if (this.money >= money) {// 2、取钱System.out.println(name + "来取钱成功,吐出:" + money);// 3、更新余额this.money -= money;System.out.println(name + "取钱后剩余:" + this.money);} else {// 4、余额不足System.out.println(name + "来取钱,余额不足!");}}}
public static void main(String[] args) {// 1、定义线程类,创建一个共享的账户对象Account acc = new Account("ICBC-111", 100000);// 2、创建 2 个线程对象,代表小明和小红同时进来了。new DrawThread(acc, "小明").start();new DrawThread(acc, "小红").start();// 1、定义线程类,创建一个共享的账户对象Account acc2 = new Account("ICBC-112", 100000);// 2、创建 2 个线程对象,代表小明和小红同时进来了。new DrawThread(acc2, "小明2").start();new DrawThread(acc2, "小红2").start();}
方式二:同步方法
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。public synchronized void drawMoney(double money) {// 0、先获取是谁来取钱,线程的名字就是人名String name = Thread.currentThread().getName();// 1、判断账户是否够钱if (this.money >= money) {// 2、取钱System.out.println(name + "来取钱成功,吐出:" + money);// 3、更新余额this.money -= money;System.out.println(name + "取钱后剩余:" + this.money);} else {// 4、余额不足System.out.println(name + "来取钱,余额不足!");}}
同步方法底层原理:
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用 this 作为的锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认用 类名.class 作为的锁对象。
方式三:Lock 锁
为了更清晰的表达如何加锁和释放锁,Java 1.5 以后提供了一个新的锁对象 Lock,更加灵活、方便。
Lock 实现提供比使用 synchronized 方法和语句可以获得更广泛的锁定操作。
Lock 是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来构建 Lock 锁对象。
public ReentrantLock():获得 Lock 锁的实现类对象@Data @AllArgsConstructor @NoArgsConstructor public class Account { private String cardId; private double money; // 账户的余额 // final修饰后:锁对象是唯一和不可替换的 private final Lock lock = new ReentrantLock(); public void drawMoney(double money) { // 0、先获取是谁来取钱,线程的名字就是人名 String name = Thread.currentThread().getName(); lock.lock(); // 上锁 try { if(this.money >= money){ // 钱够了 System.out.println(name+"来取钱,吐出:" + money); // 更新余额 this.money -= money; System.out.println(name+"取钱后,余额剩余:" + this.money); }else{ // 3、余额不足 System.out.println(name+"来取钱,余额不足!"); } } finally { lock.unlock(); // 解锁 } } }
