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 ...
随机推荐
- ElasticSearch 聚合分析
公号:码农充电站pro 主页:https://codeshellme.github.io ES 中的聚合分析(Aggregations)是对数据的统计分析功能,它的优点是实时性较高,相比于 Hadoo ...
- vue:子组件通过调用父组件的方法的方式传参
在本案例中,由于子组件通过调用父组件的方法的方式传参,从而实现修改父组件data中的对象,所以需要啊使用$forceUpdate()进行强制刷新 父组件: provide() { return { s ...
- vue:表单验证时,trigger的值什么时候选blur什么时候选change
对el-input输入框的验证,trigger的值选blur,即失去焦点时进行验证. 下拉框(el-select).日期选择器(el-date-picker).复选框(el-checkbox).单选框 ...
- docker轻量级监控-sysdig
sysdig Sysdig = system(系统)+dig(挖掘).Sysdig 是一个开源系统发掘工具,用于系统级别的勘察和排障,可以把它看作一系列Linux系统工具的组合,主要包括: strac ...
- Android 7.0 TextView点击事件无效修复方案
public class PostTextView extends TextView { private Handler handler = new Handler(Looper.getMainLoo ...
- Bootstrap下拉菜单、按钮式下拉菜单
1. 概述 下拉菜单使用频率也是比较高的,比较常见的使用场景是在导航菜单栏,某个主菜单含有下拉的子菜单. Bootstrap为下拉菜单提供了两种实现方式,即普通的下拉菜单还有按钮式的下拉菜单.我们先看 ...
- Go benchmark 一清二楚
前言 基准测试(benchmark)是 go testing 库提供的,用来度量程序性能,算法优劣的利器. 在日常生活中,我们使用速度 m/s(单位时间内物体移动的距离)大小来衡量一辆跑车的性能,同理 ...
- WIFI6 基本知识(一)
什么是WI-FI6(802.11ax) Wi-Fi 6 是下一代 802.11ax 标准的简称.随着 Wi-Fi 标准的演进,WFA 为了便于 Wi-Fi 用户和设备厂商轻松了解其设备连接或支持的 W ...
- MySQL 表的约束与数据库设计
DQL 查询语句 排序 # 单列排序 * 只按某一个字段进行排序,单列排序 # 组合排序 * 同时对多个字段进行排序,如果第1个字段相等,则按照第2个字段排序,依次类推 * 语法: # 具体操作 * ...
- css实现鼠标滑过出现从中间向两边扩散的下划线
这个效果一开始我是在华为商城页面上看到的,刚开始还以为挺复杂,实现的时候还有点没头绪.不过,还好有百度,借此记录一下我在导航条上应用的实现方法. 主要是借助了伪元素,代码如下: <div cla ...