java多线程-初探(四)

java多线程-初探(三)

本文阐述经典的多线程生产者、消费者模型。

涉及线程等待、唤醒、死锁以及常用的synchronized跟JDK5的Lock接口两种方式的知识点。

生产者、消费者模型初步理解

生产者:负责给资源中保存数据(资源),当资源中的数据被放满了,这时生产者就应该停止继续保存数据。

消费者:负责从资源中取出数据进行消费。当资源中的数据被取完,这时就应该停止取出数据的动作。

生产者在给资源中保存数据的时候,应该需要判断资源中的数据是否已经保存满了。

消费者在从资源中取出数据的时候,应该判断资源中是否有数据。

 

单生产,单消费多线程初步实现

class Main

package com.thread.four;

public class Main {

    public static void main(String[] args) {
        //创建资源类
        Resource resource = new Resource() ;
        //创建生产者
        Producer producer = new Producer(resource) ;
        //创建消费者
        Consumer consumer = new Consumer(resource) ;
        //创建线程类
        Thread producerThread = new Thread(producer) ;//生产者的线程
        Thread consumerThread = new Thread(consumer) ;//消费者的线程
        //启动线程
        producerThread.start();
        consumerThread.start();
    }
}

class Producer

package com.thread.four;

/**
 * 生产者
 * @author w954
 *
 */
public class Producer implements Runnable{

	private Resource resource ;
	
	public Producer(Resource resource){
		this.resource = resource;
	}

	@Override
	public void run(){
		for(int i = 0 ; i <= 100 ; i++){
			resource.setResource("一大碗白米饭");
		}
	}
	
}

class Consumer

package com.thread.four;

/**
 * 消费者
 * @author w954
 *
 */
public class Consumer implements Runnable{

	private Resource resource ;
	
	public Consumer(Resource resource){
		this.resource = resource;
	}

	public void run(){
		for(int i = 0 ; i <= 100 ; i++){
			resource.getResource();
		}
	}
}

class Resource

package com.thread.four;

/**
 * 资源类
 * @author 	w954
 *
 */
public class Resource{

	// 定义资源
	private String resource ;

	// 定义锁
	private static final Object lock = new Object();

	int count = 1;

	/**
	 * 生产者调用的生产资源的方法
	 * @param resourceName	资源名
	 */
	public void setResource(String resourceName){
		synchronized (lock) {
			if(resource == null) {
				resource = resourceName + count++;
				System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
			}
		}
	}

	/**
	 * 消费者调用的消费资源的方法
	 */
	public void getResource(){
		synchronized (lock) {
			if(resource != null) {
				System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
				resource = null;
			}
		}
	}

}

控制台输出

我们想要的结果

一生产一消费,生产者生产完一个资源,消费者就消费一个资源。

并且预计是生产100碗饭,消费也是消费100碗饭。

上述代码可能导致的现象

生产者一直在生产、消费者一直在消费。

导致原因

生产者的cpu一直没有切换到消费者,导致生产者一直在生产饭。

消费者的cpu一直没有切换到生产者,导致消费者一直在消费饭。但是代码判断非空了,所以没有打印。

解决思路

生产者生产的时候判断现在有没有饭,如果有就唤醒消费者先去消费,消费者消费完成之后唤醒生产者去生产饭,以此达到和谐。

关键性代码

class Resource

/**
	 * 生产者调用的生产资源的方法
	 * @param resourceName	资源名
	 */
	public void setResource(String resourceName){
		synchronized (lock) {
			if(resource != null) {
				// 资源不为空,自己等待
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			resource = resourceName + count++;
			System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
			// 生产完成,现在有资源了,唤醒消费者去消费
			lock.notify();
		}
	}

	/**
	 * 消费者调用的消费资源的方法
	 */
	public void getResource(){
		synchronized (lock) {
			if(resource == null) {
				// 没资源了,自己等待
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
			resource = null;
			// 消费完成,现在没有资源了,唤醒生产者去生产
			lock.notify();
		}
	}

以此和谐得不得了。接下来解释一下上述代码。

wait

当线程执行wait方法时,会把当前的锁释放,然后让出CPU,进入等待状态。

notify

当执行notify方法时,会唤醒一个处于等待该对象锁的线程,然后继续往下执行。

解释完上述两个等待唤醒啥意思后,我们来简单读一下代码。

首次当消费者来调用getResource函数消费的时候,发现resource==null。于是消费者调用wait函数,自己等待,把lock这个对象锁释放了。cpu此时就会跑到其他空闲线程上,发现有一个生产者线程,就执行生产者,生产者获取锁进入同步代码块(消费者等待的时候已经把锁释放了),判断资源为空,就去生产资源了,生产完成就调用notify函数,唤醒刚刚等待这个lock对象锁的消费者线程,从刚刚消费者等待那里接着往下执行,消费者就去消费了,消费完又唤醒等待这个对象锁的线程,此时发现没有等待的线程,cpu就随意切换了。切换到哪个都能正常执行。

多生产,多消费实现

所谓多生产多消费就是把生产者弄成多个,消费者也多个。

我们刚刚的代码可以看到生产者就一个,消费者也就一个。

我们先直接更改代码运行看看会出现什么问题。

class Main

package com.thread.four;

public class Main {

    public static void main(String[] args) {
        //创建资源类
        Resource resource = new Resource() ;
        //创建生产者
        Producer producer = new Producer(resource) ;
        //创建消费者
        Consumer consumer = new Consumer(resource) ;
        //创建线程类
        Thread producerThread = new Thread(producer) ;//生产者的线程
        Thread producerThread1 = new Thread(producer) ;//生产者的线程
        Thread consumerThread = new Thread(consumer) ;//消费者的线程
        Thread consumerThread1 = new Thread(consumer) ;//消费者的线程
        //启动线程
        producerThread.start();
        producerThread1.start();
        consumerThread.start();
        consumerThread1.start();
    }
}

出现了消费者消费null的情况。为啥嘞????

导致原因

最根本的原因是因为消费者唤醒了上一个在等待的消费者。

我们来理解一下这句话,假设上一个消费者执行到判断资源的时候,发现没有资源,于是处于等待。

这时cpu该死没切到我们想要的生产者线程上,它跑到了另一个消费者线程上,那这个消费者又来了。发现没资源,又等待。

现在的状态就是两个处于等待的消费者线程,两个空闲的生产者线程。

这时cpu终于切换到了生产者线程上,这时候生产者生产完资源,执行完成该唤醒处于等待这个lock锁的对象的线程(也就是那两个处于等待的消费者其中一个),唤醒某一个消费者之后,这个消费者消费了资源,又接下去又执行唤醒处于等待这个lock锁对象的线程(也就是另一个处于等待的消费者),这时这个消费者醒了,当他去消费时候,这时资源已经被上一个消费者消费了。所以这个消费者就输出null。

初步解决思路

消费者或者生产者唤醒之后,再判断一次是否有商品。这样是否就解决了?

class Resource

/**
	 * 生产者调用的生产资源的方法
	 * @param resourceName	资源名
	 */
	public void setResource(String resourceName){
		synchronized (lock) {
			while (resource != null) {
				// 资源不为空,自己等待
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			resource = resourceName + count++;
			System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
			// 生产完成,现在有资源了,唤醒消费者去消费
			lock.notify();
		}
	}

	/**
	 * 消费者调用的消费资源的方法
	 */
	public void getResource(){
		synchronized (lock) {
			while (resource == null) {
				// 没资源了,自己等待,让生产者先去生产
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
			resource = null;
			// 消费完成,现在没有资源了,唤醒生产者去生产
			lock.notify();
		}
	}

把if的判断改成while,这样确实每次都会判断了。

但是代码出现bug了,执行到一半,程序不动了。这种现象称为死锁现象

上述代码错误原因分析

生产者1生产完资源进入等待的栈内存中,cpu切换到生产者2上,生产者2判断是否有资源,此时有资源,生产者2调用wait函数也进入栈内存等待了。此时两个生产者都在栈内存中等待。

这时cpu切换到消费者1身上,消费者1把资源消费完成之后进入栈内存等待,同时调用notify函数把两个生产者其中一个唤醒了,假设生产者1被唤醒了,但是此时cpu却切换到消费者2身上,消费者2判断没有资源也进入等待的栈内存中。

此时栈内存中等待的分别是生产者2、消费者1、消费者2

这是cpu执行到生产者1,生产者1判断没有资源则生产完资源后也进入栈内存等待,这时调用notify函数唤醒一个等待的线程,这时如果唤醒的是生产者2,那生产者2判断是否有资源,有资源也进入栈内存等待了。

结果就是四个线程全在等待。导致程序无法继续执行。

在JDK1.5之前的解决上述这个死锁的方案

将notify函数改成notifyAll函数。

notity:唤醒一个等待该锁对象的等待线程。

notify:唤醒所有在等待该锁对象的等待线程。

如果不管是生产者还是消费者,唤醒的时候把栈内存中所有的线程都唤醒是不是就能解决?
我们来试试看!

class Main

/**
	 * 生产者调用的生产资源的方法
	 * @param resourceName	资源名
	 */
	public void setResource(String resourceName){
		synchronized (lock) {
			while (resource != null) {
				// 资源不为空,自己等待
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			resource = resourceName + count++;
			System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
			// 生产完成,现在有资源了,唤醒消费者去消费
			lock.notifyAll();
		}
	}

	/**
	 * 消费者调用的消费资源的方法
	 */
	public void getResource(){
		synchronized (lock) {
			while (resource == null) {
				// 没资源了,自己等待,让生产者先去生产
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
			resource = null;
			// 消费完成,现在没有资源了,唤醒生产者去生产
			lock.notifyAll();
		}
	}

至此和谐得不得了!但是如果这么好用,也不会出现后来JDK5中的Lock接口了。

上述代码的弊端

唤醒notifyAll函数,必然会唤醒跟自己一样的线程,唤醒没问题,没bug,但是效率低啊!

如生产者把生产者也给唤醒了,消费者把消费者也给唤醒了。

如果最终能实现生产者只唤醒消费者,而消费者只唤醒生产者,那岂不是万物和谐了?

JDK5中的Lock接口跟Condition接口实现多生产多消费方式

Lock

在synchronized上的锁是开发人员自己定义的任意锁对象,而在jdk5中专门提供Lock接口来表示同步中的锁对象。

它代替了同步和同步中的锁,将synchronized代码块获取跟释放锁的操作指定到具体的方法中。

如:使用Lock接口获取锁就要手动调用lock()函数获取锁,释放锁则调用unlock()函数释放。

Condition

代替原有的wait、notify、notifyAll等待唤醒机制。

通常获取condition对象采用lock.newCondition;获得。

每new一个condition可以绑定一个等待唤醒线程。如题就是两个condition。一个生产者一个消费者。

lock跟condition关系图

以此来达到生产者唤醒消费者,消费者唤醒生产者。(调用对方的唤醒函数)

class Resource

package com.thread.four;

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

/**
 * 采用JDK1.5的Lock接口实现方式
 */
public class JdkLockResource {

    // 定义资源
    private String resource ;

    // 定义锁
    private static final Lock lock = new ReentrantLock();

    // 定义生产者线程
    private static final Condition producer = lock.newCondition();

    // 定义消费者线程
    private static final Condition consumer = lock.newCondition();

    int count = 0;

    /**
     * 生产者调用的生产资源的方法
     * @param resourceName	资源名
     */
    public void setResource(String resourceName){
        // 获取锁
        lock.lock();
        try {
            while (resource != null){
                // 生产者等待消费者消费
                try {
                    producer.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            resource = resourceName + count++;
            System.out.println(Thread.currentThread().getName() + "--生产者--已生产:" + resource);
            // 唤醒消费者消费
            consumer.signal();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 消费者调用的消费资源的方法
     */
    public void getResource(){
        // 获取锁
        lock.lock();
        try {
            while (resource != null){
                // 消费者等待生产者生产
                try {
                    consumer.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
            // 模拟消费,此时应将资源置空
            resource = null;
            // 消费完成。唤醒生产者线程进行生产资源	唤醒一条生产者线程
            producer.signal();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

}

简要说明

释放锁因为要手动调用unlock函数,如抛异常,则锁无法释放,所以必须要在finally处添加释放锁逻辑。

Condition对象是从lock锁对象中获取监视具体的线程的,所以可以用他来管理生产者跟消费者的等待唤醒。

那业务开发中是应该都用jdk5的Lock接口吗?

不一定!

个人建议:如果不需要等待唤醒机制,只是操作同一个数据,则用synchronized更为简洁方便。

但如果涉及到多个线程操作同一组数据,如买票跟添加票的业务场景,或者商品上架数量跟抢购的业务场景,则用jdk5的Lock接口实现等待唤醒机制效率更高

猜你喜欢

转载自blog.csdn.net/wkh___/article/details/84835991