线程安全
什么是线程安全问题?
- 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
1.线程安全问题出现的原因?
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
用程序模拟线程安全问题
取钱案例
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万
分析:
- 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
- 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)
- 创建2个线程,传入同一个账户对象给2个线程处理
- 启动2个线程,同时去同一个账户对象中取钱10万
//测试类public class ThreadTest {public static void main(String[] args) throws Exception {//1.创建一个账户对象,代表两个人共享账户Account acc = new Account("ICBC-110", 100000);//2.创建两个线程,(创建一个线程类)分别代表小红,小明,再去同一个账户中取钱、// -》就需要把同一个账户对象交给两个线程//通过DrawThread有参构造器把同一个账户对象交给两个线程Thread t1 = new DrawThread(acc, "小明");//小明t1.start();//t1.join();//让小明这个线程先执行
// ——名字正好是线程的名字——》有参构造器多接一个名字的参数用name来接再通过super送给Thread提供的有参构造器new DrawThread(acc, "小红").start();//小红}
}
//账户类
import javax.print.DocFlavor;
import java.security.PrivateKey;public class Account {private String IdCard;private double money;//账户的余额public Account() {}public Account(String idCard, double money) {this.IdCard = idCard;this.money = money;}//小明小红会同时取钱public void drawMoney(double money) {//取钱的金额也叫money——取钱的金额//首先得知道是谁来取钱——》可以为线程设置一个名字在ThreadTest类里边String name = Thread.currentThread().getName();//得到当前线程对象//1.判断余额是否足够 //this是一个变量,表示当前方法调用者的地址值if (this.money >= money) {//this.money指的是当前账户的余额System.out.println(name + "来取钱,取" + money + "钱成功!");//取完钱之后更新一下余额this.money = this.money - money;System.out.println(name + "取钱后余额为" + this.money);} else {System.out.println(name + "来取钱,余额不足");}}public String getIdCard() {return IdCard;}public void setIdCard(String idCard) {IdCard = idCard;}public Double getMoney() {return money;}public void setMoney(Double money) {this.money = money;}}
//线程类
public class DrawThread extends Thread {private Account acc;//创造有参构造器public DrawThread(Account acc, String name) {//接账户类型的对象-》怎么样能让账户对象能够被run方法使用来取钱?-》再往上面定义一个成员变量——》类型就是账户类型,名字就叫acc//我们就会把接到的账户对象交给定义的成员变量acc,此时就能用acc取钱//怎么交给他?super(name);//再通过super送给Thread提供的有参构造器this.acc = acc;//小明线程对象接了acc这个账户对象把他交给上面的成员变量acc}@Overridepublic void run() {//取钱 -》用这个账户对象取钱10wacc.drawMoney(100000);}
}
线程同步
认识线程同步
线程同步就是解决线程安全问题的方案。
线程同步的思想就是让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步的常见方案
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
方式一:同步代码块
- 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步锁的注意事项
- 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
锁对象用的是一个字符串的"锁",而字符串的"锁"在我们的系统中永远只有一份,也就意味着这个对象对于再多出来的其他线程来说也都是同一个对象,而现在用"锁"这个对象来作为锁,就会带来很大的问题,(第一个共享资源的1号抢到这个对象“锁”作为锁了,此时不光会锁住第一个共享资源的2号3号,也会锁住其他共享资源的其他人,导致其他人也不能“取钱”;实际上1号抢到锁只需要锁住2号3号就行了,不能锁住“其他家庭的人”。现在之所以会出现这个问题就是因为现在这个“锁”对象锁的范围太大了,对于所有的线程来说都是同一个对象,就不合适了。应该是自己家的人有一个锁对象,“一家一个锁对象”,这样才会控制每一家人的线程访问情况。)怎么做???
官方建议:在这个地方要用共享资源作为锁。(这个地方是一个实例方法,就应该用this作为锁,此时this正好代表共享资源。代码例子中,小明小红这两个线程的话,此时this正好代表的是acc账户;同理,如果是小黑小白两个线程进行到drawMoney这来的话,小黑小白拿的是acc1这个账户,此时this代表的是acc1这个账户,他就只会锁住小黑和小白,这样就不会干扰别人了。)
注意:在实例方法中,建议使用this作为锁,正好代表线程的共享资源的。就不会出现一些问题。
拓展:可能会遇到多个线程调用静态方法,(假设静态方法是有50个线程在调用他)应该用什么锁?
答:如果是静态方法,官方建议我们使用类名.class作为锁。类名.class他是一个字节码文件,这个class文件在系统中只有一份,那为什么静态方法就建议class作为锁呢?这是因为静态方法就是被所有线程通过这个类名来进行访问的,现在要锁住所有的线程,就直接用类名.class(本身就只有一份)锁住当前访问这个静态方法的所有线程,只允许一个线程进来就可以了。合理!
总结:
锁对象随便选择一个唯一的对象好不好呢?不好,会影响其他无关线程的执行
锁对象的使用规范:
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
//同步代码块
/***ctrl+alt+t 将访问共享资源(访问账户在同时取钱)的核心代码,放到同步代码块里面,对他进行上锁*同步代码块还需要声明一个所谓的同步锁,就是用一个Java对象表示的一把锁*而且锁要求对应当前执行的线程必须是同一个对象才可以*双引号给出的一个对象在计算机中(常量池)只有一份,对于当前执行的线程来说肯定是同一个对象——》当然 *可以锁住线程,只允许一个线程进来*/
synchronized ("锁") {if (this.money >= money) {System.out.println(name + "来取钱,取" + money + "钱成功!");this.money = this.money - money;System.out.println(name + "取钱后余额为" + this.money);} else {System.out.println(name + "来取钱,余额不足");}}
方式二:同步方法
- 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
是同步代码块好还是同步方法好一点?
- 范围上:同步代码块锁的范围更小,同步方法锁的范围更大,
- 可读性:同步方法更好
底层是有一个隐含的锁的,如果方法是实例方法隐含的锁是用this作为锁的,只是看不到,(this此时正好代表他们共享的acc账户),每次只允许一个人进来访问这个锁,访问完毕后自动解锁,其他线程进来就不会有安全问题。如果以后方法是静态方法的话,也可以用synchronized把他声明成一个同步方法,这样也能够保证线程安全,只是默认隐含的锁是类名.class作为锁。
//同步方法public synchronized void drawMoney(double money) {String name = Thread.currentThread().getName();if (this.money >= money) {System.out.println(name + "来取钱,取" + money + "钱成功!");this.money = this.money - money;System.out.println(name + "取钱后余额为" + this.money);} else {System.out.println(name + "来取钱,余额不足");}}
方式三:Lock锁
- LocK锁是IDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
- Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
Lock的常用方法
private final Lock lk = new ReentrantLock();
//创建一个Lock锁对象
//final->lk记录的这个锁对象是不能被替换的,将锁对象进行一个保护
//是下面的取钱方法要用锁对象,所以锁对象要放在账户类里面,方便下面使用
//实例变量,属于一个对象变量。——》每一个账户对象都应该有一个锁对象
//放在这里创建锁对象定成一个实例变量的形式:是因为创建账户对象的时候,同时也可以创建一个属于这个账户的唯一一个锁对象出来
//Lock锁public void drawMoney(double money) {String name = Thread.currentThread().getName();lk.lock();//加锁:意思是如果有多个线程对象执行到这来,只允许一个线程进行加锁再进来,执行完毕后应该解锁try {//即便加锁之后中间程序出现问题、bug被拦截之后最终还是能进行解锁,这样就比较安全//好处就在于一个线程进来出现问题,最终还是会解锁,其他线程还是能进来跑if (this.money >= money) {System.out.println(name + "来取钱,取" + money + "钱成功!");this.money = this.money - money;System.out.println(name + "取钱后余额为" + this.money);} else {System.out.println(name + "来取钱,余额不足");}} catch (Exception e) {e.printStackTrace();} finally {lk.unlock();//解锁}}
线程通信【了解】
什么是线程通信?
- 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
线程通信的常见模型(生产者与消费者模型)
- 生产者线程负责生产数据
- 消费者线程负责消费生产者生产的数据。
- 注意:生产者生产完数据应该等待自己,通知消费者消费消费者消费完数据也应该等待自己,再通知生产者生产。
Object类的等待和唤醒方法:(写代码时顺序应该先唤醒再等待!!!!!!!!!!!)
注意:上述方法应该使用当前同步锁对象进行调用。
无论这里发生了什么情况,谁先抢到锁谁后抢到锁,最终一定是一个生产->消费的过程(做一个包子->吃一个包子)
//需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
// 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
public class ThreadTest {public static void main(String[] args) {Desk desk = new Desk();//需要创建三个生产者线程(三个厨师)//用匿名内部类的方式创建线程/* new Thread(new Runnable() {@Overridepublic void run() {}}).start();*///Lambdanew Thread(() -> {while (true) {//不断的抢桌子放包子desk.put();}},"厨师1").start();new Thread(() -> {while (true) {//不断的抢桌子放包子desk.put();}},"厨师2").start();new Thread(() -> {while (true) {//不断的抢桌子放包子desk.put();}},"厨师3").start();//创建2个消费者(2个吃包子的)new Thread(() -> {while (true) {//不断的抢桌子放包子desk.get();}},"吃货1").start();new Thread(() -> {while (true) {//不断的抢桌子放包子desk.get();}},"吃货2").start();}
}
import java.util.ArrayList;
import java.util.List;//桌子上面要放一个包子
public class Desk {private List<String> list = new ArrayList<>();//放包子(一个)//厨师1 厨师2 厨师3public synchronized void put() {try {String name = Thread.currentThread().getName();//拿到名字就知道是哪一个厨师进来了//判断是否有包子if (list.size()==0){list.add(name + "做了一个肉包子");System.out.println(name+"做了一个肉包子");Thread.sleep(2000);//做包子花了2s//把异常trycatch不要往外面抛不然去外面还得抛//线程通信的重点:等待自己,不去竞争cpu资源和锁,把自己暂停 (提高整体的性能)——》对应的是唤醒别人this.notifyAll();this.wait();}else {//厨师再进来就会发现有包子,不做——》也就该等待自己,唤醒别人this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}//拿包子//吃货1 吃货2public synchronized void get() {try {String name = Thread.currentThread().getName();//拿到名字就知道是哪一个吃货进来了if (list.size()==1){//有包子,吃掉System.out.println(name+"吃掉"+list.get(0));//从集合里边把包子取出来,因为是放一个包子就是索引0list.clear();//吃完之后把集合清空——》唤醒别人,等待自己Thread.sleep(1000);//吃包子花了1sthis.notifyAll();this.wait();}else {//没有包子,唤醒别人,等待自己this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}
}