线程安全与线程同步
1.什么是线程安全问题?
多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题
取钱的线程安全问题场景:
两个人他们有一个共同的账户,余额是10万元,如果两个人同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题?
(1)线程安全问题出现的原因:
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
2.线程同步
线程同步就是解决线程安全问题的方案
(1)线程同步的思想:让多个线程实现先后依次访问共享资源,这样就解决了安全问题
(2)线程同步的常见方案
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
(3)线程同步方式
-
方式一:同步代码块
①作用:把访问共享资源的核心代码给上锁,以此保证线程的安全
②原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
③同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
④锁对象不能随便选择一个唯一的对象,会影响其他无关线程的执行,例如String字符串
⑤锁对象的使用规范
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
public class Demo {
public static void main(String[] args) {
//3、创建账户对象(共享数据)
Account acc = new Account("9527", 100000);
//4、两个线程同时取款(多个线程操作共享数据,出现线程安全问题)
new MyThread(acc, "小明").start();
new MyThread(acc, "小红").start();
}
}
//2、线程类,封装取款的代码
class MyThread extends Thread {
private Account acc;
public MyThread(Account acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
//取钱时,要传递取款金额
acc.drawMoney(100000);
}
}
//1、账户类,包含取款功能
class Account {
//卡号
private String cardID;
//余额
private double money;
public Account() {
}
public Account(String cardID, double money) {
this.cardID = cardID;
this.money = money;
}
public String getCardID() {
return cardID;
}
public void setCardID(String cardID) {
this.cardID = cardID;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
//取款功能,参数money为取款金额
public void drawMoney(double money) {
//获取当前线程对象的名称
String name = Thread.currentThread().getName();
synchronized (this) {
if (this.money >= money) {
System.out.println(name + "取钱:" + money + "成功");
this.money -= money;
System.out.println(name + "取钱后余额为:" + this.money);
} else {
System.out.println(name + "取钱失败,余额不足");
}
}
}
//对于静态方法建议使用字节码(类名.class)对象作为锁对象
public static void test(){
synchronized (Account.class){
}
}
}
-
方式二:同步方法
①作用:把访问共享资源的核心方法上锁,以此保证线程安全
②原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
③同步方法的底层原理
- 同步方法其实底层也是由隐式锁对象的,只是锁的范围是整个方法代码
- 如果方法是实例方法:同步方法默认this作为锁的对象
- 如果方法是静态方法:同步方法默认用类名.class作为锁对象
//线程类和测试类同上
//1、账户类,包含取款功能
class Account {
//卡号
private String cardID;
//余额
private double money;
public Account() {
}
public Account(String cardID, double money) {
this.cardID = cardID;
this.money = money;
}
public String getCardID() {
return cardID;
}
public void setCardID(String cardID) {
this.cardID = cardID;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
//取款功能,参数money为取款金额
public synchronized void drawMoney(double money) {
String name = Thread.currentThread().getName();
if (this.money >= money) {
System.out.println(name + "取钱:" + money + "成功");
this.money -= money;
System.out.println(name + "取钱后余额为:" + this.money);
} else {
System.out.println(name + "取钱失败,余额不足");
}
}
//静态方法
public synchronized static void test(){
}
}
④同步代码块和同步方法的区别
同步代码块:锁对象可以指定,锁的范围可以指定,性能高且灵活
同步方法:锁对象不能指定,锁的是方法体,阅读性更高
-
方式三:Lock锁
①Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
②Lock是接口,不能直接被实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象
public ReentrantLock():获得Lock锁的实现类对象
③Lock的常用方法
- void lock():获得锁
- void unlock():释放锁
④Lock锁使用规范
- 锁对象创建在成员位置,使用final修饰
- 释放锁的代码写在finally块中
public class Demo { public static void main(String[] args) { //3、创建账户对象(共享数据) Account acc = new Account("9527", 100000); //4、两个线程同时取款(多个线程操作共享数据,出现线程安全问题) new MyThread(acc, "小明").start(); new MyThread(acc, "小红").start(); } } //2、线程类,封装取款的代码 class MyThread extends Thread { private Account acc; public MyThread(Account acc, String name) { super(name); this.acc = acc; } @Override public void run() { //取钱时,要传递取款金额 acc.drawMoney(100000); } } //1、账户类,包含取款功能 class Account { //卡号 private String cardID; //余额 private double money; //规范1、锁对象创建在成员位置,使用final修饰 private final ReentrantLock lock = new ReentrantLock(); public Account() { } public Account(String cardID, double money) { this.cardID = cardID; this.money = money; } public String getCardID() { return cardID; } public void setCardID(String cardID) { this.cardID = cardID; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } //取款功能,参数money为取款金额 public void drawMoney(double money) { String name = Thread.currentThread().getName(); try { //上锁 lock.lock(); if (this.money >= money) { System.out.println(name + "取钱:" + money + "成功"); this.money -= money; System.out.println(name + "取钱后余额为:" + this.money); } else { System.out.println(name + "取钱失败,余额不足"); } } catch (Exception e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } } }