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 ...
随机推荐
- 不能回滚的Redis事务还能用吗
前言 事务是关系型数据库的特征之一,那么作为 Nosql 的代表 Redis 中有事务吗?如果有,那么 Redis 当中的事务又是否具备关系型数据库的 ACID 四大特性呢? Redis 有事务吗 这 ...
- Vulhun-y0usef靶机通关
Vulhub-y0sef靶机通关 安装靶机环境,下载地址:https://www.vulnhub.com/entry/y0usef-1,624/ 网络模式:桥接 目标:user.txt和root.tx ...
- Java流程控制:循环结构
一.简介 顺序结构的程序语句只能被执行一次,如果您想要同样的操作执行多次,就需要使用循环结构. Java中有三种主要的循环结构: 'while'循环 'do...while'循环 'for'循环 在J ...
- DRF简介/接口概念
目录 一.drf框架简介 1. drf安装 2. drf的优势 二.接口 1. 接口的概念 2. 接口文档 3. 接口规范(restful) 3.1 url链接规范 3.2 请求方式规范 3.3 响应 ...
- C++构造函数的选择
构造函数用来初始化类对象.构造函数有好几种,在编程时如何选择不同的构造函数呢?做个总结 一.默认构造函数 默认构造函数是在程序员没有声明任何构造函数的时,编译器为了初始化类对象自己进行的默认构造函数. ...
- C++中tuple类型
tuple是C++11新标准里的类型.它是一个类似pair类型的模板.pair类型是每个成员变量各自可以是任意类型,但是只能有俩个成员,而tuple与pair不同的是它可以有任意数量的成员.但是每个确 ...
- Git:使用远程仓库
远程仓库可使用Github.Gitee,或自建Gitlab.Gogs服务器,这里使用Github. 配置本地用户名和邮箱 # 配置本地用户的用户名邮箱(保存在用户.gitconfig文件) $ git ...
- ZooTeam 前端周刊|第 111期
转: ZooTeam 前端周刊|第 111期 ZooTeam 前端周刊|第 111期 浏览更多往期周刊,请访问: https://weekly.zoo.team 基于Vue的前端架构,我做了这15点 ...
- SpringBoot项目创建与单元测试
前言 Spring Boot 设计之初就是为了用最少的配置,以最快的速度来启动和运行 Spring 项目.Spring Boot使用特定的配置来构建生产就绪型的项目. Hello World 可以 ...
- #String类简述(小白理解,小白编写,欢迎大神指点,小白跪谢)
@ 目录 一.前言(可忽略) 二.String变量的认知 三.String类的构造方法 四.String类的基本方法 4.1 toString()方法 4.2 equals()方法 4.3 equal ...