• Java学习笔记(二十九)


    在完成对C语言的学习后,我最近开始了对C++和Java的学习,目前跟着视频学习了一些语法,也跟着敲了一些代码,有了一定的掌握程度。现在将跟着视频做的笔记进行整理。本篇博客是整理Java知识点的第二十九篇博客。

    本篇博客介绍了Java的多线程安全问题和生产者消费者模式。

    本系列博客所有Java代码都使用IntelliJ IDEA编译运行,版本为2022.1。所用JDK版本为JDK11

    目录

    多线程的安全问题

    卖票案例

    同步代码块

    同步方法解决数据安全问题

    线程安全的类

    Lock锁

    生产者消费者模式

    生产者消费者模式概述

    生产者消费者模式案例


    多线程的安全问题

    卖票案例

    要求模拟很简单的卖票案例,共有100张票,有三个窗口卖票。

    1. public class BuyTicket implements Runnable{
    2. private int tickets = 100;
    3. public void run() {
    4. while (true) {
    5. if (tickets > 0) {
    6. System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
    7. tickets -= 1;
    8. try {
    9. Thread.sleep(100);
    10. } catch (InterruptedException e) {
    11. throw new RuntimeException(e);
    12. }
    13. } else {
    14. System.out.println(Thread.currentThread() + " don't have any ticket left");
    15. try {
    16. Thread.sleep(100);
    17. } catch (InterruptedException e) {
    18. throw new RuntimeException(e);
    19. }
    20. }
    21. }
    22. }
    23. }

    BuyTicket类的成员变量tickets的值是100。run方法中设计了死循环,如果还有票就卖出票并输出卖票信息,卖完了就提示没有余票了。为了便于控制,因此采取让线程暂停的方法,避免输出的频率过快。因为是死循环,因此在没有余票后会手动停掉程序。

    1. public class BuyTickettest {
    2. public static void main(String[] args){
    3. BuyTicket bt = new BuyTicket();
    4. Thread t1 = new Thread(bt,"A");
    5. Thread t2 = new Thread(bt,"B");
    6. Thread t3 = new Thread(bt,"C");
    7. t1.start();
    8. t2.start();
    9. t3.start();
    10. }
    11. }

    BuyTickettest类创建了三个对象,分别表示A B C窗口,并启动多线程。

    下面是即将卖玩票以及刚卖完票后的部分结果:

    Thread[A,5,main] is selling the94 ticket
    Thread[C,5,main] is selling the94 ticket
    Thread[B,5,main] is selling the94 ticket
    Thread[C,5,main] is selling the97 ticket
    Thread[A,5,main] is selling the97 ticket
    Thread[B,5,main] is selling the97 ticket
    Thread[A,5,main] is selling the100 ticket
    Thread[B,5,main] is selling the100 ticket
    Thread[C,5,main] is selling the100 ticket
    Thread[B,5,main] don't have any ticket left
    Thread[A,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[B,5,main] don't have any ticket left
    Thread[A,5,main] don't have any ticket left

    但是数据和现实不符,不可能三个窗口卖的票数编号一样,然后再同时将票数减三。这是程序的随机性导致的一些问题。

    同步代码块

    多线程程序出现数据安全问题的原因是:多线程环境,有共享数据,有多条语句操作共享数据。解决多线程安全问题要让程序没有安全问题。

    解决方式是把多条语句操作共享数据的代码给锁起来,任意时刻只能有一个线程执行。

    Java提供了同步代码块的方式解决。格式是:

    synchronized(任意对象){

    多条语句操作共享数据的代码

    }

    synchronized(任意对象)相当于给代码加锁,任意对象可以看作一把锁。

    同步代码块解决了多线程的数据安全问题,但是当线程很多时,每个线程都判断同步上的锁,很耗费资源,降低程序运行效率。

    1. public class BuyTicket implements Runnable{
    2. private int tickets = 100;
    3. private Object obj = new Object();
    4. public void run() {
    5. while (true) {
    6. synchronized(obj) {
    7. if (tickets > 0) {
    8. System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
    9. tickets -= 1;
    10. try {
    11. Thread.sleep(100);
    12. } catch (InterruptedException e) {
    13. throw new RuntimeException(e);
    14. }
    15. } else {
    16. System.out.println(Thread.currentThread() + " don't have any ticket left");
    17. try {
    18. Thread.sleep(100);
    19. } catch (InterruptedException e) {
    20. throw new RuntimeException(e);
    21. }
    22. }
    23. }
    24. }
    25. }
    26. }

    这段代码改进了上面的程序,加入了同步代码锁。执行的类代码见上文。

    下面是即将卖完票以及刚卖完票后的部分结果:

    Thread[C,5,main] is selling the90 ticket
    Thread[B,5,main] is selling the91 ticket
    Thread[B,5,main] is selling the92 ticket
    Thread[C,5,main] is selling the93 ticket
    Thread[C,5,main] is selling the94 ticket
    Thread[C,5,main] is selling the95 ticket
    Thread[C,5,main] is selling the96 ticket
    Thread[C,5,main] is selling the97 ticket
    Thread[A,5,main] is selling the98 ticket
    Thread[A,5,main] is selling the99 ticket
    Thread[A,5,main] is selling the100 ticket
    Thread[A,5,main] don't have any ticket left
    Thread[A,5,main] don't have any ticket left
    Thread[A,5,main] don't have any ticket left

    现在票数是从1递增到100,这符合现实逻辑。

    同步方法解决数据安全问题

    同步方法就是把synchronized关键字加到方法上。格式是:

    修饰符 synchronized 返回值类型 方法名 (方法参数){...}

    同步方法的锁对象是this

    同步静态方法是把synchronized关键字加到静态方法上。格式是:

    修饰符 static synchronized 返回值类型 方法名(方法参数){...}

    同步静态方法的锁对象是类名.class

    1. public class BuyTicket implements Runnable{
    2. private int tickets = 100;
    3. private Object obj = new Object();
    4. public void run() {
    5. while (true) {
    6. synchronized(this) {
    7. if(tickets > 0){
    8. sellTicket();
    9. } else {
    10. System.out.println(Thread.currentThread() + " don't have any ticket left");
    11. try {
    12. Thread.sleep(100);
    13. } catch (InterruptedException e) {
    14. throw new RuntimeException(e);
    15. }
    16. }
    17. }
    18. }
    19. }
    20. public synchronized void sellTicket(){
    21. if (tickets > 0) {
    22. System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
    23. tickets -= 1;
    24. try {
    25. Thread.sleep(100);
    26. } catch (InterruptedException e) {
    27. throw new RuntimeException(e);
    28. }
    29. }
    30. }
    31. }

    程序将剩余票数大于0时卖票的操作移到了SellTicket方法中,并且将其置为同步方法。执行此代码的类见上文。

    下面是即将卖完票以及刚卖完票后的部分结果:

    Thread[C,5,main] is selling the90 ticket
    Thread[C,5,main] is selling the91 ticket
    Thread[C,5,main] is selling the92 ticket
    Thread[C,5,main] is selling the93 ticket
    Thread[C,5,main] is selling the94 ticket
    Thread[C,5,main] is selling the95 ticket
    Thread[C,5,main] is selling the96 ticket
    Thread[A,5,main] is selling the97 ticket
    Thread[A,5,main] is selling the98 ticket
    Thread[A,5,main] is selling the99 ticket
    Thread[A,5,main] is selling the100 ticket
    Thread[A,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[B,5,main] don't have any ticket left

    现在票数是从1递增到100,这符合现实逻辑。

    1. public class BuyTicket implements Runnable{
    2. private static int tickets = 100;
    3. private Object obj = new Object();
    4. public void run() {
    5. while (true) {
    6. synchronized(BuyTicket.class) {
    7. if(tickets > 0){
    8. sellTicket();
    9. } else {
    10. System.out.println(Thread.currentThread() + " don't have any ticket left");
    11. try {
    12. Thread.sleep(100);
    13. } catch (InterruptedException e) {
    14. throw new RuntimeException(e);
    15. }
    16. }
    17. }
    18. }
    19. }
    20. public static synchronized void sellTicket(){
    21. if (tickets > 0) {
    22. System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
    23. tickets -= 1;
    24. try {
    25. Thread.sleep(100);
    26. } catch (InterruptedException e) {
    27. throw new RuntimeException(e);
    28. }
    29. }
    30. }
    31. }

    这段代码将tickets成员变量设为静态变量。程序将剩余票数大于0时卖票的操作移到了SellTicket静态方法中,并且将其置为同步静态方法。执行此代码的类见上文。

    下面是即将卖完票以及刚卖完票后的部分结果:

    Thread[A,5,main] is selling the90 ticket
    Thread[C,5,main] is selling the91 ticket
    Thread[B,5,main] is selling the92 ticket
    Thread[B,5,main] is selling the93 ticket
    Thread[C,5,main] is selling the94 ticket
    Thread[A,5,main] is selling the95 ticket
    Thread[A,5,main] is selling the96 ticket
    Thread[C,5,main] is selling the97 ticket
    Thread[B,5,main] is selling the98 ticket
    Thread[C,5,main] is selling the99 ticket
    Thread[A,5,main] is selling the100 ticket
    Thread[A,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[B,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    现在票数是从1递增到100,这符合现实逻辑。

    线程安全的类

    StringBuffer是线程安全,可变的字符序列。从JDK5开始被StringBuilder替代。通常使用StringBuilder,因为这个更快。

    Vector改进了List接口,它是被同步的,如果不需要线程安全,通常使用ArrayList。

    Hashtable实现了一个哈希表,该类实现Map接口,且被同步。如果不需要线程安全,就使用HashMap。

    Lock锁

    同步代码块以及同步方法设置了锁,但是我们没有直接看到在哪里加上了锁,在哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK5后提供了一个新的锁对象Lock

    Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

    Lock类提供了获得锁和释放锁的方法:

    void lock()获得锁。

    void unlock()释放锁。

    Lock类不能直接实例化,这里采用它的实现类ReentrantLock来实例化。

    ReentrantLock()创建一个ReentrantLock的实例。

    使用ReentrantLock需要导包,import java.util.concurrent.locks.ReentrantLock

    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class BuyTicket implements Runnable{
    4. private int tickets = 100;
    5. private Lock lock = new ReentrantLock();
    6. public void run() {
    7. while (true) {
    8. lock.lock();
    9. if (tickets > 0) {
    10. System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
    11. tickets -= 1;
    12. try {
    13. Thread.sleep(100);
    14. } catch (InterruptedException e) {
    15. throw new RuntimeException(e);
    16. }
    17. } else {
    18. System.out.println(Thread.currentThread() + " don't have any ticket left");
    19. try {
    20. Thread.sleep(100);
    21. } catch (InterruptedException e) {
    22. throw new RuntimeException(e);
    23. }
    24. }
    25. lock.unlock();
    26. }
    27. }
    28. }

    程序在开始执行卖票的语句前获得锁,在执行完卖票的语句后释放锁。执行此代码的类见上文。

    下面是即将卖完票以及刚卖完票后的部分结果:

    Thread[B,5,main] is selling the90 ticket
    Thread[B,5,main] is selling the91 ticket
    Thread[B,5,main] is selling the92 ticket
    Thread[B,5,main] is selling the93 ticket
    Thread[B,5,main] is selling the94 ticket
    Thread[B,5,main] is selling the95 ticket
    Thread[B,5,main] is selling the96 ticket
    Thread[B,5,main] is selling the97 ticket
    Thread[C,5,main] is selling the98 ticket
    Thread[C,5,main] is selling the99 ticket
    Thread[C,5,main] is selling the100 ticket
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    Thread[C,5,main] don't have any ticket left
    现在票数是从1递增到100,这符合现实逻辑。

    生产者消费者模式

    生产者消费者模式概述

    生产者消费者模式是一个经典的多线程协作的模式。

    此类问题一般包含两类线程,一类是生产者线程用于生产数据,一类是消费者线程用于消费数据。

    为了解耦生产者和消费者的关系,通常采用共享的数据区域。生产者生产数据后直接放置在共享数据区中,不关心消费者的行为。消费者只需要从共享数据区中获取数据,不需要关心生产者的行为。

    为了体现生产和消费的等待和唤醒,Java提供了几个方法。 这几个方法在Object类中。

    void wait()导致当前的线程等待,直到另一个线程调用该对象的notify方法或notifyAll方法。 

    void notify()唤醒正在等待对象监视器的单个线程。

    void notifyAll()唤醒正在等待对象监视器的所有线程。

    生产者消费者模式案例

    下面以送牛奶和取牛奶为例。

    1. public class milkbox {
    2. private int number;
    3. private boolean flag = false;
    4. public synchronized void put(int number){
    5. if(flag == true){
    6. try {
    7. wait();
    8. } catch (InterruptedException e) {
    9. throw new RuntimeException(e);
    10. }
    11. }
    12. this.number = number;
    13. System.out.println("There is putting " + number + " milk");
    14. flag = true;
    15. notifyAll();
    16. }
    17. public synchronized void get(){
    18. if(flag == false){
    19. try {
    20. wait();
    21. } catch (InterruptedException e) {
    22. throw new RuntimeException(e);
    23. }
    24. }
    25. System.out.println("There is getting " + number + " milk");
    26. flag = false;
    27. notifyAll();
    28. }
    29. }

    milkbox是奶箱类。成员变量number的值代表奶的数目,flag用于标记。put方法用于送奶,接受一个参数表示送入奶的数量,如果flag为true就进入等待状态,否则将number的值设置为参数的值,然后输出相关信息,将flag置为true,随后唤醒其他线程。get方法用于取奶,无参数,如果flag是false就进入等待状态,否则输出相关信息,将flag置为false,随后唤醒其他线程。

    1. public class producer implements Runnable{
    2. milkbox mb;
    3. public producer(){}
    4. public producer(milkbox mb){
    5. this.mb = mb;
    6. }
    7. public void run(){
    8. int i;
    9. for(i = 1;i <= 5;i += 1){
    10. mb.put(i);
    11. }
    12. }
    13. }

    producer类是生产者,有一个milkbox类对象mb。run方法采用循环,依次将1至5作为mb的put方法的参数。

    1. public class customer implements Runnable{
    2. milkbox mb;
    3. public customer(){}
    4. public customer(milkbox mb){
    5. this.mb = mb;
    6. }
    7. public void run(){
    8. while(true){
    9. mb.get();
    10. }
    11. }
    12. }

    customer类是消费者,有一个milkbox类对象mb,run方法采用死循环,不断执行mb的get方法。

    1. public class milktest {
    2. public static void main(String[] args){
    3. milkbox mb = new milkbox();
    4. producer p = new producer(mb);
    5. customer c = new customer(mb);
    6. Thread t1 = new Thread(p);
    7. Thread t2 = new Thread(c);
    8. t1.start();
    9. t2.start();
    10. }
    11. }

    这是相关测试代码。

    程序的输出是:

    There is putting 1 milk
    There is getting 1 milk
    There is putting 2 milk
    There is getting 2 milk
    There is putting 3 milk
    There is getting 3 milk
    There is putting 4 milk
    There is getting 4 milk
    There is putting 5 milk
    There is getting 5 milk

    输出完这些内容后程序不输出任何内容,将其手动结束。

  • 相关阅读:
    editplus如何批量删除包含某个字符串的行
    QT中软件cpu占用率很高,甚至达到了50% 62%左右
    qsort函数和模拟实现qsort函数
    python中pdf转图片的操作方法二
    AndroidStudio案例——简单计算器
    Go语言支持闭包吗?说说你对它的理解
    3. 数组+【矩阵压缩存储】:对称、三角、三对角、稀疏矩阵
    AI 帮写代码 67 元/月,GitHub Copilot 开启收费模式!
    【机器学习】符号主义类模型:解码智能的逻辑之钥
    combit Report Server 29
  • 原文地址:https://blog.csdn.net/m0_71007572/article/details/126439581