多线程二:生产者与消费者、深入分析多线程、多线程案例 - Java高级特性2

目录

生产者与消费者模型

程序的基本实现

解决数据同步

线程的等待与唤醒

多线程的深入分析

停止线程

后台守护线程

volatile关键字

多线程案例

数字加减

生产电脑

竞拍抢答


学习笔记

生产者与消费者模型

在多线程开发过程中最为著名的是生产者与消费者操作,该操作的主要流程如下:

  • 生产者负责信息内容的生产;

  • 每当生产者生产完成一项完整的信息之后,消费者要从这里面取走信息;

  • 如果生产者没有生产完,消费者要等待它生产完成;

  • 如果消费者还没有对信息进行消费,则生产者应该等消费者消费信息完成之后,再继续进行生产。

程序的基本实现

可以将生产者与消费者定义为两个独立的线程类对象,但是对于现在生产的数据,可以使用如下的组成:

  • 数据一:title = 张三、content = 宇宙大帅哥;

  • 数据二:title = 李四、content = 猥琐第一人;

既然生产者与消费者是两个独立的线程,那么这两个独立的线程之间就应该有一个数据保存的集中点,那么可以单独定义一个Message类实现数据的保存。

范例:实现程序基本结构

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Message msg = new Message() ;
		new Thread(new Producer(msg)).start() ; // 启动生产者线程
		new Thread(new Consumer(msg)).start() ; // 启动消费者线程
	}
}
class Producer implements Runnable {
	private Message msg ;
	public Producer(Message msg) {
		this.msg = msg ;
	}
	
	@Override
	public void run() {
		for (int x = 0; x < 100; x ++ ) { // 生产100组数据
			if(x%2 == 0) {
				this.msg.setTitle("张三");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.msg.setContent("宇宙大帅哥");
			} else {
				this.msg.setTitle("李四");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				this.msg.setContent("猥琐第一人");
			}
			
		}
	}
}

class Consumer implements Runnable {
	private Message msg ;
	public Consumer(Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for (int x = 0; x < 100; x ++) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(this.msg.getTitle() + "-" + this.msg.getContent() );
		}
	}
}


class Message {
	private String title ;
	private String content ;
	
	public void setContent(String content) {
		this.content = content;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getContent() {
		return content;
	}
	public String getTitle() {
		return title;
	}
	
}

通过整个代码的执行你会发现此时有两个问题:

问题一:数据不同步;

问题二:应该是生产一个取走一个,但是发现有重复生产和重复取出的问题

解决数据同步

如果要解决问题,首先要解决的是数据同步的问题,如果要想解决数据同步,最简单的做法就是使用synchronized关键字定义同步代码块或同步方法,于是这个时候对于同步的处理可以直接在Message中完成。

范例:解决同步操作

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Message msg = new Message() ;
		new Thread(new Producer(msg)).start() ; // 启动生产者线程
		new Thread(new Consumer(msg)).start() ; // 启动消费者线程
	}
}
class Producer implements Runnable {
	private Message msg ;
	public Producer(Message msg) {
		this.msg = msg ;
	}
	
	@Override
	public void run() {
		for (int x = 0; x < 100; x ++ ) { // 生产100组数据
			if(x%2 == 0) {
				this.msg.set("张三", "宇宙大帅哥" );
			} else {
				this.msg.set("李四", "猥琐第一人");
			}
			
		}
	}
}

class Consumer implements Runnable {
	private Message msg ;
	public Consumer(Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for (int x = 0; x < 100; x ++) {
			System.out.println(this.msg.get() );
		}
	}
}



class Message {
	private String title ;
	private String content ;
	
	public synchronized void set(String title, String content) {
		this.title = title ;
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.content = content ;
	}
	public synchronized String get() {
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return this.title + "-" + this.content ;
	}  
}

在进行同步处理的时候肯定需要一个同步的处理对象,那么此时肯定要将同步操作交由Message类处理是最合适的。此时发现数据已经可以正常保持一致了,但是对于重复操作的问题依然存在。

线程的等待与唤醒

如果现在要想解决生产者消费者问题,那么最好的解决方案就是使用等待与唤醒操作机制,等待和唤醒的机制主要依靠Object类中提供的方法处理的:

  • 等待机制:

      |- 死等:public final void wait​() throws InterruptedException

      |- 设置等待时间:public final void wait​(long timeout) throws InterruptedException

      |- 设置等待时间:public final void wait​(long timeout, int nanos) throws InterruptedException

  • 唤醒:

     |- 唤醒第一个等待线程:public final void notify​()

     |- 唤醒全部等待线程:public final void notifyAll​()

如果此时有若干个等待线程的时候,那么notify()表示的是唤醒第一个等待的,而其它的线程继续等待,而notifyAll表示会唤醒所有等待的线程,那个线程的优先级高就有可能先执行。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。

对于当前的问题主要的解决应该通过Message类完成处理。

范例:修改Message类

package cn.ren.demo;

public class ThreadDemo {
	public static void main(String[] args) throws Exception{
		Message msg = new Message() ;
		new Thread(new Producer(msg)).start() ; // 启动生产者线程
		new Thread(new Consumer(msg)).start() ; // 启动消费者线程
	}
}

class Message {
	private String title ;
	private String content ;
	private boolean flag = true; // 表示可以取走或者放入的形式,设置为true表示最开始先生产
	// flag = true 表示允许生产放入,但不允许取走消费
	// flag = false 表示允许取走消费,不允许生产放入 
	public synchronized void set(String title, String content) { // 生产放入
		while (this.flag == false) { // 无法生产,等待消费
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.title = title ;
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.content = content ;
		this.flag = false ; // 已经生产放入过了
		super.notify();  // 唤醒等待的线程
	}
	 
	public synchronized String get() { // 取走消费
		while (this.flag == true) { // 还未生产放入,需要等待
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try { // 注意这里,返回后还能执行finally
			return this.title + "-" + this.content ;
			} finally { // 不管如何都要执行
				this.flag = true ; // 继续生产
				super.notify() ;
			}
	} 
}



class Producer implements Runnable {
	private Message msg ;
	public Producer(Message msg) {
		this.msg = msg ;
	}
	
	@Override
	public void run() {
		for (int x = 0; x < 100; x ++ ) { // 生产100组数据
			if(x%2 == 0) {
				this.msg.set("张三", "宇宙大帅哥" );
			} else {
				this.msg.set("李四", "猥琐第一人");
			}
			
		}
	}
}

class Consumer implements Runnable {
	private Message msg ;
	public Consumer(Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for (int x = 0; x < 100; x ++) { // 消费
			System.out.println(this.msg.get() );
		}
	}
}

这种处理形式就是在进行多线程开发最原始的处理方案,整个的等待、同步、唤醒机制都由开发者通过原生代码自行实现控制。

多线程的深入分析

停止线程

在多线程的操作之中,如果要启动线程使用的是Thread类中的start()方法,而如果对于多线程需要进行停止处理,Thread类原本提供由stop()方法,但是对于这些方法从JDK1.2版本开始就已经开始废除了,而知道现在不建议出现在代码之中,而除了stop()之外还有几个方法也被禁用了:destory()、suspend()、resume()。

之所以废除这些方法,主要的原因是因为这些方法有可能导致线程的死锁,所以从JDK1.2开始,就都不建议使用。如果要想进行线程的停止需要通过一种柔和的方式进行。

范例:实现线程柔和的停止

package cn.ren.demo;

public class ThreadDemo {
	public static boolean flag = true ;
	public static void main(String[] args) throws InterruptedException {
		new Thread(()-> {
			long num = 0 ;
			while(flag) {
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "、正在运行。num = " + num ++);
			}
		}, "执行线程" ).start() ;
		Thread.sleep(200) ; // 运行200毫秒
		flag = false ; // 停止线程
	}
}

万一现在有其它线程去控制这个flag的内容,那么这个时候对于线程的停止也不是说停就立刻停止的,而是会在执行中判断flag的内容来完成。注意一定要记得存在主线程。

后台守护线程

现在假设有一个人并且这个人有一个保镖,那么这个保镖一定在这个人活着的时候进行守护。所以在多线程里面可以进行守护线程的定义,也就是说如果现在主线程的程序或者其它的线程还在执行的时候,那么守护线程将一直存在,并且运行在后台的状态。

在Thread类里面提供有如下的守护线程的方法:

  • 设置为守护线程:public final void setDaemon​(boolean on)

  • 判断是否为守护线程:public final boolean isDaemon​()

范例:使用守护线程

package cn.ren.demo;

public class ThreadDemo {

	public static void main(String[] args) throws InterruptedException {
		Thread userThread = new Thread(()-> {
			for (int x = 0; x < 10; x ++) {
				try {
					Thread.sleep(100) ;
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "、正在运行.x= " + x);
			}
		}, "用户线程" ) ; // 完成核心业务
		Thread daemonThread = new Thread(()-> {
			for (int x = 0; x < Integer.MAX_VALUE; x ++) {
				try {
					Thread.sleep(100) ;
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "、正在运行.x= " + x);
			}
		}, "守护线程" ) ; 
		daemonThread.setDaemon(true);
		userThread.start() ;
		daemonThread.start() ;
		
	}
}

可以发现所有的守护线程都是围绕在用户线程的周围,如果程序执行完毕了,守护线程也就消失了,在整个JVM里面最大的守护线程就是GC线程。

程序执行中GC线程会一直存在,如果程序执行完毕,GC线程也将消失。

volatile关键字

在多线程的定义中,volatile关键字主要在属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理,在一些书上就将其错误的理解为同步属性了。

在正常进行变量处理的时候往往会经历如下的几个步骤:

  • 获取变量原有的数据内容副本;

  • 利用副本为变量进行数学计算;

  • 将计算后的变量,保存到原始空间之中;

而如果一个属性上追加volatile关键字,表示不使用副本,而是直接操作原始变量,相当于节约了:拷贝副本与重新保存的步骤。

package cn.ren.demo;

class MyThread implements Runnable {
	private int ticket = 5 ;
	@Override
	public void run() {
		synchronized (this) {
			while(this.ticket > 0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + ": 买票处理.ticket = " + this.ticket --);
			}
		}
	}
}


public class ThreadDemo {

	public static void main(String[] args) throws InterruptedException {
		MyThread mt = new MyThread() ;
		new Thread(mt, "票贩子A").start();
		new Thread(mt, "票贩子B").start();
		new Thread(mt, "票贩子C").start();
		
	}
}

面试题 :请解释volatile与synchronized的区别?

  • volatile主要在属性上使用,而sysnchronized是在代码块或方法上使用;

  • volatile无法描述同步的处理,而是直接内存的处理,它只是一种直接内存的处理,避免了数据的拷贝和重新写入保存的操作。

  • synchronized是实现同步的。

多线程案例

数字加减

设计4个线程对象,两个线程执行减,两个线程执行加;

package cn.ren.demo;

class Resource {
	private int num = 0 ; // 进行加减操作的数据
	private boolean flag = false ; // 先执行加操作
	// flag = true 表示进行加操作但无法进行减操作;
	// flag = false 表示进行减操作但无法进行加操作 InterruptedException
	
	public synchronized void add () throws Exception { // 执行加法操作
		while (this.flag == false) { // 现在在执行减法操作,加法等待  // 如果不设置等待,有可能会一直执行该操作,即重复问题
				super.wait();    // 使用Object中的方法 //
		} // 注意try...catch 与  throws 只能使用一种,否则程序会出错,原因未知
		Thread.sleep(100);
		this.num ++ ;
		System.out.println("【加法操作-" + Thread.currentThread().getName() + "】 num = " + this.num + this.flag);
		this.flag = false ; // 加法草走执行完毕,需要执行减法操作
		super.notifyAll();
	}
	
	
	public synchronized void sub() throws Exception {
		while (this.flag == true) {
				super.wait();
		} 
		Thread.sleep(200);
		this.num -- ;
		System.out.println("【减法操作-" + Thread.currentThread().getName() + "】 num = " + this.num+ this.flag);
		this.flag = true ;
		super.notifyAll();
	}
}


class AddThread implements Runnable {
	private Resource resource ; // 资源
	public AddThread(Resource resource) {
		this.resource = resource ;
	}
	
	@Override
	public void run() {
		for (int x = 0 ; x < 10; x ++) {
			try {
				this.resource.add();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}
}

class SubThread implements Runnable {
	private Resource resource ; // 资源
	public SubThread(Resource resource) {
		this.resource = resource ;
	}
	@Override
	public void run() {
		for (int x = 0 ; x < 10; x ++) {
			try {
				this.resource.sub();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}
}


public class ThreadDemo {

	public static void main(String[] args) throws InterruptedException {
		Resource res = new Resource() ;
		AddThread at = new AddThread(res) ;
		SubThread st = new SubThread(res) ;
		new Thread(at, "加法线程-A").start(); 
		new Thread(at, "加法线程-B").start(); 
		new Thread(st, "减法线程-X").start(); 
		new Thread(st, "减法线程-Y").start();
		
	}
}

调试的时候,一定要加延迟,没有延迟就是耍流氓。

注意:在多线程编程中,if和wile是由区别的,被唤醒的线程是从wait之后的代码开始执行的。所以,if判断不会再执行,而while则会再一次执行判断。即使用if,可能会出现-1等情况,-1表示,多个减法操作进入等待而后被唤醒;

当某个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去了对象的锁功能,使得其他线程可以访问该对象。

生产电脑

设计一个生产电脑和搬运电脑的类,要求生产一台就搬运走一台,如果没有新的电脑生产出来,则搬运工要等待电脑生产;如果生产出的电脑没有被搬走,则要等待电脑搬走后再生产,并统计出生产电脑的数量。

标准的生产者和消费者模型。

package cn.ren.demo;
public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException {
		Resource res = new Resource() ;
		new Thread(new Producer(res)).start() ;
		new Thread(new Consumer(res)).start() ;
		
	}
}
class Producer implements Runnable {
	private Resource resource ;
	public Producer (Resource resource) {
		this.resource = resource ;
	}
	
	@Override
	public void run() {
		for (int x = 0; x < 50; x++) {
			try {
				this.resource.make();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

class Consumer implements Runnable {
	private Resource resource ;
	public Consumer (Resource resource) {
		this.resource = resource ;
	}
	
	@Override
	public void run() {
		for (int x = 0; x < 50; x++) {
			try {
				this.resource.get();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

class Computer {
	private static int count = 0 ;  // 表示生产的个数
	private String name ;
	private double price ;
	
	public Computer(String name, double price) {
		this.name = name ;
		this.price = price ;
		count ++ ;
	}
	
	public String toString() {
		return"【第" + count + "台电脑】" + "名字:" + this.name + "、价格:"  + this.price ;
	}	
}

class Resource {
	private Computer computer ;
	// flag = true 表示生产,没东西搬走
	
	public synchronized void make () throws Exception {
		while (this.computer != null) {
			super.wait();
		}
		Thread.sleep(100);
		this.computer = new Computer("57", 10.00) ;
		System.out.println("【生产电脑】" + this.computer);
		super.notifyAll() ;
	}
	public synchronized void get () throws Exception {
		while (this.computer == null) {
			super.wait();
		}
		Thread.sleep(100);
		System.out.println("【取走电脑】" + this.computer);
		this.computer = null ;
		super.notifyAll();
		
	}
	
}



竞拍抢答

实现一个竞拍抢答程序:有三个抢答者,同时发出抢答指令抢答成功给出成功提示,未抢答成功给出失败提示。

对于这个多线程的操作,由于里面牵扯到数据的返回问题,那么做好使用callable接口实现返回提示。

package cn.ren.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyThread implements Callable<String> {
	private boolean flag = false ; // 抢答处理
	
	@Override
	public String call() throws Exception {
		synchronized (this) { // 数据同步
			if (this.flag == false) { // 抢答成功
				this.flag = true ;
				return Thread.currentThread().getName() + "抢答成功" ;
			} else {
				return Thread.currentThread().getName() + "抢答失败" ;
			}
		}
	}
}


public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		MyThread mt = new MyThread() ;
		FutureTask<String> taskA = new FutureTask(mt) ;
		FutureTask<String> taskB = new FutureTask(mt) ;
		FutureTask<String> taskC = new FutureTask(mt) ;
		new Thread(taskA, "竞赛者A").start() ;
		new Thread(taskB, "竞赛者B").start() ;
		new Thread(taskC, "竞赛者C").start() ;
		System.out.println(taskA.get());
		System.out.println(taskB.get());
		System.out.println(taskC.get());
	}
}



 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值