Java学习笔记(二十九)

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

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

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

目录

多线程的安全问题

卖票案例

同步代码块

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

线程安全的类

Lock锁

生产者消费者模式

生产者消费者模式概述

生产者消费者模式案例


多线程的安全问题

卖票案例

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

public class BuyTicket implements Runnable{   
    private int tickets = 100;
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
                tickets -= 1;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            } else {
                System.out.println(Thread.currentThread() + " don't have any ticket left");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

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

public class BuyTickettest {
    public static void main(String[] args){
        BuyTicket bt = new BuyTicket();
        Thread t1 = new Thread(bt,"A");
        Thread t2 = new Thread(bt,"B");
        Thread t3 = new Thread(bt,"C");

        t1.start();
        t2.start();
        t3.start();
    }
}

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(任意对象)相当于给代码加锁,任意对象可以看作一把锁。

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

public class BuyTicket implements Runnable{
    private int tickets = 100;
    private Object obj = new Object();
    public void run() {
        while (true) {
            synchronized(obj) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
                    tickets -= 1;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    System.out.println(Thread.currentThread() + " don't have any ticket left");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

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

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

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

public class BuyTicket implements Runnable{    
    private int tickets = 100;
    private Object obj = new Object();
    public void run() {
        while (true) {
            synchronized(this) {
                if(tickets > 0){
                sellTicket();
                } else {
                    System.out.println(Thread.currentThread() + " don't have any ticket left");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
    public  synchronized void sellTicket(){
    if (tickets > 0) {
        System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
        tickets -= 1;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

}

程序将剩余票数大于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,这符合现实逻辑。

public class BuyTicket implements Runnable{

    private static int tickets = 100;
    private Object obj = new Object();
    public void run() {
        while (true) {
            synchronized(BuyTicket.class) {
                if(tickets > 0){
                sellTicket();
                } else {
                    System.out.println(Thread.currentThread() + " don't have any ticket left");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
    public static synchronized void sellTicket(){
    if (tickets > 0) {
        System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
        tickets -= 1;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
}

这段代码将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

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BuyTicket implements Runnable{
    private int tickets = 100;
    private Lock lock = new ReentrantLock();
    public void run() {
        while (true) {
            lock.lock();
                if (tickets > 0) {
                    System.out.println(Thread.currentThread() + " is selling the" + (101 - tickets) + " ticket");
                    tickets -= 1;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    System.out.println(Thread.currentThread() + " don't have any ticket left");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                lock.unlock();
        }
    }
}

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

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

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()唤醒正在等待对象监视器的所有线程。

生产者消费者模式案例

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

public class milkbox {
    private int number;
    private boolean flag = false;
    public synchronized void put(int number){
        if(flag == true){
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        this.number = number;
        System.out.println("There is putting " + number + " milk");
        flag = true;
        notifyAll();
    }

    public synchronized void get(){
        if(flag == false){
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("There is getting " + number + " milk");
        flag = false;
        notifyAll();
    }
}

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

public class producer implements Runnable{
    milkbox mb;
    public producer(){}
    public producer(milkbox mb){
        this.mb = mb;
    }

    public void run(){
        int i;
        for(i = 1;i <= 5;i += 1){
            mb.put(i);
        }
    }
}

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

public class customer implements Runnable{
    milkbox mb;
    public customer(){}
    public customer(milkbox mb){
        this.mb = mb;
    }

    public void run(){
        while(true){
            mb.get();
        }
    }
}

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

public class milktest {
    public static void main(String[] args){
        milkbox mb = new milkbox();
        producer p = new producer(mb);
        customer c = new customer(mb);

        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

这是相关测试代码。

程序的输出是:

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值