飞道的博客

深入理解java多线程

359人阅读  评论(0)

Java线程

一、线程概述

什么是线程:

线程是操作系统所能进行运算调度的最小单位,是进程中的一个执行流程,一个进程可以运行多个线程。

线程的特性:

线程基本不拥有系统资源,只有一点必不可少的、能保证独立运行的资源,但同一进程中的线程都可以共享该进程的资源。线程拥有自己的堆栈和局部变量,但没有独立的地址空间。一个线程应当包含:一个指向当前正在执行的指令的指针、一个栈、一个用于描述正在执行的线程的处理器的状态值、一个私有的数据区。线程有独立的程序入口、顺序执行序列和程序的出口。一个程序至少有一个进程,一个进程至少有一个线程,该线程被称为主线程(main方法所在的线程)

线程的状态转换:

新建态:线程对象已创建,但还没有调用start方法

就绪态:线程已启动,可以被调度算法选中执行,但还没有被选中,此时线程处在可运行的状态

运行态:线程被调度算法选中获得计算资源,执行相应的操作

阻塞/等待/超时等待:线程因某种原因放弃CPU使用权,暂时停止运行

死亡态:run方法运行结束会让线程进入死亡态,未捕获的异常导致run方法的终止也会让线程进入死亡态

二、线程的创建

在Java中,创建线程有两种方式,一是继承Thread类并覆盖run方法,二是实现Runnable接口。通过继承Thread的方法创建线程,我们只需要之间new一个子类即可,但是如果我们使用实现Runnable接口创建一个新线程,我们想要启动这个线程,依然需要依靠Thread类实现,我们可以把实现了Runnable接口的对象作为参数传入Thread的构造函数,看上去比直接继承Thread创建线程要复杂,但出于Java单继承的特性,有些时候我们的线程类还需要继承其他的类,继承Thread的方法就没有办法使用。

class Thread1 extends Thread{
    @Override
    public void run() {
        super.run();
        for (int i = 0;i < 10;i++){
            System.out.println("Thread1 "+i);
        }
    }
}
class Thread2 implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i < 10;i++){
            System.out.println("Thread2 "+i);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 runnable = new Thread2();
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

当然也可以使用匿名内部类让代码看上去更简洁

public class Test {
    public static void main(String[] args) {
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                super.run();
            }
        };
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
            }
        });
        thread1.start();
        thread2.start();
    }
}

三、线程的启动

线程的启动需要调用start方法,在调用start之前,线程将处于新建态,而调用start后线程将转变为就绪态。要注意的是,run方法是一个极其普通的方法,在线程调用start方法前,run方法和普通方法并没有什么差别,它的执行顺序是固定,只有在调用了start启动线程后,run方法的执行才存在未知性。例如以下代码的输出结果是固定的,无论尝试多少次输出结果都将是thread1 thread2

    public static void main(String[] args) {
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                super.run();
                System.out.println("thread1");
            }
        };
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2");
            }
        });
        thread1.run();
        thread2.run();
    }
thread1
thread2

四、线程优先级

线程是存在优先级的,优先级的范围在1-10之间,默认的优先级是NORM_PRIORITY,这个值是5。如果我们需要自定义线程的优先级,可以使用setPriority(int newPriority)方法,如果参数的范围不在1-10的范围内,编译器不会报错,但运行时会抛出java.lang.IllegalArgumentException异常。线程的优先级是固定的,但线程的执行顺序依然是有一定的随机性的,两个线程之间的优先级越相近,随机性的可能性就越大,但调度算法会尽量将资源让给高优先级的线程。综上,我们在编写多线程的程序时,不应该将线程的优先级作为线程运行顺序的衡量标准,只能将线程优先级作为一种提高程序效率的方法,不可过度依赖线程优先级。

五、线程相关的一些方法

1、sleep()方法

sleep()是Thread的静态方法,作用是强制当前线程休眠,进入阻塞状态,以减缓线程,在休眠时间结束前,线程不会重新返回运行态或就绪态,当休眠时间到达后线程将返回就绪态。若线程在睡眠过程中被打断则会抛出InterruptdedException异常。线程睡眠是让所有线程都获得一定的运行机会的好方法,sleep方法中的参数只是线程不会运行的最短时间,因为线程返回到就绪态后不一定会立刻被调度算法选中。需要注意的是sleep方法并不会释放锁资源。

public class Test {
    public static void main(String[] args) {
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                super.run();
                for (int i = 0;i < 5;i++){
                    System.out.println("Thread1 "+i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i < 5;i++){
                    System.out.println("Thread2 "+i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

运行结果:

Thread1 0
Thread2 0
Thread1 1
Thread2 1
Thread2 2
Thread1 2
Thread2 3
Thread1 3
Thread2 4
Thread1 4

2、yield()方法

yield是一个线程让步的方法,当一个线程调用yield方法后,线程会进入就绪态,以允许优先级相同的线程获得运行的机会,让相同的线程之间能适当的轮换,但要注意的是,yield方法并不能保证将资源让步给其他线程,因为回到就绪态的线程依然有被调度算法选中的可能。

3、join()方法

从jdk源码中我们能找到这样一条注释:“Waits for this thread to die.”,等待这个线程死亡,也就是说当一个线程调用join方法时,创建它的父线程将会挂起等待子线程运行结束才继续运行。例如下面这段程序,在Thread1的run方法中定义一个Thread2,Thread2调用join方法,那么Thread1只有在Thread2运行结束后才会打印出结果,因此这段程序的输出中Thread1一定是在Thread2后面的。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                super.run();
                Thread thread2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0;i < 5;i++){
                            System.out.println("Thread2 "+i);
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
                thread2.start();
                try {
                    thread2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0;i < 5;i++){
                    System.out.println("Thread1 "+i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();
    }
}

运行结果:

Thread2 0
Thread2 1
Thread2 2
Thread2 3
Thread2 4
Thread1 0
Thread1 1
Thread1 2
Thread1 3
Thread1 4

六、线程同步

1、什么是同步问题?

既然一个进程中的线程共享该进程的资源,那么就不可避免的出现这样的情况,进程中有一个变量resource,它的初值为1,线程Thread1想要执行resource=5+1,而Thread2想要执行++resource,两个线程在启动后可能在任意位置中断,那么当两个线程都执行完时,resource的值到底是多少呢?可能是6,也可能是7,这种不确定性会对数据造成破坏,对程序的稳定性与安全性造成影响,显然这是不可接受。

2、锁

如何解决同步问题呢?答案是使用java提供的synchronized关键字。synchronized关键字可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块。

对象锁

在java中每个对象都内置一个锁,当程序运行到某个对象的非静态synchronized方法或代码块时,会自动获得与正在执行代码的类的当前实例(即this指向的对象)有关的锁。锁是互斥的,一个对象只有一个锁,因此当一个线程获得了某个对象的锁后,其他线程就无法获得该对象的锁。当线程退出了synchronized方法或代码块时,也就意味着锁被释放了。需要注意的是,只能利用synchronized关键字同步方法和代码块,而不能同步变量。当线程睡眠时,它所持有的锁不会释放。一个线程可以获得多个锁,例如一个对象的同步方法中调用了气筒对象的同步方法。

对象锁的简单例子:

class Resource implements Runnable{
    int re = 1;
    public synchronized  void add(){
        Thread t = Thread.currentThread();
        System.out.println(t.getName()+"获取了锁");
        for (int i = 0;i < 5;i++) {
            re++;
            System.out.println(t.getName() + "打印了" + re);
        }
        System.out.println(t.getName()+"释放了锁");
    }
    @Override
    public void run() {
        add();
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Resource resource = new Resource();
        Thread thread1 = new Thread(resource,"Thread1");
        Thread thread2 = new Thread(resource,"Thread2");
        thread1.start();
        thread2.start();
    }
}

运行结果:

Thread1获取了锁
Thread1打印了2
Thread1打印了3
Thread1打印了4
Thread1打印了5
Thread1打印了6
Thread1释放了锁
Thread2获取了锁
Thread2打印了7
Thread2打印了8
Thread2打印了9
Thread2打印了10
Thread2打印了11
Thread2释放了锁

当然我们也可以不对方法同步,而是对代码块同步,以下代码效果和上面一样

class Resource implements Runnable{
    int re = 1;
    public void add(){
        Thread t = Thread.currentThread();
        synchronized (this) {
            System.out.println(t.getName() + "获取了锁");
            for (int i = 0; i < 5; i++) {
                re++;
                System.out.println(t.getName() + "打印了" + re);
            }
            System.out.println(t.getName() + "释放了锁");
        }
    }
    @Override
    public void run() {
        add();
    }
}

类锁

类锁是对类的静态方法上锁,类的所有对象共享一个锁,我们也可以理解成是将该类的Class对象上锁。对静态方法加锁,当一个线程访问一个类的静态synchronized方法时,会获得类锁,其他线程将无法访问该静态方法,也无法访问该类的其他静态方法,因为类锁的实质是对class对象加锁,而每个类只有一个class对象。例如下面这段代码,两个线程分别访问两个不同的Resource对象,Resource拥有两个静态同步方法fun1和fun2,在run方法中我们生成一个随机数,如果随机数是偶数,那么该线程将执行fun1方法,若随机数不是偶数,那么该线程将执行fun2方法,我们可以从结果中发现,当两个线程分别执行不同的静态同步方法时,其中一个线程也会因为另一个线程获得了类锁而阻塞。

class Resource implements Runnable {
    static int re = 1;
    public static synchronized void fun1() throws InterruptedException {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + "获取了锁");
        for (int i = 0; i < 10; i++) {
            System.out.println(t.getName() + "打印了" + re);
            Thread.sleep(100);
        }
        System.out.println(t.getName() + "释放了锁");
    }
    public static synchronized void fun2() throws InterruptedException {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + "获取了锁");
        for (int i = 0; i < 10; i++) {
            System.out.println(t.getName() + "打印了" + re);
            Thread.sleep(100);
        }
        System.out.println(t.getName() + "释放了锁");
    }
    @Override
    public void run() {
        Random random = new Random();
        int ran = random.nextInt(100);
        try {
            if (ran%2==0){
                System.out.println("执行了fun1");
                fun1();
            }
            else {
                System.out.println("执行了fun2");
                fun2();
            }
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Resource(), "Thread1");
        Thread thread2 = new Thread(new Resource(), "Thread2");
        thread1.start();
        thread2.start();
    }
}

运行结果:

执行了fun2
执行了fun1
Thread1获取了锁
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1打印了1
Thread1释放了锁
Thread2获取了锁
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2释放了锁

当然上面的静态同步方法也可以替换为下面的形式(对class对象上锁),效果也是一样的

public static void fun1() throws InterruptedException {
    synchronized (Resource.class) {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + "获取了锁");
        for (int i = 0; i < 10; i++) {
            System.out.println(t.getName() + "打印了" + re);
            Thread.sleep(100);
        }
        System.out.println(t.getName() + "释放了锁");
    } }

public static void fun2() throws InterruptedException {
    synchronized (Resource.class) {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + "获取了锁");
        for (int i = 0; i < 10; i++) {
            System.out.println(t.getName() + "打印了" + re);
            Thread.sleep(100);
        }
        System.out.println(t.getName() + "释放了锁");
    }
}

3、阻塞

当一个线程试图获得一个对象的锁,而该锁已经被其他线程持有时。该线程会在该对象上被阻塞,线程需等待持有锁的线程释放锁后。需要注意的是:

  • 每个对象有自己的对象锁,有且仅有一个,调用同一个对象的非静态同步方法的线程彼此会阻塞,而调用不同对象的非静态同步方法则相互之间没有影响
  • 每个类有自己的对象锁,有且仅有一个,嗲用同一个类的静态同步方法(包括不同的静态同步方法)的线程将彼此阻塞
  • 类的静态同步方法与类实例的非静态同步方法之间没有影响,不会相互阻塞,一个线程可以同时拥有类锁和对象锁

4、线程安全

在看集合的相关资料时,经常会看到那个类是线程不安全的,哪个类是线程安全的,那么什么是线程安全呢?当有多个线程访问同一个对象时,若我们作为使用者,不用考虑这些线程在运行时环境下的调度和交替进行,也不需要进行额外的同步操作,在对对象进行任何操作,调用对象的任何行为都能获得正确的结果,那么这个类就是线程安全的。我们简单的可以理解为无论对对象作出什么样的操作,调用方都不用进行额外的同步操作都能保证结果正确,那么就是线程安全的。java中常见的线程安全类:Vector、Stack、HashTable、Enumeration

5、同步的另一种方式——volatile关键字

volatile是一种轻量级的同步机制,在了解volatile之前,我们需要了解一下java内存模型,在Java中所有的变量都存储在主内存中,每条线程拥有自己的工作内存,线程不能直接读写主内存中的变量,只能通过各自的工作内存与主内存交换来完成对变量的操作,也就是说,一个线程想要获取某个主内存中的变量时,会先把它读入其工作内存,同样的,一个线程想要写入一个主内存的变量,需要先修改其工作内存中的变量,再写会主内存,那么当多个线程对一个变量同时操作时,就可能出现一个线程已经在自己的工作内存中修改了变量但还没同步到主内存时另一个线程已经修改了主内存的值。

而当一个变量被volatile修饰后,改变量就不在具备该特性,volatile保证此变量对所有线程的可见性,意思是,当一个线程修改了该变量的值时,其他线程可以立即得知。

volatile赋予变量的第二个特性是禁止对指令重排序。关于这一点目前还没太搞明白,后面有时间重新整理一遍。

七、死锁

解决了线程的同步问题后并不可以高枕无忧,线程的同步还会带来另一个棘手的问题——线程的死锁。当有两个资源A、B,两个线程Thread1、Thread2分别持有A、B,他们的下一步操作分别是获得资源B、A,然而B、A分别都在对方的手上,他们都在等待对方运行结束释放资源,然而不获取对方的资源他们都无法释放自己占有的资源,两个线程在互相等待而无法继续运行,这就形成了死锁。

  • 线程死锁经典案例——哲学家就餐问题

问题描述:假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。

哲学家在拿叉子前并不会告知其他哲学家,拿起叉子后也不会放下叉子,直至他获得了两把叉子并且吃完了东西。那么就会出现这样的情况,每个哲学家都获得了它右手边的叉子,这样会导致每个哲学家都缺少左手边的叉子,都在等待左手边的哲学家放下叉子,这样就陷入了死锁。

如何解决这样的死锁?方法一时添加一个服务生进行管理,所有哲学家只有经过服务生的允许才能拿起叉子;方法二是每个哲学只能拿起0个或2个叉子,即只有当哲学家左右两边的叉子都处于空闲状态才拿起叉子。方法三是对叉子进行编号,规定任意一个哲学家想要用餐时,只能先拿起编号较小的叉子,如果该叉子被占用,则等待。

  • 死锁产生的条件

1、互斥使用,当一个资源被一个线程占用时,其他线程不能使用

2、不可抢占,资源请求者不能强制从资源站有着手中夺取资源,资源只能由占有者主动释放

3、请求和保持,请求者在请求某个资源时仍保持对其他资源的占有

4、循环等待,存在一个循环等待队列

破坏以上任何一个条件都可以让死锁消失

八、Object类中与线程有关的方法

在Object类中也提供了一些与线程相关的方法,用于实现线程间的协作

1、wait()方法

wait方法有三个重载,分别是void wait()、void wait(long millis)和void wait(long millis,int nanos)。wait方法会导致该线程进入等待状态,直至被其他线程以notify或notifyAll唤醒。而wait(long millis)方法中,millis参数表示时间,调用wait方法后线程会进入等待状态,直至指定时间后唤醒。wait(long millis,int nanos)he 和wait(long millis)的区别只是时间精度更高。这些方法必须在synchronized方法或代码块中使用,否则会抛出IllegalMonitorStateException异常。wait方法在调用后,线程会释放持有的锁,并让出CPU资源,我们依然用随机数来控制线程的状态进行实验,当线程获得的随机数是偶数时,调用wait方法,下面的代码表面,当先获得锁的线程thread1调用了wait方法后thread2便获得了锁,由此可以证明,wait方法会释放当前锁。

class Resource implements Runnable {
    static int re = 1;
    public void fun() throws InterruptedException {
        synchronized (this) {
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "获取了锁");
            Random random = new Random();
            int ran = random.nextInt(100);
            System.out.println(t.getName()+"获取了随机数:"+ran);
            if (ran%2==0){
                System.out.println(t.getName()+"调用wait");
                wait();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(t.getName() + "打印了" + re);
                Thread.sleep(100);
            }
            System.out.println(t.getName() + "释放了锁");
        } }
    @Override
    public void run() {
        try {
            fun();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Resource resource = new Resource();
        Thread thread1 = new Thread(resource, "Thread1");
        Thread thread2 = new Thread(resource, "Thread2");
        thread1.start();
        thread2.start();
    }
}

运行结果:

Thread1获取了锁
Thread1获取了随机数:46
Thread1调用wait
Thread2获取了锁
Thread2获取了随机数:81
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2打印了1
Thread2释放了锁

2、notify()/notifyAll()方法

notify/notifyAll用于唤醒正在等待该对象锁的线程。notify会随机唤醒等待某一个资源的线程中的一个,使得该线程退出等待队列进入可运行状态;notifyAll会唤醒所有正在等待某一个资源的线程,使得这些线程退出等待队列,进入可运行状态。

下面的代码中,thread1、thread2和thread3共享lock对象,thread1和thread2的run方法会调用wait方法,使得thread1和thread2在lock上等待,而thread3则会调用notify/notifyAll方法,为了让thread1和thread2在thread3调用唤醒方法前就进入等待状态,我们在thread3方法启动前让主线程睡眠500毫秒。最终实验结果如下,notify只唤醒了thread1,thread2则仍处于等待状态,而notifyAll唤醒了thread1和thread2

class waitThread implements Runnable {

    private Object lock;

    public waitThread(Object lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+"获取了"+lock);
            System.out.println(t.getName()+"调用了wait方法");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(t.getName()+"被唤醒了,等待结束");
        }
    }
}

class notifyThread implements Runnable{

    private Object lock;
    public notifyThread(Object lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+"获取了"+lock);
            System.out.println(t.getName()+"执行操作");
            System.out.println(t.getName()+"唤醒某个线程");
            lock.notify();//lock.notifyAll();
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        String lock = new String("对象锁");
        Thread thread1 = new Thread(new waitThread(lock), "Thread1");
        Thread thread2 = new Thread(new waitThread(lock), "Thread2");
        Thread thread3 = new Thread(new notifyThread(lock),"thread3");
        thread1.start();
        thread2.start();
        Thread.sleep(100);
        thread3.start();
    }
}
//调用notify方法
Thread1获取了对象锁
Thread1调用了wait方法
Thread2获取了对象锁
Thread2调用了wait方法
thread3获取了对象锁
thread3执行操作
thread3唤醒某个线程
Thread1被唤醒了,等待结束
//调用notifyAll方法
Thread1获取了对象锁
Thread1调用了wait方法
Thread2获取了对象锁
Thread2调用了wait方法
thread3获取了对象锁
thread3执行操作
thread3唤醒某个线程
Thread2被唤醒了,等待结束
Thread1被唤醒了,等待结束

4、生产者消费者问题

生产者消费者问题是一个很经典的并发协作问题。问题的详情大概是这样的,有一个生产者,专门生产产品,还有一个消费者会消费生产者生产的产品,生产者生产的商品并不会直接交给消费者消费,而是会存入一个仓库,,消费者需要消费产品时则到仓库中取,当仓库满时,生产者停止生产进入等待并通知消费者消费,当仓库空时,消费者则会停止消费进入等待并通知生产者生产。

显而易见,这里的产品即线程共享的对象,仓库即存放多个对象的区域,而生产者和消费者就是共享资源的进程。当仓库满时,生产者线程会调用wait等待并调用notify唤醒消费者,当仓库空时,消费者会调用wait线程等待并调用notify唤醒生产者。

以下是生产着消费者问题的java代码

class Godown {//仓库类
    public static final int capacity = 5;//最大容量
    private int restCapacity;//剩余容量
    public void setRestCapacity(int restCapacity) {
        this.restCapacity = restCapacity;
    }
    //生产产品
    public synchronized void add() throws InterruptedException {
        Thread t = Thread.currentThread();
        while (restCapacity == 0) {
            System.out.println("仓库已满,"+t.getName()+"进入等待");
            wait();
        }
        restCapacity--;
        System.out.println(t.getName()+"生产了一个产品并放入了仓库,仓库容量为:" + restCapacity);
        notifyAll();
    }
    //消费产品
    public synchronized void use() throws InterruptedException {
        Thread t = Thread.currentThread();
        while (restCapacity == capacity) {
            System.out.println("仓库已空,"+t.getName()+"进入等待");
            wait();
        }
        restCapacity++;
        System.out.println(t.getName()+"消费了一个产品,仓库容量为:" + restCapacity);
        notifyAll();
    }
}
class Productor implements Runnable {
    private Godown godown;
    public Productor(Godown godown) {
        this.godown = godown;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
                godown.add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Customer implements Runnable {
    private Godown godown;
    public Customer(Godown godown) {
        this.godown = godown;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
                godown.use();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Godown godown = new Godown();
        godown.setRestCapacity(Godown.capacity);
        Thread productor1 = new Thread(new Productor(godown), "生产者1");
        Thread productor2 = new Thread(new Productor(godown), "生产者2");
        Thread productor3 = new Thread(new Productor(godown), "生产者3");
        Thread custumor1 = new Thread(new Customer(godown), "消费者1");
        Thread custumor2 = new Thread(new Customer(godown), "消费者2");
        productor1.start();
        productor2.start();
        productor3.start();
        custumor1.start();
        custumor2.start();
    }
}

以上代码的部分运行结果

生产者2生产了一个产品并放入了仓库,仓库容量为:4
消费者2消费了一个产品,仓库容量为:5
仓库已空,消费者1进入等待
生产者3生产了一个产品并放入了仓库,仓库容量为:4
生产者1生产了一个产品并放入了仓库,仓库容量为:3
消费者1消费了一个产品,仓库容量为:4
生产者2生产了一个产品并放入了仓库,仓库容量为:3
生产者1生产了一个产品并放入了仓库,仓库容量为:2
消费者2消费了一个产品,仓库容量为:3
生产者3生产了一个产品并放入了仓库,仓库容量为:2
消费者1消费了一个产品,仓库容量为:3
生产者2生产了一个产品并放入了仓库,仓库容量为:2
消费者2消费了一个产品,仓库容量为:3
生产者1生产了一个产品并放入了仓库,仓库容量为:2
生产者3生产了一个产品并放入了仓库,仓库容量为:1
消费者1消费了一个产品,仓库容量为:2
消费者2消费了一个产品,仓库容量为:3
生产者3生产了一个产品并放入了仓库,仓库容量为:2
生产者2生产了一个产品并放入了仓库,仓库容量为:1
生产者1生产了一个产品并放入了仓库,仓库容量为:0
消费者1消费了一个产品,仓库容量为:1
生产者2生产了一个产品并放入了仓库,仓库容量为:0
仓库已满,生产者1进入等待
消费者2消费了一个产品,仓库容量为:1
生产者3生产了一个产品并放入了仓库,仓库容量为:0
仓库已满,生产者1进入等待
消费者1消费了一个产品,仓库容量为:1
生产者1生产了一个产品并放入了仓库,仓库容量为:0

当然这是最简单的生产者消费者问题,还可以拓展为多个仓库的生产者消费者问题。与线程的协同合作相关的案例除了生产者消费者问题外还有读写问题、理发师问题等。

九、总结

最后对线程做一个小小的总结,对java多线程有了一定了解后,我们再回过头来看一看开始的状态转换图,列出哪些方法会引起哪些状态的转换。线程相关的更深入的知识,包括原子性、重排序和线程池等后面有空再整理了。


转载:https://blog.csdn.net/qq_40401156/article/details/106578039
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场