首页 文章 精选 留言 我的

精选列表

搜索[AI编程],共10000篇文章
优秀的个人博客,低调大师

Java并发编程(04):线程间通信,等待/通知机制

一、概念简介 1、线程通信 在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题,通常使用互斥锁来控制该逻辑。但是在还有这样一类场景,任务执行是有顺序控制的,例如常见的报表数据生成: 启动数据分析任务,生成报表数据; 报表数据存入指定位置数据容器; 通知数据搬运任务,把数据写入报表库; 该场景在相对复杂的系统中非常常见,如果基于多线程来描述该过程,则需要线程之间通信协作,才能有条不紊的处理该场景业务。 2、等待通知机制 如上的业务场景,如果线程A生成数据过程中,线程B一直在访问数据容器,判断该过程的数据是否已经生成,则会造成资源浪费。正常的流程应该如图,线程A和线程B同时启动,线程A开始处理数据生成任务,线程B尝试获取容器数据,数据还没过来,线程B则进入等待状态,当线程A的任务处理完成,则通知线程B去容器中获取数据,这样基于线程等待和通知的机制来协作完成任务。 3、基础方法 等待/通知机制的相关方法是Java中Object层级的基础方法,任何对象都有该方法: notify:随机通知一个在该对象上等待的线程,使其结束wait状态返回; notifyAll:唤醒在该对象上所有等待的线程,进入对象锁争抢队列中; wait:线程进入waiting等待状态,不会争抢锁对象,也可以设置等待时间; 线程的等待通知机制,就是基于这几个基础方法。 二、等待通知原理 1、基本原理 等待/通知机制,该模式下指线程A在不满足任务执行的情况下调用对象wait()方法进入等待状态,线程B修改了线程A的执行条件,并调用对象notify()或者notifyAll()方法,线程A收到通知后从wait状态返回,进而执行后续操作。两个线程通过基于对象提供的wait()/notify()/notifyAll()等方法完成等待和通知间交互,提高程序的可伸缩性。 2、实现案例 通过线程通信解决上述数据生成和存储任务的解耦流程。 public class NotifyThread01 { static Object lock = new Object() ; static volatile List<String> dataList = new ArrayList<>(); public static void main(String[] args) throws Exception { Thread saveThread = new Thread(new SaveData(),"SaveData"); saveThread.start(); TimeUnit.SECONDS.sleep(3); Thread dataThread = new Thread(new AnalyData(),"AnalyData"); dataThread.start(); } // 等待数据生成,保存 static class SaveData implements Runnable { @Override public void run() { synchronized (lock){ while (dataList.size()==0){ try { System.out.println(Thread.currentThread().getName()+"等待..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("SaveData .."+ dataList.get(0)+dataList.get(1)); } } } // 生成数据,通知保存 static class AnalyData implements Runnable { @Override public void run() { synchronized (lock){ dataList.add("hello,"); dataList.add("java"); lock.notify(); System.out.println("AnalyData End..."); } } } } 注意:除了dataList满足写条件,还要在AnalyData线程执行通知操作。 三、管道流通信 1、管道流简介 基本概念 管道流主要用于在不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读取数据,进而实现不同线程间的通信。 实现分类 管道字节流:PipedInputStream和PipedOutputStream; 管道字符流:PipedWriter和PipedReader; 新IO管道流:Pipe.SinkChannel和Pipe.SourceChannel; 2、使用案例 public class NotifyThread02 { public static void main(String[] args) throws Exception { PipedInputStream pis = new PipedInputStream(); PipedOutputStream pos = new PipedOutputStream(); // 链接输入流和输出流 pos.connect(pis); // 写数据线程 new Thread(new Runnable() { public void run() { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 将从键盘读取的数据写入管道流 PrintStream ps = new PrintStream(pos); while (true) { try { System.out.print(Thread.currentThread().getName()); ps.println(br.readLine()); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }, "输入数据线程:").start(); // 读数据线程 new Thread(new Runnable() { public void run() { BufferedReader br = new BufferedReader(new InputStreamReader(pis)); while (true) { try { System.out.println(Thread.currentThread().getName() + br.readLine()); } catch (IOException e) { e.printStackTrace(); } } } }, "输出数据线程:").start(); } } 写线程向管道流写入数据,读线程读取数据,完成基本通信流程。 四、生产消费模式 1、业务场景 基于线程等待通知机制:实现工厂生产一件商品,通知商店卖出一件商品的业务流程。 2、代码实现 public class NotifyThread03 { public static void main(String[] args) { Product product = new Product(); ProductFactory productFactory = new ProductFactory(product); ProductShop productShop = new ProductShop(product); productFactory.start(); productShop.start(); } } // 产品 class Product { public String name ; public double price ; // 产品是否生产完毕,默认没有 boolean flag ; } // 产品工厂:生产 class ProductFactory extends Thread { Product product ; public ProductFactory (Product product){ this.product = product; } @Override public void run() { int i = 0 ; while (i < 20) { synchronized (product) { if (!product.flag){ if (i%2 == 0){ product.name = "鼠标"; product.price = 79.99; } else { product.name = "键盘"; product.price = 89.99; } System.out.println("产品:"+product.name+"【价格:"+product.price+"】出厂..."); product.flag = true ; i++; // 通知消费者 product.notifyAll(); } else { try { // 进入等待状态 product.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } // 产品商店:销售 class ProductShop extends Thread { Product product ; public ProductShop (Product product){ this.product = product ; } @Override public void run() { while (true) { synchronized (product) { if (product.flag == true ){ System.out.println("产品:"+product.name+"【价格"+(product.price*2)+"】卖出..."); product.flag = false ; product.notifyAll(); //唤醒生产者 } else { try { product.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } 流程描述:ProductFactory生成一件商品,通知商店售卖,通过flag标识判断控制是否进入等待状态,商店卖出商品后,再次通知工厂生产商品。 五、源代码地址 GitHub·地址 https://github.com/cicadasmile/java-base-parent GitEE·地址 https://gitee.com/cicadasmile/java-base-parent

优秀的个人博客,低调大师

Java并发编程(03):多线程并发访问,同步控制

一、并发问题 多线程学习的时候,要面对的第一个复杂问题就是,并发模式下变量的访问,如果不理清楚内在流程和原因,经常会出现这样一个问题:线程处理后的变量值不是自己想要的,可能还会一脸懵的说:这不合逻辑吧? 1、成员变量访问 多个线程访问类的成员变量,可能会带来各种问题。 public class AccessVar01 { public static void main(String[] args) { Var01Test var01Test = new Var01Test() ; VarThread01A varThread01A = new VarThread01A(var01Test) ; varThread01A.start(); VarThread01B varThread01B = new VarThread01B(var01Test) ; varThread01B.start(); } } class VarThread01A extends Thread { Var01Test var01Test = new Var01Test() ; public VarThread01A (Var01Test var01Test){ this.var01Test = var01Test ; } @Override public void run() { var01Test.addNum(50); } } class VarThread01B extends Thread { Var01Test var01Test = new Var01Test() ; public VarThread01B (Var01Test var01Test){ this.var01Test = var01Test ; } @Override public void run() { var01Test.addNum(10); } } class Var01Test { private Integer num = 0 ; public void addNum (Integer var){ try { if (var == 50){ num = num + 50 ; Thread.sleep(3000); } else { num = num + var ; } System.out.println("var="+var+";num="+num); } catch (Exception e){ e.printStackTrace(); } } } 这里案例的流程就是并发下运算一个成员变量,程序的本意是:var=50,得到num=50,可输出的实际结果是: var=10;num=60 var=50;num=60 VarThread01A线程处理中进入休眠,休眠时num已经被线程VarThread01B进行一次加10的运算,这就是多线程并发访问导致的结果。 2、方法私有变量 修改上述的代码逻辑,把num变量置于方法内,作为私有的方法变量。 class Var01Test { // private Integer num = 0 ; public void addNum (Integer var){ Integer num = 0 ; try { if (var == 50){ num = num + 50 ; Thread.sleep(3000); } else { num = num + var ; } System.out.println("var="+var+";num="+num); } catch (Exception e){ e.printStackTrace(); } } } 方法内部的变量是私有的,且和当前执行方法的线程绑定,不会存在线程间干扰问题。 二、同步控制 1、Synchronized关键字 使用方式:修饰方法,或者以控制同步块的形式,保证多个线程并发下,同一时刻只有一个线程进入方法中,或者同步代码块中,从而使线程安全的访问和处理变量。如果修饰的是静态方法,作用的是这个类的所有对象。 独占锁属于悲观锁一类,synchronized就是一种独占锁,假设处于最坏的情况,只有一个线程执行,阻塞其他线程,如果并发高,处理耗时长,会导致多个线程挂起,等待持有锁的线程释放锁。 2、修饰方法 这个案例和第一个案例原理上是一样的,不过这里虽然在修改值的地方加入的同步控制,但是又挖了一个坑,在读取的时候没有限制,这个现象俗称脏读。 public class AccessVar02 { public static void main(String[] args) { Var02Test var02Test = new Var02Test (); VarThread02A varThread02A = new VarThread02A(var02Test) ; varThread02A.start(); VarThread02B varThread02B = new VarThread02B(var02Test) ; varThread02B.start(); var02Test.readValue(); } } class VarThread02A extends Thread { Var02Test var02Test = new Var02Test (); public VarThread02A (Var02Test var02Test){ this.var02Test = var02Test ; } @Override public void run() { var02Test.change("my","name"); } } class VarThread02B extends Thread { Var02Test var02Test = new Var02Test (); public VarThread02B (Var02Test var02Test){ this.var02Test = var02Test ; } @Override public void run() { var02Test.change("you","age"); } } class Var02Test { public String key = "cicada" ; public String value = "smile" ; public synchronized void change (String key,String value){ try { this.key = key ; Thread.sleep(2000); this.value = value ; System.out.println("key="+key+";value="+value); } catch (InterruptedException e) { e.printStackTrace(); } } public void readValue (){ System.out.println("读取:key="+key+";value="+value); } } 在线程中,逻辑上已经修改了,只是没执行到,但是在main线程中读取的value毫无意义,需要在读取方法上也加入同步的线程控制。 3、同步控制逻辑 同步控制实现是基于Object的监视器。 线程对Object的访问,首先要先获得Object的监视器 ; 如果获取成功,则会独占该对象 ; 其他线程会掉进同步队列,线程状态变为阻塞 ; 等Object的持有线程释放锁,会唤醒队列中等待的线程,尝试重启获取对象监视器; 4、修饰代码块 说明一点,代码块包含方法中的全部逻辑,锁定的粒度和修饰方法是一样的,就写在方法上吧。同步代码块一个很核心的目的,减小锁定资源的粒度,就如同表锁和行级锁。 public class AccessVar03 { public static void main(String[] args) { Var03Test var03Test1 = new Var03Test() ; Thread thread1 = new Thread(var03Test1) ; thread1.start(); Thread thread2 = new Thread(var03Test1) ; thread2.start(); Thread thread3 = new Thread(var03Test1) ; thread3.start(); } } class Var03Test implements Runnable { private Integer count = 0 ; public void countAdd() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(this) { count++ ; System.out.println("count="+count); } } @Override public void run() { countAdd() ; } } 这里就是锁定count处理这个动作的核心代码逻辑,不允许并发处理。 5、修饰静态方法 静态方法属于类层级的方法,对象是不可以直接调用的。但是synchronized修饰的静态方法锁定的是这个类的所有对象。 public class AccessVar04 { public static void main(String[] args) { Var04Test var04Test1 = new Var04Test() ; Thread thread1 = new Thread(var04Test1) ; thread1.start(); Var04Test var04Test2 = new Var04Test() ; Thread thread2 = new Thread(var04Test2) ; thread2.start(); } } class Var04Test implements Runnable { private static Integer count ; public Var04Test (){ count = 0 ; } public synchronized static void countAdd() { System.out.println(Thread.currentThread().getName()+";count="+(count++)); } @Override public void run() { countAdd() ; } } 如果不是使用同步控制,从逻辑和感觉上,输出的结果应该如下: Thread-0;count=0 Thread-1;count=0 加入同步控制之后,实际测试输出结果: Thread-0;count=0 Thread-1;count=1 6、注意事项 继承中子类覆盖父类方法,synchronized关键字特性不能继承传递,必须显式声明; 构造方法上不能使用synchronized关键字,构造方法中支持同步代码块; 接口中方法,抽象方法也不支持synchronized关键字 ; 三、Volatile关键字 1、基本描述 Java内存模型中,为了提升性能,线程会在自己的工作内存中拷贝要访问的变量的副本。这样就会出现同一个变量在某个时刻,在一个线程的环境中的值可能与另外一个线程环境中的值,出现不一致的情况。 使用volatile修饰成员变量,不能修饰方法,即标识该线程在访问这个变量时需要从共享内存中获取,对该变量的修改,也需要同步刷新到共享内存中,保证了变量对所有线程的可见性。 2、使用案例 class Var05Test { private volatile boolean myFlag = true ; public void setFlag (boolean myFlag){ this.myFlag = myFlag ; } public void method() { while (myFlag){ try { System.out.println(Thread.currentThread().getName()+myFlag); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } 3、注意事项 可见性只能确保每次读取的是最新的值,但不支持变量操作的原子性; volatile并不会阻塞线程方法,但是同步控制会阻塞; Java同步控制的根本:保证并发下资源的原子性和可见性; 四、源代码地址 GitHub·地址 https://github.com/cicadasmile/java-base-parent GitEE·地址 https://gitee.com/cicadasmile/java-base-parent

优秀的个人博客,低调大师

并发编程的艺术03-Bakery互斥锁算法

导读 本章会介绍Bakery互斥锁算法,涉及到并发下的公平性问题,有界计数器和无界计数器问题,存储单元数量下界问题。 公平性 无饥饿特性能够保证每一个调用 lock() 函数的线程最终都将进入临界区,但并不能保证进入临界区需要多长时间。理想情况下如果线程 A 在线程 B 之前调用 lock() 函数,那么 A 应该在 B 之前进入临界区。然而,运用现有的工具无法确定那个线程首先调用 lock() 函数。取而代之的做法是,将 lock() 函数代码划分为两个部分: 1. 门廊区: 其执行区间由有限个操作步组成。 2. 等待区: 其执行区间可能包扩无穷个操作步。 门廊区应该在有限步数内完成一种强约束条件。称这种约束为有界无等待演进特性。对于公平的定义:满足下面条件的锁称为先到先服务的:如果线程 A 门廊区的结束在线程 B 门廊区的开始之前完成,那么线程 A 比定不会被线程 B 赶超。 按照我们的惯例来举一个生活中的例子来帮助读者理解这种计算机术语都抽象描述。 大多数人都去银行办理过业务,如图1所示很多人都在等待,他们等待的依据是什么呢?总得有个先来后到吧,要不然有人插队岂不是要发生争吵了。于是银行想了一个办法给每一个来办理业务的顾客发一个号码,这个号码就是大家排队的依据。银行按照先到先服务(First-Come-First-Served)(这里的“先到”指的是谁先获取到号码而不是谁先进入银行)的准则来控制当前该叫到那个号码的持有者来办理业务。这种做法就是一种保障公平性的机制。在这个例子中银行中的取号机可以抽象为前文提到的"门廊区",而客户坐在椅子上等待可以抽象为前文提到的"等待区"。 Bakery 算法 在了解了公平性之后对Bakery算法就很容易理解了,因为Bakery保证公平性的方式和前文中举的银行排号例子原理是一样的。每个线程在门廊区得到一个序号,然后一直等待,直到再也没有序号比自己更早的线程尝试进入临界区止。 该算法中 flag[A] 是一个布尔型标志,表示线程 A 是否想要进入临界区; lable[A] 是一个整数型,说明线程进入面包店的相对次数。 Bakery 算法是无死锁的,正在等待的线程中,比定存在某一个线程 A具有最小的 lable[A] ,那么这个线程绝不会等待其他线程。 注意,既然满足无死锁又满足先到先服务特性的算法必定是无饥饿的。 class BakeryLock implements Lock { private boolean[] flag; private int[] label; private int n; public BakeryLock(int n) { this.n = n; flag = new boolean[n]; label = new int[n]; for (int i = 0;i < n; i++) { flag[i] = false; label[i] = 0; } } public void lock() { int i = ThreadID.get(); flag[i] = true; label[i] = max(label) + 1; for (int k = 0; k < n; k++) { while ((k != i) && flag[k] && ((label[k] < label[i]) || ((label[k] == label[i]) && k < i))) { } } } public void unlock() { flag[ThreadID.get()] = false; } private int max(int[] elementArray) { int maxValue = Integer.MIN_VALUE; for (int element : elementArray) { if (element > maxValue) { maxValue = element; } } return maxValue; } } 有界计数器和无界计数器 在理解了Bakery算法后,我们再来仔细看看这个算法中的问题。首先就是存在的一个bug ,就是label[i]的值会出现溢出的可能。 lable 值是无限增长的,因此在生命周期很长的系统中不得不考虑溢出的问题。如果某个线程的 lable 在其他线程都不知情的情况下从一个很大的数返回到 0 ,那么公平性将被破坏。例如到2038年1月18日,Unix 的 time_t 数据结构将会溢出,因为其秒数值是从 1970 年 1 月开始计算的,而在那一刻将会超过 2 的 32 次方。大多数采用 64-bit 计数器的应用程序在其声明周期内是不可能发生这种“回零”问题的。 Bakery 算法保证公平性的做法是确保某个线程在另一个线程之前得到一个 lable 值,那么后一个线程的 lable 值一定比前者大。通过仔细观察 Bakery 算法代码,我们可以得出一个线程需要具备两种能力: 1. 读取其他线程的 lable (扫描)。 2. 为自己设置一个更大的 lable (标记)。 这时候的 Bakery 算法中的 lable 值获取看起来像是这样:这个数是随着时间无限向后增长的,显然它是无限的 ,直到出现溢出问题。 为了解决这个溢出问题我们考虑使用有界的 lable 值获取,类似这样(这是只有两个线程的情况): 在这个有向环中是一系列的节点 n0 , n1 , ... , nk ,其中有一条边从 n0到n1,有一条边从n1到n2,最后一条边从n(k - 1) 到 nk ,并有一条边从nk返回n0。边定义结果集上的次序关系为:0 < 1 , 1 < 2 , 2 < 0。两个线程的 lable 在 0 , 1 , 2 三个节点中不断的轮转改变。 N 个 线程的情况较为复杂暂时不进行讨论,只是说明结论。 存储单元数量下界 还记得我们之前说过的么,会介绍一些经典但是不实用的互斥锁算法,Bakery算法就是其中之一。及时 Bakery 算法十分的简洁,无饥饿,无死锁,而且公平,那么它为什么不实用呢?最主要的问题是要读写 N 个不同的存储单元。(N 是线程的最大个数)。 那么是否有更好的基于读/写存储器的锁算法可以避免这种开销呢?答案是否定的。也就是说任何一种无死锁的锁算法在最坏情况下至少需要读/写 N 个不同的存储单元。正是因为如此,才促使我们的多处理器计算机中,增加了一些功能要比读/写更强大的同步操作,并以这些操作作为互斥算法的基础。 现在我们要说明为什么这种线性下界是解决互斥问题时锁固有的。要记得一点只能通过读/写指令访问存储单元具有一个重要限制:一个线程向某个指定单元写的任何信息,在其他线程读取之前可能会被覆盖。下面是证明过程 到这里Bakery算法就讲完了,希望对你有帮助,如果觉得有收获还请点击"再看"自持作者。本人才疏学浅,如果文中有不当之处还请留言指正。 看完本文有收获?请分享给更多人 微信关注「黑帽子技术」加星标,看精选 IT技术文章

优秀的个人博客,低调大师

声明式 API 编程框架 Mars-java 发布 3.0.9

本次更新如下: 分布式锁只需一行注解 新增文件下载功能封装 多个文件上传可以一次性接收 文件上传可以配置大小限制 分布式锁 @RedisLock(key = "自己定义一个key") public int insert(){ return 1; } 这个锁会在事务开启之前获取,在事务提交以后解锁,并且只对MarsBean有效,对API和DAO无效 如果你不需要锁住整个方法,只需要锁一小段代码,那么可以去我的官网查看相应资料 文件下载 在service实现类的方法里传入HttpMarsResponse对象,然后一行代码搞定 public void downLoad(HttpMarsResponse response) { try { File file = new File("/Users/yuye/Desktop/demo.mov"); InputStream inputStream = new FileInputStream(file); response.downLoad("文件名称",inputStream); } catch (Exception e){ } } 文件上传接收 在接收参数的实体类里面 添加如下属性 public class UserInfoDTO { //单个文件 private MarsFileUpLoad file; //多个文件 private MarsFileUpLoad[] files; } 配置文件大小限制 在配置文件里加入 以下配置 #上传文件大小(默认 1M,10M),单位k fileUpload: fileSizeMax: 100 sizeMax: 1000 同时,Mars-cloud 3.0.6也发布了 官网:http://mars-framework.com/

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册