为什么要有队列?因为资源只有一个,如果开启多线程,多个线程同时对于一个资源进行操作,这样得到的各个线程操作之后的资源的值是不可靠、不可用的;并且线程的执行顺序我们是不能控制的,所以最后得到什么结果我们是不能控制的;为了解决这个问题,队列就诞生了,就是让多线程进行排队,一个一个的进行操作,这就解决了哪一个线程先使用资源的问题
有队列了还要🔒干嘛?因为线程执行的过程我们是不可控的,所以我们不能保证线程执行起来它真的就乖乖地在队列里面等着,而不会干扰正在执行地线程,比如当前A线程正在使用资源,B线程也去使用资源,这就会导致最后资源的计算结果出现错误;所以为了保证正在执行地线程地安全,就有了🔒的概念;我们可以把资源想象成一个单间中的马桶,线程就是一长溜等待上厕所的人,每个人都想先去上厕所,所以就会挣厕所,就会造成大家都上不了厕所的情况,所以就出现了队列;虽然排了队,但是你在上厕所的时候可能有一个老大哥来了,强行要抢你的厕所,那么这就让你不得不暂停,这个时候就体现出了🔒的必要性,你就去之后把门锁上,那么外面的人就只能乖乖等你出来之后才能再进去了线程同步就是需要队列+🔒才能实现,线程同步就是让多线程安全的访问同一资源
🔒在Java中有一个关键词叫synchronized
线程同步显然就会降低性能,即虽然我们开了多线程,但是我们又让每个线程都排队,并且去给每个线程都配上了一把🔒,让每个线程都把自己的事情做完了才开🔒,这就破坏了高效的异步执行,变为了同步执行;但是这是高效和安全执行的一种权衡和妥协,相对于高性能不可靠的结果,我们更愿意选择低性能可靠的结果
【代码举例】不可靠的多线程
public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket ticket = new BuyTicket(); new Thread(ticket).start(); new Thread(ticket).start(); new Thread(ticket).start(); } } class BuyTicket implements Runnable{ private int ticket = 10;//总票数 private boolean flag = true; @Override public void run() { //买票 while (flag){ buyTicket(); } } private void buyTicket(){ if (this.ticket>0){ System.out.println(Thread.currentThread().getName()+"买到了一张票,现在余票数 = "+ --ticket); } else { this.flag = false; System.out.println("票卖完了!"); } try { Thread.sleep(100); //线程休眠,可以放大问题的发生可能性 } catch (InterruptedException e) { e.printStackTrace(); } return; } }上图中的异常就是多线程开启但是没有开启线程同步的时候容易出现的错误,究其原因都是因为多个线程同时访问了同一个资源,而线程对于资源的操作又是通过拷贝之后在自己的内存空间中进行操作,操作完成之后再将操作结果返回资源,对资源原来的值进行覆盖造成了
倒序异常&重复出现异常:A线程和B线程同时访问资源变量i,此时资源的值为9,因为线程执行是异步的,且执行顺序我们不能人为的干预,所以A和B谁先执行完都有可能,但是不管谁先执行完,它们返回的资源i的值都应该是8;假设A线程先执行完,资源i变成了8,但是在线程B返回自己的执行结果8之前来了线程C,它也执行了资源i减1的操作,并且先于线程B结束,那么它返回的i资源的数值应该是 i = 7;等到线程B执行完了,返回的资源 i = 8,就会对线程C刚刚修改的资源 i的值产生覆盖,所以资源数又变成了8
上面的解析中8重复出现,7出现之后又出现了8,这还只是同时有3个线程对于同一资源进行没有线程同步的访问,如果线程数增多,出错的情况可能更多
所以需要排队和+🔒实现线程同步,保证同一个资源被多个线程访问的时候的安全
注意:我们只是在线程访问同一个资源的时候才上锁,线程执行其他的东西的时候还是异步的,所以即使性能减弱,也不会完全退化为同步操作
【代码例子】同步块:这里需要举一个新的、麻烦一点的例子,银行取钱
public class UnsafeBank { public static void main(String[] args) { Account account = new Account(100,"100W的钱"); Draw draw1 = new Draw(account,50,"张三"); Draw draw2 = new Draw(account,100,"李四"); draw1.start(); draw2.start(); } } class Account{ //银行账户 int balance;//账户余额 String cardID;//钱用来干什么/这笔钱的名称 public Account(int balance, String cardID) { this.balance = balance; this.cardID = cardID; } } class Draw extends Thread{ //银行取钱这件事 Account account;//银行中的账户 int drawingMoney;//取出的钱的金额 public Draw(Account account, int drawingMoney, String name){ //构造 super(name); //父类为Thread,所以调用的是为线程取名字的构造,name用于标识当前是谁在去钱 this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { if (account.balance - drawingMoney <0){ System.out.println("取款金额大于余额,取款失败"); return; } try { Thread.sleep(100); //使用sleep()放大多线程出错的效果 } catch (InterruptedException e) { e.printStackTrace(); } account.balance = account.balance - drawingMoney; //当前余额 = 原来的金额 - 取出的金额 System.out.println(Thread.currentThread().getName()+"取走了"+this.drawingMoney +",当前余额为 = "+account.balance); } }
从上面的执行结果来看,显然不是我们想要的,因为银行账户中只有100w,但是两个人却取出了150w
上面3种情况就是在运行的时候出现的3种结果,并且没有一次是正常的结果,这就是多线程访问同一资源不使用线程同步的后果
结果 = 0,因为张三和李四线程同时发现银行余额为100,所以都通过了判断,然后张三线程先执行完毕,所以balance余额变为50,但是还没有执行到输出;李四后执行完,将自己的执行结果0返回获取修改了balance的值,这就造成了最后输出的都是0
结果 = 50:上面同理,但是是张三后执行完
结果 = -50:因为张三和李四线程同时发现银行余额为100,所以都通过了判断,然后李四线程在执行减余额之前,张三线程就已经执行完毕,并且将余额修改为50w,但是还有没有输出;李四线程读取到的余额就是50,减去自己的100,就是-50,然后李四后执行完,将自己的执行结果-50返回获取修改了balance的值,这就造成了最后输出的都是-50
使用同步方法尝试解决问题 使用同步方法没有效果的原因:因为我们使用的关键字synchronized获取的🔒是类的🔒,也就是说它🔒的是当前执行取钱这个过程的Draw,但是并多个线程同时访问的资源balance并不是类Draw的成员变量,所以我们在类Draw的run()上加上关键字synchronized是没有意义的,因为我们没有锁住被多个线程访问的资源,它是另一个类中的成员属性,它才是问题的核心,所以我们需要锁得时Account类的balance属性但是balance是一个属性,我们怎么锁?记住:锁来自于对象,所以我们不需要锁属性balance,只需要锁住使用的Account对象的代码块即可,所以我们需要使用到同步代码块将Account对象锁起来 注意:同步代码块🔒的是被操作的那个资源所属的对象,虽然同步监视器obj可以是任何的对象,但是如果不🔒进行增删改的那个资源,还是会出现多线程竞争同一个资源的错误情况上面列出了死锁的四个必要条件,即要出现死锁必须满足上面的4个条件,所以我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
【代码举例】死锁
public class DeadLock { public static void main(String[] args) { MakeUp makeUp1 = new MakeUp(0,"张三"); MakeUp makeUp2 = new MakeUp(2,"李四"); makeUp1.start(); makeUp2.start(); } } class LipStick{} //口红资源 class Mirror{} //镜子资源 class MakeUp extends Thread{ //化妆这件事 static Mirror mirror = new Mirror(); static LipStick lipStick = new LipStick(); int choice; //选择获取什么资源 String name; //获取资源的线程名称 public MakeUp(int choice, String name) { this.choice = choice; this.name = name; } @Override public void run() { makeup(); } public void makeup(){ if (choice == 0){ synchronized (lipStick){ System.out.println(name+"选择了口红的锁"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (mirror){ System.out.println(name+"选择了镜子的锁"); } } } else { synchronized (mirror){ System.out.println(name+"选择了镜子的锁"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lipStick){ System.out.println(name+"选择了口红的锁"); } } } } }
上面的代码中,都是先获取一种资源的🔒,然后在此基础上再去获取另一个资源的🔒,这就出现了死锁的条件,所以下面的结果出现了死锁
【解决】破坏死锁条件:不让线程在获取资源1的基础上再去获取资源2
在程序中一定要避免死锁的出现,做法就是破坏死锁形成的条件