文章目录
线程安全问题
线程安全问题指的是: 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
举例: 取钱模型演示
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?
在取钱之前都需要判断余额是否足够, 例如两个线程同时执行, 两个线程都进行了余额判断, 发现余额充足;
此时小明线程取走10万, 账户余额为0; 小红线程由于此时已经判断过余额, 继续取钱的时候就不会继续判断余额, 直接将余额取出来; 那么两个人都取走了10万, 银行就亏了10万, 这就是多线程带来的安全问题
线程安全问题模拟, 我们将上面的例子用代码模拟出来多线程的安全隐患:
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
- 需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
- 需要定义一个线程类,线程类可以处理账户对象。
- 创建2个线程对象,传入同一个账户对象。
- 启动2个线程,去同一个账户对象中取钱10万。
实现步骤:
模拟一个账户类给小明线程和小红线程, 我们模拟关键信息即可
publicclassAccount{privatedouble money;publicAccount(){}publicAccount(double money){this.money = money;}/**
取钱方法
*/publicvoiddrawMoney(double money){// 获取取钱人的名字String name =Thread.currentThread().getName();if(this.money >= money){// 判断余额是否充足System.out.println(name +"取走了"+ money +"元");this.money -= money;}else{System.out.println("余额不足");}}publicdoublegetMoney(){return money;}publicvoidsetMoney(double money){this.money = money;}}
定义一个线程类用来处理账户对象
publicclassDrawThreadextendsThread{privateAccount acc;publicDrawThread(Account acc,String name){super(name);this.acc = acc;}@Overridepublicvoidrun(){
acc.drawMoney(100000);}}
在主类中, 创建2个线程对象,传入同一个账户对象; 启动2个线程,去同一个账户对象中取钱10万
publicclassThreadDemo{publicstaticvoidmain(String[] args)throwsException{// 创建账户对象Account acc =newAccount(100000);// 创建两个子线程, 并启动线程newDrawThread(acc,"小明").start();// 小明取走了100000.0元newDrawThread(acc,"小红").start();// 小红取走了100000.0元// 主线程睡眠两秒后, 查看余额Thread.sleep(2000);System.out.println(acc.getMoney());// -100000.0}}
线程同步
为了解决上面线程安全的问题。
取钱案例出现问题的原因?
多个线程同时执行,发现账户都是够钱的。
如何才能保证线程安全呢?
让多个线程实现先后依次排队访问共享资源,这样就解决了安全问题
线程同步的核心:
线程同步的核心是加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
加锁的方式有三种方式: 同步代码块, 同步方法和Lock锁, 下面我们来分别学习加锁的方法
方式一: 同步代码块
作用:将出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步代码块上锁的格式:
synchronized(同步锁对象){// 操作共享资源的代码(核心代码)}
例如我们给上面模拟的线程安全例子中的核心代码进行加锁操作:
进行加锁操作就解决了线程安全问题
publicclassAccount{privatedouble money;publicAccount(){}publicAccount(double money){this.money = money;}/**
取钱方法
*/publicvoiddrawMoney(double money){String name =Thread.currentThread().getName();// 对核心代码进行加锁, 这里锁对象随便使用了一个字符串模拟(无实际代表意义)synchronized("chen"){if(this.money >= money){System.out.println(name +"取走了"+ money +"元");this.money -= money;}else{System.out.println("余额不足");}}}publicdoublegetMoney(){return money;}publicvoidsetMoney(double money){this.money = money;}}
锁对象注意:
理论上:锁对象只要对于当前同时执行的线程来说是唯一的一个对象即可。
但是实际上使用任意唯一的锁对象并不好, 会影响其他无关线程的执行, 例如上面例子中, 会将其他无关的账户也锁起来。
锁对象的规范要求:
规范上:建议使用共享资源作为锁对象。
- 对于实例方法中, 建议使用this作为锁对象。
// 加锁, 使用共享资源作为锁对象synchronized(this){}
- 对于静态方法中, 建议使用字节码(类名.class)对象作为锁对象。
// 加锁, 使用共享资源作为锁对象synchronized(类名.class){}
方式二: 同步方法
作用: 将出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法的上锁格式:
修饰符 synchronized 返回值类型 方法名称(形参列表){// 操作共享资源的代码}
同步方法上锁演示代码:
// 给核心方法上锁publicsynchronizedvoiddrawMoney(double money){String name =Thread.currentThread().getName();if(this.money >= money){System.out.println(name +"取走了"+ money +"元");this.money -= money;}else{System.out.println("余额不足");}}
同步方法底层原理:
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
方式三: Lock锁
Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock实现提供比使用synchronized方法和语句可以获得更广泛灵活的锁定操作。
Lock是接口不能直接实例化,这里采用它的
实现类ReentrantLock
来构建Lock锁对象。
方法名称说明ReentrantLock()获得Lock锁的实现类对象
Lock的API:
方法名称说明lock()获得锁unlock()释放锁
Lock使用演示代码:
基本使用如下, 我们可以看出Lock使用是非常灵活的
publicclassAccount{privatedouble money;// 定义一个示例变量锁对象, 每创建一个类就会创建一个锁对象, 加final修饰, 表示不可替换privatefinalLock lock =newReentrantLock();publicAccount(){}publicAccount(double money){this.money = money;}publicsynchronizedvoiddrawMoney(double money){String name =Thread.currentThread().getName();// 调用锁对象上锁
lock.lock();if(this.money >= money){System.out.println(name +"取走了"+ money +"元");this.money -= money;}else{System.out.println("余额不足");}// 调用锁对象解锁
lock.unlock();}publicdoublegetMoney(){return money;}publicvoidsetMoney(double money){this.money = money;}}
但是如果我们上锁和解锁之间的代码出现了异常, 永远都不会执行解锁操作, 所以更严谨的写法是将解锁的操作放到try…finally中, 保证会执行解锁的操作
publicsynchronizedvoiddrawMoney(double money){String name =Thread.currentThread().getName();// 调用锁对象上锁try{
lock.lock();if(this.money >= money){System.out.println(name +"取走了"+ money +"元");this.money -= money;}else{System.out.println("余额不足");}}finally{// 调用锁对象解锁
lock.unlock();}}
版权归原作者 学全栈的灌汤包 所有, 如有侵权,请联系我们删除。