1.线程安全
线程安全问题
多个线程同时操作同一个共享资源且存在修改该资源的时候可能会出现业务安全问题,称为线程安全问题。
线程安全问题出现的原因
①存在多线程并发。
②同时访问共享资源。
③存在修改共享资源。
线程安全问题案例模拟-取钱业务
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类,线程类可以处理账户对象。
③:创建2个线程对象,传入同一个账户对象。
④:启动2个线程,去同一个账户对象中取钱10万。
示例代码如下:
Account账户类
publicclassAccount { privateStringcardId; doublemoney; // 账户余额publicAccount() { } publicAccount(StringcardId, doublemoney) { this.cardId=cardId; this.money=money; } publicvoiddrawMoney(doubledrawMoney) throwsInterruptedException { // 获取取钱人姓名StringuserName=Thread.currentThread().getName(); // 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款"); // 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元"); // 更新余额this.money-=drawMoney; System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元"); } else { // 余额不足System.out.println(userName+"取款余额不足!"); } } publicStringgetCardId() { returncardId; } publicvoidsetCardId(StringcardId) { this.cardId=cardId; } publicdoublegetMoney() { returnmoney; } publicvoidsetMoney(doublemoney) { this.money=money; } }
取钱线程类
publicclassDrawThreadextendsThread { // 接收处理的账户对象privateAccountacc; publicDrawThread() { } publicDrawThread(Accountacc, Stringname) { super(name); this.acc=acc; } publicvoidrun() { // 调用取钱方法try { acc.drawMoney(100000); } catch (InterruptedExceptione) { e.printStackTrace(); } } }
测试类
publicclassThreadDemo { publicstaticvoidmain(String[] args) { // 1.定义线程类,创建一个共享的账户对象Accountacc=newAccount("icbc-001", 100000); // 2.创建2个线程对象,代表小明和小红同时登陆账户取钱newDrawThread(acc, "小明").start(); newDrawThread(acc, "小红").start(); } }
程序运行结果如下:
小红取款余额100000.0余额足够,开始取款
小明取款余额100000.0余额足够,开始取款
小明成功取出100000.0元
小明取钱后,账户余额剩余0.0元
小红成功取出100000.0元
小红取钱后,账户余额剩余-100000.0元
分析:小红取款线程访问账户资源,判断余额,余额足够,开始取钱(线程休眠代表取钱过程),在此期间,小明取款线程访问账户资源,判断余额,余额足够,开始取钱,后两者均取钱成功,更新账户余额。
小红线程取钱过程中,在更新账户余额之前,小明线程执行判断余额任务,在小红线程取款完毕后,小明线程已经执行完判断余额任务,不再重复判断,同样开始取款,出现线程安全问题。
2.线程同步
2.1线程同步思想概述
取钱案例出现问题的原因?
多个线程同时执行,发现账户都是够钱的。
如何才能保证线程安全呢?
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的核心思想
加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
2.2方式一:同步代码块
同步代码块
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步代码块代码格式:
synchronized(同步锁对象) {
操作共享资源的代码(核心代码)
}
锁对象要求
理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。
例如,对于某一确定字符串,在堆内存中具有唯一地址,同步锁对象若是某一字符串,则针对所有的线程来讲,锁对象均是唯一的,则表明所有的线程对象均需要遵守唯一进入,任务执行结束后解锁的原则。同步锁对象若是某一对象,因不同对象的地址不同,因此当且仅当对于不同的线程来说,同步锁对象是同一对象时,才会上锁,若对于不同的线程对象,同步锁对象是不同的(堆内存中地址不同),那么这些线程对象不受同步锁的影响。
因此锁对象用任意唯一的对象并不好,会影响其他无关线程的执行。
锁对象的规范要求
规范上:建议使用共享资源作为锁对象。
对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象(与用字符串效果一致,类名.class更规范)。
示例代码如下:
更改第3节Account账户类中的drawMoney方法代码。
publicvoiddrawMoney(doubledrawMoney) throwsInterruptedException { // 获取取钱人姓名StringuserName=Thread.currentThread().getName(); // 同步代码块synchronized (this) { // 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款"); // 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元"); // 更新余额this.money-=drawMoney; System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元"); } else { // 余额不足System.out.println(userName+"取款余额不足!"); } } }
程序运行结果如下:
小明取款余额100000.0余额足够,开始取款
小明成功取出100000.0元
小明取钱后,账户余额剩余0.0元
小红取款余额不足!
2.3方式二:同步方法
同步方法
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法格式:
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
同步方法底层原理
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法,同步方法默认用this作为的锁对象,但是代码要求高度面向对象。
如果方法是静态方法,同步方法默认用类名.class作为的锁对象。
示例代码如下:
更改第3节Account账户类中的drawMoney方法代码。
publicsynchronizedvoiddrawMoney(doubledrawMoney) throwsInterruptedException { // synchronized代表同步方法// 获取取钱人姓名StringuserName=Thread.currentThread().getName(); // 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款"); // 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元"); // 更新余额this.money-=drawMoney; System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元"); } else { // 余额不足System.out.println(userName+"取款余额不足!"); } }
2.4方式三:Lock锁
Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
ReentrantLock构造器
方法名 |
说明 |
public ReentrantLock() |
获得Lock锁的实现类对象 |
Lock类常用API
方法名 |
说明 |
void lock() |
获得锁 |
void unlock() |
释放锁 |
Lock使用格式:
public void m() {
lock.lock();
try {
// ...method body
} finally {
lock.unlock();
}
}
示例代码如下:
更改第3节Account账户类的代码。
publicclassAccount { privateStringcardId; doublemoney; // 账户余额// final修饰,表示锁对象唯一且不可替换privatefinalLocklock=newReentrantLock(); publicAccount() { } publicAccount(StringcardId, doublemoney) { this.cardId=cardId; this.money=money; } publicvoiddrawMoney(doubledrawMoney) throwsInterruptedException { // synchronized代表同步方法// 获取取钱人姓名StringuserName=Thread.currentThread().getName(); lock.lock(); // 上锁try { // 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款"); // 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元"); // 更新余额this.money-=drawMoney; System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元"); } else { // 余额不足System.out.println(userName+"取款余额不足!"); } } finally { // 将解锁方法添加到finally语句中,保证即使try语句出现异常,也可以将上锁的资源解锁,等待下一线程访问lock.unlock(); // 解锁 } } publicStringgetCardId() { returncardId; } publicvoidsetCardId(StringcardId) { this.cardId=cardId; } publicdoublegetMoney() { returnmoney; } publicvoidsetMoney(doublemoney) { this.money=money; } }