Java多线程编程(同步、死锁、生产消费者问题)
Java多线程编程(同步、死锁、生产消费):
关于线程同步以及死锁问题:
线程同步概念:是指若干个线程对象并行进行资源的访问时实现的资源处理保护操作;
线程死锁概念:是指两个线程都在等待对方先完成,造成程序的停止的状态;
先了解相应的概念,后面深入理解。
同步:
举个例子:还是卖票问题(经典️)
不存在同步
开启三个线程(售票员)测试
package com.xbhog;
class MyThread implements Runnable {// 定义线程执行类
private int ticket = 3;// 总票数为6张
@Override
public void run() {
while (true) { // 持续卖票
if (this.ticket > 0) { // 还有剩余票
try {
Thread.sleep(100); // 模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程的名字
System.out.println(Thread.currentThread().getName() +
"卖票,ticket = " + this.ticket--);
} else {
System.out.println("***** 票已经卖光了 *****");
break;// 跳出循环
}
}
}
}
public class Java多线程核心 {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "售票员A").start(); // 开启卖票线程
new Thread(mt, "售票员B").start(); // 开启卖票线程
new Thread(mt, "售票员C").start(); // 开启卖票线程
}
}
结果:
第一次随机运行: | 第二次随机运行: |
---|---|
售票员B卖票,ticket = 2 售票员C卖票,ticket = 3 售票员A卖票,ticket = 3 售票员A卖票,ticket = 1 售票员B卖票,ticket = -1 * 票已经卖光了 * 售票员C卖票,ticket = 0 * 票已经卖光了 * * 票已经卖光了 * | 售票员B卖票,ticket = 1 * 票已经卖光了 * 售票员A卖票,ticket = 3 * 票已经卖光了 * 售票员C卖票,ticket = 2 * 票已经卖光了 * |
存在上述原因是因为在代码中两个地方存在多线程访问时出现模糊的问题:
this.ticket>0;
this,ticket--;
假设现在剩余的票数为1张;当第一个线程满足售票的条件的时候(此时还未减少票数),其他的线程也可能同时满足售票的条件,这样同时进行自减减就可能造成负数!
解决上述问题就需要采用线程同步技术实现;
首先需要明确,在Java中实现线程同步(synchronized)的方法有两个:
同步代码块(同步策略加在方法内部)
package com.xbhog.多线程1;
class MyThread implements Runnable { // 定义线程执行类
private int ticket = 3; // 总票数为6张
@Override
public void run() {
while (true) { // 持续卖票
synchronized(this) { // 同步代码块
if (this.ticket > 0) { // 还有剩余票
try {
Thread.sleep(100); // 模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"卖票,ticket = " + this.ticket--);
} else {
System.out.println("***** 票已经卖光了 *****");
break; // 跳出循环
}
}
}
}
}
public class Java多线程同步代码块 {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt, "售票员A").start(); // 开启卖票线程
new Thread(mt, "售票员B").start(); // 开启卖票线程
new Thread(mt, "售票员C").start(); // 开启卖票线程
}
}售票员A卖票,ticket = 3
售票员C卖票,ticket = 2
售票员B卖票,ticket = 1
***** 票已经卖光了 *****
***** 票已经卖光了 *****
***** 票已经卖光了 *****同步方法(同步策略加在方法上)
class MyThread implements Runnable { // 定义线程执行类
private int ticket = 3; // 总票数为6张
@Override
public void run() {
while (this.sale()) { // 调用同步方法
;
}
}
public synchronized boolean sale() { // 售票操作
if (this.ticket > 0) {
try {
Thread.sleep(100); // 模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"卖票,ticket = " + this.ticket--);
return true;
} else {
System.out.println("***** 票已经卖光了 *****");
return false;
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "售票员A").start(); // 开启卖票线程
new Thread(mt, "售票员B").start(); // 开启卖票线程
new Thread(mt, "售票员C").start(); // 开启卖票线程
}
}售票员A卖票,ticket = 3
售票员C卖票,ticket = 2
售票员B卖票,ticket = 1
***** 票已经卖光了 *****
***** 票已经卖光了 *****
***** 票已经卖光了 *****
同步的本质:在同一个时间段只允许有一个线程执行资源,所以在此线程对象未执行完的过程中其他线程对象将处于等待的状态。
同步的优点与缺点:
可以保证数据的准确性
数据线程的访问安全
程序的处理性能下降
死锁:
实例:
假如现在又张三想要李四的画,李四想要张三的书,那么张三对李四说:把你的画给我,我就给你书;
李四对张三说:把你的书给我,我就给你画;
此时:张三在等待李四,李四在等待张三,两人一直等待下去形成死锁;
观察线程的死锁:(实现张三李四)
package com.xbhog.死锁;
class Book {
public synchronized void tell(Painting paint) { // 同步方法
System.out.println("张三对李四说:把你的画给我,我就给你书,不给画不给书!");
paint.get();
}
public synchronized void get() { // 同步方法
System.out.println("张三得到了李四的画开始认真欣赏。");
}
}
class Painting {
public synchronized void tell(Book book) { // 同步方法
System.out.println("李四对张三说:把你的书给我,我就给你画,不给书不给画!");
book.get();
}
public synchronized void get() { // 同步方法
System.out.println("李四得到了张三的书开始认真阅读。");
}
}
public class DeadLock implements Runnable{
private Book book = new Book();
private Painting paint = new Painting();
public DeadLock() {
new Thread(this).start();
book.tell(paint);
}
@Override
public void run() {
paint.tell(book);
}
public static void main(String[] args) {
new DeadLock() ;
}
}
由于现在电脑的配置问题,该代码有可能在一次运行中展示不出效果来,需要多次运行观察效果;
效果图:
由此引申出了生产者与消费者模型。
生产者与消费者问题:
首先需要明确生产者与消费者为两个线程对象,是对同一资源进行数据的保存与读取;
基本操作是:生产者生产一个资源,消费者则取走一个资源,一一对应。
对应类关系图:
我们需要设想一个问题,如果不加任何操作的话,会出现什么问题?
数据错位:当生产者线程只是开辟了一个栈空间保存信息名称,在想存数据但是还没存数据的时候切换到了消费者线程上,那么消费者线程将会把这个信息名称与上个信息的内容进行结合联系,这样就造成了数据的错位。
重复数据:当生产者放了若干次的数据,消费者才开始取数据,或者消费者取完,但生产者还没生产新数据时又取了直接已经取过得数据。
解决以上两个问题需要涉及到以下两个知识点:
设置同步代码块或设置同步方法>>>解决数据错误问题
Object线程等待与唤醒>>>解决数据重复设置以及重复取出的问题
增加数据同步方法或同步代码块:
在本程序中,生产者与消费者代表的都是线程对象,所以同步操作只能在Message类中,可以将set与get方法设置为单独的同步方法。
class Message {
private String title ; // 保存信息的标题
private String content ; // 保存信息的内容
public synchronized void set(String title, String content) {
this.title = title;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.content = content;
}
public synchronized String get() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.title + " --> " + this.content;
}
// setter、getter略
}
class Producer implements Runnable { // 定义生产者
private Message msg = null ;
public Producer(Message msg) {
this.msg = msg ;
}
@Override
public void run() {
for (int x = 0; x < 50; x++) { // 生产50次数据
if (x % 2 == 0) {
this.msg.set("xbhog","22") ; // 设置属性
} else {
this.msg.set("xbhog","www.cnblog.cn/xbhog") ; // 设置属性
}
}
}
}
class Consumer implements Runnable { // 定义消费者
private Message msg = null ;
public Consumer (Message msg) {
this.msg = msg ;
}
@Override
public void run() {
for (int x = 0; x < 50; x++) { // 取走50次数据
System.out.println(this.msg.get()); // 取得属性
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Message msg = new Message() ; // 定义Message对象,用于保存和取出数据
new Thread(new Producer(msg)).start() ; // 启动生产者线程
new Thread(new Consumer(msg)).start() ; // 取得消费者线程
}
}
Object线程等待与唤醒机制:
线程的等待与唤醒只能依靠Object来完成,如果想要让生产者与消费者一个一个拿,一个一个取,那么需要加入标志位来确定线程的当前状态;
由图所示:
当生产者线程与消费者线程进入时,判断当前的标志位是否为true,
true:表示生产者可以生产资源,但是消费者不能取走资源
false:表示生产者不能生产资源,但是消费者需要取走资源
class Message {
private String title ;
private String content ;
private boolean flag = true; // 表示生产或消费的形式
// flag = true:允许生产,但是不允许消费
// flag = false:允许消费,不允许生产
public synchronized void set(String title,String content) {
if (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() {
if (this.flag == true) { // 还未生产,需要等待
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return this.title + " - " + this.content ;
} finally { // 不管如何都要执行
this.flag = true ; // 继续生产
super.notify(); // 唤醒等待线程
}
}
}
在本程序中追加一个数据产生与消费的控制逻辑成员属性,通过此程序的值控制实现线程的等待与唤醒处理操作,从而解决线程重复操作的问题。
Java多线程编程(同步、死锁、生产消费者问题)的更多相关文章
- Java 多线程 -- 协作模型:生产消费者实现方式一:管程法
多线程通过管程法实现生产消费者模式需要借助中间容器作为换从区,还包括生产者.消费者.下面以蒸馒头为列,写一个demo. 中间容器: 为了防止数据错乱,还需要给生产和消费方法加锁 并且生产者在容器写满的 ...
- Java 多线程 -- 协作模型:生产消费者实现方式二:信号灯法
使用信号灯法实现生产消费者模式需要借助标志位. 下面以演员表演,观众观看电视为列,写一个demo 同一资源 电视: //同一资源 电视 class Tv { String voice; // 信号灯 ...
- Java多线程编程(4)--线程同步机制
一.锁 1.锁的概念 线程安全问题的产生是因为多个线程并发访问共享数据造成的,如果能将多个线程对共享数据的并发访问改为串行访问,即一个共享数据同一时刻只能被一个线程访问,就可以避免线程安全问题.锁 ...
- Java多线程编程——进阶篇二
一.线程的交互 a.线程交互的基础知识 线程交互知识点需要从java.lang.Object的类的三个方法来学习: void notify() 唤醒在此对象监视器上等待的单个 ...
- Java多线程编程详解
转自:http://programming.iteye.com/blog/158568 线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Ja ...
- Java多线程编程核心技术
Java多线程编程核心技术 这本书有利于对Java多线程API的理解,但不容易从中总结规律. JDK文档 1. Thread类 部分源码: public class Thread implements ...
- Java多线程编程核心技术(三)多线程通信
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时 ...
- 《Java多线程编程核心技术》知识梳理
<Java多线程编程核心技术> @author ergwang https://www.cnblogs.com/ergwang/ 文章末尾附pdf和png下载链接 第1章 Java多线程技 ...
- Java多线程编程总结(精华)
Java多线程编程总结 2007-05-17 11:21:59 标签:多线程 java 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http ...
随机推荐
- 纯CSS实现内容放大缩小效果
先搭架子 再实现第一个内容 填充更多内容 拆掉border,查看最终效果 html代码 <!-- 服务 --> <div class="service"> ...
- js获取数字数组最大值的几种方式
原生Math.max方法 Math.max 方法不能接收数组,可以使用ES6的...将数组打散 const arr = [111, 12, 111, 34, 2, 5, 76]; console.lo ...
- 链接服务器sql语句
EXEC sp_addlinkedserver @server='sha',--被访问的服务器别名 @srvproduct='', @provider='SQLOL ...
- python进阶(3)序列化与反序列化
序列化与反序列化 按照某种规则,把内存中的数据保存到文件中,文件是一个字节序列,所以必须要把内存数据转换成为字节序列,输出到文件,这就是序列化:反之,从文件的字节恢复到内存,就是反序列化: pytho ...
- 小白养成记——MySQL中的排名函数
1.ROW_NUMBER() 函数 依次排序,没有并列名次.如 SELECT st.ID '学号', st.`NAME` '姓名', sc.SCORE '成绩', ROW_NUMBER() OVER( ...
- Hyperf-事件机制+异常处理
Hyperf-事件机制+异常处理 标签(空格分隔): php, hyperf 异常处理器 在 Hyperf 里,业务代码都运行在 Worker 进程 上,也就意味着一旦任意一个请求的业务存在没有捕获处 ...
- wxWidgets源码分析(3) - 消息映射表
目录 消息映射表 静态消息映射表 静态消息映射表处理过程 动态消息映射表 动态消息映射表处理过程 消息映射表 消息是GUI程序的核心,所有的操作行为均通过消息传递. 静态消息映射表 使用静态Event ...
- APP跳转小程序,小程序跳转APP
关注公共号,搜索 "APP跳转小程序,小程序跳转APP",查看原文 前置条件: 开发环境:windows 开发框架:uni-app , H5+,nativeJS,mpvue 编辑器 ...
- Node更丝滑的打开方式
Node更丝滑的打开方式 1. 使用背景 最近前端的一个项目,使用gulp作为工程化.在运行过程中出现如下错误 gulp[3192]: src\node_contextify.cc:628: Asse ...
- C++的指针,引用,指向指针的引用和Java中的引用
#include <iostream> #include<algorithm> using namespace std; class Test { public: Test(i ...