Java学习之线程进阶

以你之姓@ 2024-04-08 08:38 217阅读 0赞

上一篇分享了线程的基本概念和创建线程的方法,以及简单应用,下边分享一下进阶内容。

线程的同步和锁

Java是支持多线程的,那么什么是多线程呢?
并发执行机制原理

简单地说就是把一个处理器划分为若干个短的时间片,每个时间片依次轮流地执行处理各个应用程序,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果。

多线程

就是把操作系统中的这种并发执行机制原理运用在一个程序中,把一个程序划分为若干个子任务,多个子任务并发执行,每一个任务就是一个线程。这就是多线程程序。

我们了解了多线程,再来看看Java的多线程并发时容易出现哪些问题
下边看一个案例:
需求:要求模拟火车站售票场景,三个窗口同时发售50张火车票。

  1. class MySync implements Runnable{
  2. int ticket = 50;
  3. @Override
  4. public void run() {
  5. while (true){
  6. if (ticket > 0){
  7. System.out.println(Thread.currentThread().getName() + "售票:"+ ticket+"号");
  8. ticket--;
  9. }else {
  10. System.out.println("售罄");
  11. break;
  12. }
  13. }
  14. }
  15. }
  16. public class Demo5 {
  17. public static void main(String[] args) {
  18. MySync mySync = new MySync();
  19. Thread thread1 = new Thread(mySync,"一号");
  20. thread1.start();
  21. Thread thread2 = new Thread(mySync,"二号");
  22. thread2.start();
  23. Thread thread3 = new Thread(mySync,"三号");
  24. thread3.start();
  25. }
  26. }

执行结果:
在这里插入图片描述
可以看到,结果并没有像我们预期的那样,反而出现了不合常理的BUG,50号车票被每一个窗口都售出一次,这显然是错误的。我们来了解一种新的概念。

使用同步方法

由于三个窗口对于同一个数据的引用并没有做到同步,所以导致出现了BUG。对于这个问题,Java给我们提供了一个同步方法,使用synchronized关键字来修饰方法。

  1. class MySync implements Runnable{
  2. int ticket = 50;
  3. @Override
  4. public synchronized void run() {
  5. while (true){
  6. if (ticket > 0){
  7. System.out.println(Thread.currentThread().getName() + "售票:"+ ticket+"号");
  8. ticket--;
  9. }else {
  10. System.out.println("售罄");
  11. break;
  12. }
  13. }
  14. }
  15. }
  16. public class Demo5 {
  17. public static void main(String[] args) {
  18. MySync mySync = new MySync();
  19. Thread thread1 = new Thread(mySync,"一号");
  20. thread1.start();
  21. Thread thread2 = new Thread(mySync,"二号");
  22. thread2.start();
  23. Thread thread3 = new Thread(mySync,"三号");
  24. thread3.start();
  25. }
  26. }

执行结果:
在这里插入图片描述
在这里插入图片描述
出现一个窗口一直将票卖完的情况,和我们预想的结果还是有些差距。需要再进行优化一下

  1. class MySync1 implements Runnable{
  2. int ticket = 50;
  3. @Override
  4. public void run() {
  5. while (true){
  6. synchronized (this){
  7. if (ticket > 0){
  8. System.out.println(Thread.currentThread().getName() + "售票:"+ ticket+"号");
  9. ticket--;
  10. }else {
  11. System.out.println("售罄");
  12. break;
  13. }
  14. }
  15. }
  16. }
  17. }
  18. public class Demo6 {
  19. public static void main(String[] args) {
  20. MySync1 mySync = new MySync1();
  21. Thread thread1 = new Thread(mySync,"一号");
  22. thread1.start();
  23. Thread thread2 = new Thread(mySync,"二号");
  24. thread2.start();
  25. Thread thread3 = new Thread(mySync,"三号");
  26. thread3.start();
  27. }
  28. }

在这里插入图片描述
至此总算和我们预期的结果一致了。那么这是什么原理呢?
其实synchronized就是给代码加锁。当使用这个关键字,修饰方法的时候,这个方法就会被锁保护。

原理:三个线程同时抢占资源,但代码块被加了锁。只允许一个线程进入。当一个线程执行时,另外的线程等待,执行完毕以后。三个线程再开始抢占资源,这样就保证了数据的同步。

守护线程和非守护线程

守护线程

守护线程是用来守护非守护线程的

非守护线程

即是普通的线程

二者之间的联系为,当非守护线程一旦执行结束,那么不管守护线程有没有执行完毕,都会自动消亡。举个常见的例子。我们在电脑上登录QQ的时候,如果我们选择退出QQ,那么所有已经打开的聊天窗口都会被关闭。QQ的主界面就是非守护线程,所有打开的聊天窗口就是守护线程。
注意:

  • 一个应用程序中必须至少一个非守护线程
  • 非守护线程一旦结束,守护线程就会自动消亡
  • main 主函数是非守护线程
  • 真实开发的时候:
    后台记录操作日志,监控内存,垃圾回收等 都可以使用守护线程

下面看一个例子:

  1. class MyThread1 implements Runnable{
  2. @Override
  3. public void run() {
  4. System.out.println("软件更新中");
  5. for (int i = 0; i <= 100; i++) {
  6. System.out.println("下载资源数...."+i+"/100");
  7. }
  8. }
  9. }
  10. public class Demo1 {
  11. public static void main(String[] args) {
  12. for (int i = 0; i < 20; i++) {
  13. System.out.println("主线程正在执行"+i);
  14. }
  15. Thread thread = new Thread(new MyThread1());
  16. //标记为守护线程,必须在线程执行之前
  17. thread.setDaemon(true);
  18. thread.start();
  19. }
  20. }

执行结果:
在这里插入图片描述
可以看到,当main函数中的循环走完,即使run方法的循环才刚刚执行了4次,但却直接停止执行了。这就是守护线程和非守护线程的直接表现。

线程死锁

什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁线程。死锁是一种状态,当两个线程互相持有对方的资源的时候,却又不主动释放这个资源的时候。会导致死锁。这两个线程就会僵持住。代码就无法继续执行。

定义看上去可能并不是那么好理解,举个简单的生活场景来解释这个问题。
我们都知道,现在各大城市的疫情防控政策都十分的完善。那么进出小区门口的时候一般都是需要扫码才能进入。注意我们的条件(一切都是在理想化的假设前提下):

  • 绿码才能进入小区的大门
  • 可是扫码的机器坏掉了,无论是谁,都扫出来的是红码
  • 根据规定,红码不允许进入小区

由于机器都是特制的,只有找专业的工作人员来维修。但是就出现了以下的问题:

  • 工作人员也是外来人员,需要绿码才能被放行
  • 但是机器坏了,只能扫出来红码
  • 所以工作人员进不来
  • 所以机器也修不好

这就是一个经典的死锁问题,因为工作人员需要去维修机器,必须要进去小区。但是扫不出来绿码,不被允许进入。机器必须要修好才能扫出来绿码,但是工作人员进不来,无法进行维修。这样就进入了一个死循环。机器和维修人员都在等待对方释放自己需要的资源,又同时持有对方拥有的资源,都需要对方先释放持有的资源才能继续进行下一步。但是双方又因为只有得到对方持有的资源才能释放自己持有的资源,这样就陷入了永远的等待。这就是死锁。
特别注意:
开发中禁止使用死锁
下边看一下一个简单的死锁代码:

  1. class DeadLock implements Runnable{
  2. private boolean flag;//标记属性
  3. private Object obj1;//
  4. private Object obj2;
  5. //有参构造
  6. public DeadLock(boolean flag, Object obj1, Object obj2) {
  7. this.flag = flag;
  8. this.obj1 = obj1;
  9. this.obj2 = obj2;
  10. }
  11. @Override
  12. public void run() {
  13. if(flag){
  14. String name = Thread.currentThread().getName();
  15. synchronized (obj1){
  16. System.out.println(name+"拿到锁1");
  17. System.out.println("等待锁2的释放");
  18. synchronized (obj2){
  19. System.out.println(name+"-》拿到锁2");
  20. }
  21. }
  22. }
  23. if (!flag){
  24. String name = Thread.currentThread().getName();
  25. synchronized (obj2){
  26. System.out.println(name+"拿到锁2");
  27. System.out.println("等待锁1的释放");
  28. synchronized (obj1){
  29. System.out.println(name+"-》拿到锁1");
  30. }
  31. }
  32. }
  33. }
  34. }
  35. public class Demo3 {
  36. public static void main(String[] args) {
  37. Object o1 = new Object();
  38. Object o2 = new Object();
  39. //第一个线程 flag 是true
  40. DeadLock deadLock = new DeadLock(true, o1, o2);
  41. new Thread(deadLock, "线程1").start();
  42. //第二个线程 flag 是flase
  43. DeadLock deadLock1 = new DeadLock(false, o1, o2);
  44. new Thread(deadLock1, "线程2").start();
  45. }
  46. }

执行结果:
在这里插入图片描述
可以看到,代码一直僵持在这里,无法执行。

线程的生命周期

一个线程从创建,到执行结束,一共有五个部分组成

1. 新建:当一个Thread类或其子类的对象被声明并创建时。新生的线程对象属于新建状态。
2. 就绪(可运行状态):处于新建状态的线程执行start()方法后,进入线程队列等待CPU时间片,该状态具备了运行的状态,只是没有分配到CPU资源。
3. 运行:就绪的线程分配到CPU资源便进入运行状态,run()方法定义了线程的操作。
4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的的执行,进入阻塞状态。
5. 消亡:当线程执行完自己的操作或提前被强制性的终止或出现异常导致结束,会进入死亡状态。
在这里插入图片描述

线程的等待与唤醒

Object类下边有这样三个和线程有关的方法

  1. wait():让线程进入阻塞状态。除非被唤醒,否则不会继续执行。
  2. notify()唤醒正在等待对象监视器的单个线程。
  3. notifyAll()唤醒正在等待对象监视器的所有线程。

注意:

线程的等待和唤醒通常都是搭配使用的,至少两个线程,其中一个线程中使用对象.wait() 那么这个线程就会阻塞,代码不会往下执行了。如何想让这个线程往下执行呢?再开另外一个线程,使用对象.notify()去唤醒另外那个等待线程。两个线程使用的是同一个对象。

下边看代码:

  1. import java.text.SimpleDateFormat;
  2. import java.util.Date;
  3. class Message{
  4. private String msg;
  5. public Message(String msg) {
  6. this.msg = msg;
  7. }
  8. public String getMsg() {
  9. return msg;
  10. }
  11. public void setMsg(String msg) {
  12. this.msg = msg;
  13. }
  14. @Override
  15. public String toString() {
  16. return "Message{" +
  17. "msg='" + msg + '\'' +
  18. '}';
  19. }
  20. }
  21. class NotifyThread implements Runnable{
  22. private Message message;
  23. public NotifyThread(Message message) {
  24. this.message = message;
  25. }
  26. @Override
  27. public void run() {
  28. try {
  29. Thread.sleep(5000);
  30. //等待5秒,让等待线程先执行等待
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. String name = Thread.currentThread().getName();
  35. Date date = new Date(System.currentTimeMillis());
  36. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  37. System.out.println(name+"开始唤醒等待线程:"+ sdf.format(date));
  38. synchronized (message){
  39. message.setMsg("修改之后的message");
  40. //message.notify();
  41. message.notifyAll();
  42. }
  43. }
  44. }
  45. class WaitThread implements Runnable{
  46. private Message message;
  47. public WaitThread(Message message) {
  48. this.message = message;
  49. }
  50. @Override
  51. public void run() {
  52. String name = Thread.currentThread().getName();
  53. Date date1 = new Date(System.currentTimeMillis());
  54. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  55. System.out.println(name+"等待唤醒时间:"+sdf.format(date1));
  56. synchronized (message){
  57. try {
  58. message.wait();
  59. } catch (InterruptedException e) {
  60. e.printStackTrace();
  61. }
  62. Date date2 = new Date(System.currentTimeMillis());
  63. System.out.println(name+ "被唤醒的时间:"+sdf.format(date2));
  64. }
  65. }
  66. }
  67. public class Demo2 {
  68. public static void main(String[] args) {
  69. Message message = new Message("我是信息");
  70. WaitThread waitThread1 = new WaitThread(message);
  71. WaitThread waitThread2 = new WaitThread(message);
  72. WaitThread waitThread3 = new WaitThread(message);
  73. NotifyThread notifyThread = new NotifyThread(message);
  74. new Thread(waitThread1,"等待线程1").start();
  75. new Thread(waitThread2,"等待线程2").start();
  76. new Thread(waitThread3,"等待线程3").start();
  77. new Thread(notifyThread,"唤醒线程").start();
  78. }
  79. }

执行结果:
在这里插入图片描述

生产者消费者模式

生产者与消费者模式就是一个多线程并发协作的模式,在这个模式中,一部分线程被用于去生产数据,另一部分线程去处理数据,于是便有了所谓的生产者与消费者。

要用Java实现这个模式,我们可以借助线程的wait(),和notify(),方法。
案例:
假设,现在有一名农民伯伯要去买拖拉机。现在制造商为了不让货卖不出去砸在自己手里,于是他就每次只生产一台,只有上一台卖掉了,才会生产下一台。农民伯伯是每次只卖一台拖拉机,只要制造商生产出一台,他就会买一台。下面看代码:

  1. //两个线程要通信,需要一个中间对象
  2. class Goods{
  3. private String name;
  4. private double price;
  5. private boolean isProduct;//是否需要生产ture,false
  6. public Goods(String name, double price, boolean isProduct) {
  7. this.name = name;
  8. this.price = price;
  9. this.isProduct = isProduct;
  10. }
  11. public String getName() {
  12. return name;
  13. }
  14. public void setName(String name) {
  15. this.name = name;
  16. }
  17. public double getPrice() {
  18. return price;
  19. }
  20. public void setPrice(double price) {
  21. this.price = price;
  22. }
  23. public boolean isProduct() {
  24. return isProduct;
  25. }
  26. public void setProduct(boolean product) {
  27. isProduct = product;
  28. }
  29. @Override
  30. public String toString() {
  31. return "Goods{" +
  32. "name='" + name + '\'' +
  33. ", price=" + price +
  34. ", isProduct=" + isProduct +
  35. '}';
  36. }
  37. }
  38. //厂家
  39. class Producer implements Runnable{
  40. private Goods goods;
  41. public Producer(Goods goods) {
  42. this.goods = goods;
  43. }
  44. @Override
  45. public void run() {
  46. //消费者消费了,就必须要生产
  47. int order = 0;
  48. while (true){
  49. synchronized (goods){
  50. try {
  51. Thread.sleep(3000);
  52. } catch (InterruptedException e) {
  53. e.printStackTrace();
  54. }
  55. if(goods.isProduct()){
  56. //需要生产
  57. //造车
  58. if (order % 2 == 0){
  59. goods.setName("奔马拖拉机");
  60. goods.setPrice(200000);
  61. }else {
  62. goods.setName("泰山拖斗");
  63. goods.setPrice(10000);
  64. }
  65. goods.setProduct(false);//设置标记为不需要生产
  66. System.out.println("制造商制造了:"+goods.getName()+",价格:"+goods.getPrice());
  67. order++;
  68. //唤醒消费者线程
  69. goods.notify();
  70. }else{
  71. //不需要生产,工厂休息
  72. try {
  73. goods.wait();
  74. } catch (InterruptedException e) {
  75. e.printStackTrace();
  76. }
  77. }
  78. }
  79. }
  80. }
  81. }
  82. //顾客
  83. class Customer implements Runnable{
  84. private Goods goods;//通过商品联系生产者和消费者
  85. public Customer(Goods goods) {
  86. this.goods = goods;
  87. }
  88. @Override
  89. public void run() {
  90. while (true){
  91. //购买车
  92. synchronized (goods){
  93. try {
  94. Thread.sleep(3000);
  95. } catch (InterruptedException e) {
  96. e.printStackTrace();
  97. }
  98. if (!goods.isProduct()){
  99. //不需生产
  100. //买
  101. System.out.println("农民伯伯买了:"+goods.getName());
  102. goods.setProduct(true);
  103. goods.notify();//唤醒生产者的等待线程
  104. }else {
  105. //买不到,自己需要等待
  106. try {
  107. goods.wait();
  108. } catch (InterruptedException e) {
  109. e.printStackTrace();
  110. }
  111. }
  112. }
  113. }
  114. }
  115. }
  116. public class Demo6 {
  117. public static void main(String[] args) {
  118. Goods goods = new Goods("奔驰大G",100000,false);
  119. Customer customer = new Customer(goods);
  120. new Thread(customer,"消费者").start();
  121. Producer producer = new Producer(goods);
  122. new Thread(producer,"制造商").start();
  123. }
  124. }

执行结果:
在这里插入图片描述

案例中,只要拖拉机卖出去,就会唤醒制造商线程,制造车辆。而此时农民伯伯线程是处于等待状态。等制造商制造了一台车,就会让自己的线程状态设为等待,而将农民伯伯的线程唤醒让他前来买车。。制造商充当生产者,农民伯伯充当消费者。就这样形成一个生产者消费者模式。

本篇内容至此结束,关于线程池,由于实力不允许,这里就不再过多介绍。。。。。

发表评论

表情:
评论列表 (有 0 条评论,217人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JAVA】多线

    取钱案例出现问题的原因?多个线程同时执行,发现账户都是够钱的。如何才能保证线程安全呢?让多个线程实现先后依次访问共享资源,这样就解决了安全问题线程同步的核心思想加锁,把共...

    相关 Java学习线

    上一篇分享了线程的基本概念和创建线程的方法,以及简单应用,下边分享一下进阶内容。 线程的同步和锁 Java是支持多线程的,那么什么是多线程呢? 并发执行机制原理

    相关 线

    一、常见的锁策略 1.1读写锁 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,

    相关 Java线

    目录 一.上节内容复习 1.线程池的实现 2.自定义一个线程池,构造方法的参数及含义 3.线程池的工作原理 4.拒绝策略 5.为什么不推荐系统提供的线程池 二.常