目录
学习笔记
生产者与消费者模型
在多线程开发过程中最为著名的是生产者与消费者操作,该操作的主要流程如下:
-
生产者负责信息内容的生产;
-
每当生产者生产完成一项完整的信息之后,消费者要从这里面取走信息;
-
如果生产者没有生产完,消费者要等待它生产完成;
-
如果消费者还没有对信息进行消费,则生产者应该等消费者消费信息完成之后,再继续进行生产。
程序的基本实现
可以将生产者与消费者定义为两个独立的线程类对象,但是对于现在生产的数据,可以使用如下的组成:
-
数据一: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());
}
}