【Java基础教程】(四十三)多线程篇 · 下:深入剖析Java多线程编程:同步、死锁及经典案例——生产者与消费者,探究sleep()与wait()的差异

Java基础教程之多线程 · 下

  • 🔹本节学习目标
  • 1️⃣ 线程的同步与死锁
    • 1.1 同步问题的引出
    • 2.2 synchronized 同步操作
    • 2.3 死锁
  • 2️⃣ 多线程经典案例——生产者与消费者
    • 🔍分析sleep()和wait()的区别?
  • 🌾 总结

在这里插入图片描述

🔹本节学习目标

  • 理解多线程中的同步与死锁的概念;
  • 掌握Object 类中对于多线程的支持;

1️⃣ 线程的同步与死锁

程序利用线程可以进行更为高效的程序处理,如果在没有多线程的程序中, 一个程序在处理某些资源时会有主方法(主线程全部进行处理),但是这样的处理速度一定会比较慢,如下图 (a) 所示。而如果采用了多线程的处理机制,利用主线程创建出许多子线程(相当于多了许多帮手), 一起进行资源的操作,如下图 (b) 所示,那么执行效率一定会比只使用一个主线程更高。

图1 单线程与多线程的区别

在程序开发中,所有程序都是通过主方法执行的,而主方法本身就属于一个主线程, 所以通过主方法创建的新的线程对象都是子线程。在Android开发中,默认运行的 Activity 就可以理解为主线程,当移动设备需要读取网络信息时往往会启动新的子线程读取,而不会在主线程中操作。

利用子线程可以进行异步的操作处理,这样可以在不影响主线程运行的前提下进行其他操作,程序的执行速度不仅变快了,并且操作起来也不会产生太多的延迟。对于这部分知识,有些刚接触Java的朋友理解起来可能会有些困难,但随着开发经验提升,自己慢慢可以领会的更多。

虽然使用多线程同时处理资源效率要比单线程高许多,但是多个线程操作同一个资源时也一定会带来一些问题,如资源操作的完整性问题等等。

1.1 同步问题的引出

同步是多线程开发中的一个重要概念,既然有同步,就一定会存在不同步的操作。多个线程操作同一资源时就有可能出现不同步的问题,例如:现在产生 N 个线程对象进行卖票操作,为了更加明显地观察不同步所带来的问题,所以下面案例程序将使用线程的休眠操作。

//	范例 1: 观察非同步情况下的操作
package com.xiaoshan.demo;class MyThread implements Runnable{private int ticket = 5;   	//一共有5张票@Overridepublic void run(){for(int x=0; x<20; x++){if(this.ticket > 0){ 	//判断当前是否还有剩余票try{Thread.sleep(100);              //休眠1s, 模拟延迟}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,ticket="+ this.ticket--);}}}
}public class TestDemo {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(); new Thread(mt,"窗口D").start();}
}

执行结果:

窗口A卖票,ticket=5
窗口B卖票,ticket=5 (错误的数据,因为不同步所引起)
窗口D卖票,ticket=4
窗口C卖票,ticket=3
窗口D卖票,ticket=2
窗口C卖票,ticket=0
窗口B卖票,ticket=-1 (错误的数据,因为不同步所引起)
窗口A卖票,ticket=1

此程序模拟了一个卖票程序的实现,其中将有4 个线程对象共同完成卖票的任务,为了保证每次在有剩余票数时实现卖票操作,在卖票前增加了一个判断条件 (if (this.ticket>0)), 满足此条件的线程对象才可以卖票,不过根据最终的结果却发现,这个判断条件的作用并不明显。

从上边范例的操作代码可以发现,对于票数的操作有如下步骤。
(1)判断票数是否大于0, 大于0 表示还有票可以卖;
(2)如果票数大于0, 则卖票出去。

但是,在上边范例的操作代码中,在第1步和第2步之间加入了延迟操作,那么一个线程就有可能在还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样一来就会出现票数为负的情况,如下图所示。

图2 多线程操作同一资源未同步的问题

2.2 synchronized 同步操作

如果想解决上边范例程序的问题,就必须使用同步操作。所谓同步操作就是一个代码块中的多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成后才可以继续执行,如下图所示。

图3 多线程同步思想

在 Java 里面如果要想实现线程的同步,操作可以使用 synchronized 关键字。 synchronized 关键字可以通过以下两种方式进行使用。

  • 同步代码块:利用 synchronized 包装的代码块,但是需要指定同步对象,一般设置为 this
  • 同步方法:利用 synchronized 定义的方法。
//	范例 2: 观察同步块
package com.xiaoshan.demo;class MyThread implements Runnable{private int ticket = 60;@Overridepublic void run(){for (int x=0; x<20; x++){synchronized(this){		//定义同步代码块if(this.ticket>0){		//判断当前是否还有剩余票try{Thread.sleep(100);	//休眠1s, 模拟延迟}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--);}}}}
}public class TestDemo {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(); new Thread(mt,"窗口D").start();}
}

程序执行结果:

窗口A卖票,ticket = 60
窗口A卖票,ticket = 59
窗口A卖票,ticket = 58
窗口A卖票,ticket = 57
窗口A卖票,ticket = 56
窗口A卖票,ticket = 55
窗口A卖票,ticket = 54
窗口A卖票,ticket = 53
窗口A卖票,ticket = 52
窗口A卖票,ticket = 51
窗口A卖票,ticket = 50
窗口A卖票,ticket = 49
窗口A卖票,ticket = 48
窗口A卖票,ticket = 47
窗口A卖票,ticket = 46
窗口A卖票,ticket = 45
窗口C卖票,ticket = 44
窗口C卖票,ticket = 43
窗口C卖票,ticket = 42
窗口C卖票,ticket = 41
窗口C卖票,ticket = 40
窗口C卖票,ticket = 39
窗口C卖票,ticket = 38
窗口C卖票,ticket = 37
窗口C卖票,ticket = 36
窗口C卖票,ticket = 35
窗口C卖票,ticket = 34
窗口C卖票,ticket = 33
窗口D卖票,ticket = 32
窗口D卖票,ticket = 31
窗口D卖票,ticket = 30
窗口D卖票,ticket = 29
窗口D卖票,ticket = 28
窗口D卖票,ticket = 27
窗口D卖票,ticket = 26
窗口D卖票,ticket = 25
窗口D卖票,ticket = 24
窗口D卖票,ticket = 23
窗口D卖票,ticket = 22
窗口D卖票,ticket = 21
窗口D卖票,ticket = 20
窗口B卖票,ticket = 19
窗口B卖票,ticket = 18
窗口B卖票,ticket = 17
窗口B卖票,ticket = 16
窗口B卖票,ticket = 15
窗口B卖票,ticket = 14
窗口B卖票,ticket = 13
窗口B卖票,ticket = 12
窗口B卖票,ticket = 11
窗口B卖票,ticket = 10
窗口B卖票,ticket = 9
窗口B卖票,ticket = 8
窗口B卖票,ticket = 7
窗口B卖票,ticket = 6
窗口B卖票,ticket = 5
窗口B卖票,ticket = 4
窗口B卖票,ticket = 3
窗口B卖票,ticket = 2
窗口B卖票,ticket = 1

此程序将判断是否有票以及卖票的两个操作都统一放到了同步代码块中,这样当某一个线程操作时,其他线程无法进入到方法中进行操作,从而实现了线程的同步操作。

可以从程序运行结果发现,卖票数量被大致平均到了各个线程,而且未出现错误数据的情况。

//	范例 3: 使用同步方法解决问题
package com.xiaoshan.demo;class MyThread implements Runnable {private int ticket = 60;	//一共有60张票@Overridepublic void run(){for(int x=0; x<20; x++){ this.sale();}}//卖票操作public synchronized void sale(){	//同步方法if(this.ticket>0){	//判断当前是否还有剩余票try{Thread.sleep(100);	//休眠1s, 模拟延迟} catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName()+ "卖票,ticket=" + this.ticket--);}}
}public class TestDemo {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(); new Thread(mt,"窗口D").start();}
}

程序执行结果:

窗口A卖票,ticket=60
窗口A卖票,ticket=59
窗口A卖票,ticket=58
窗口A卖票,ticket=57
窗口A卖票,ticket=56
窗口A卖票,ticket=55
窗口A卖票,ticket=54
窗口A卖票,ticket=53
窗口A卖票,ticket=52
窗口A卖票,ticket=51
窗口A卖票,ticket=50
窗口A卖票,ticket=49
窗口A卖票,ticket=48
窗口A卖票,ticket=47
窗口A卖票,ticket=46
窗口A卖票,ticket=45
窗口A卖票,ticket=44
窗口A卖票,ticket=43
窗口A卖票,ticket=42
窗口A卖票,ticket=41
窗口D卖票,ticket=40
窗口D卖票,ticket=39
窗口D卖票,ticket=38
窗口D卖票,ticket=37
窗口D卖票,ticket=36
窗口D卖票,ticket=35
窗口D卖票,ticket=34
窗口D卖票,ticket=33
窗口D卖票,ticket=32
窗口D卖票,ticket=31
窗口D卖票,ticket=30
窗口D卖票,ticket=29
窗口D卖票,ticket=28
窗口D卖票,ticket=27
窗口D卖票,ticket=26
窗口D卖票,ticket=25
窗口D卖票,ticket=24
窗口C卖票,ticket=23
窗口C卖票,ticket=22
窗口C卖票,ticket=21
窗口C卖票,ticket=20
窗口C卖票,ticket=19
窗口C卖票,ticket=18
窗口C卖票,ticket=17
窗口C卖票,ticket=16
窗口C卖票,ticket=15
窗口B卖票,ticket=14
窗口B卖票,ticket=13
窗口B卖票,ticket=12
窗口B卖票,ticket=11
窗口B卖票,ticket=10
窗口B卖票,ticket=9
窗口B卖票,ticket=8
窗口B卖票,ticket=7
窗口B卖票,ticket=6
窗口B卖票,ticket=5
窗口B卖票,ticket=4
窗口B卖票,ticket=3
窗口B卖票,ticket=2
窗口B卖票,ticket=1

此时利用同步方法同样解决了同步操作的问题。但是在此处需要说明一个问题:加入同步后明显比不加入同步慢许多,所以同步的代码性能会很低,但是数据的安全性会高,或者可以称为线程安全性高。

那么在了解了以上知识后,同步和异步有什么区别呢,在什么情况下分别使用它们呢?

如果一块数据要在多个线程间进行共享。例如,正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

2.3 死锁

同步就是指一个线程要等待另外一个线程执行完毕才会继续执行的一种操作形 式,虽然在一个程序中,使用同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。

例如:张三想要李四的画,李死想要张三的书,那么张三对李四说 了:“把你的画给我,我就给你书", 李四也对张三说了:“把你的书给我,我就给你画", 这时,张三在等着李四的答复,而李四也在等着张三的答复,这样下去最终结果可想而知,张三得不到李四的画,李四也得不到张三的书,这实际上就是死锁的概念,如下图所示。

图4 死锁的场景

所谓死锁就是指两个线程都在等待彼此先完成,造成了程序的停滞状态, 一般程序的死锁都是在程序运行时出现的,下面通过一个简单的范例来观察一下出现死锁的情况。

//	范例 4: 程序死锁操作
package com.xiaoshan.demo;class A{public synchronized void say(B b){System.out.println("A先生:把你的本给我,我给你笔,否则不给!");b.get();}public synchronized void get(){System.out.println("A先生:得到了本,付出了笔,还是什么都干不了!");}
}class B{public synchronized void say(A a){System.out.println("B先生:把你的笔给我,我给你本,否则不给!");a.get();}public synchronized void get(){System.out.println("B先生:得到了笔,付出了本,还是什么都干不了!");}
}public class TestDemo implements Runnable{private static A a= new A();                          //定义类对象private static B b= new B();                           //定义类对象public static void main(String[] args) throws Exception  {new TestDemo();                                                //实例化本类对象}public TestDemo(){	//构造方法new Thread(this).start();	//启动线程b.say(a);	//互相引用}@Overridepublic void run(){a.say(b);	//互相引用}
}

程序执行结果:

B先生:把你的笔给我,我给你本,否则不给!
A先生:把你的本给我,我给你笔,否则不给!
(程序将不再向下执行,并且不会退出,此为死锁情况出现)

此程序由于两个类的都使用了同步方法定义,就会造成 a 对象等待 b 对象执行完毕,而 b 对象等待 a 对象执行完毕,这样就会出现死锁现象。

综上,多个线程访问同一资源时,考虑到数据操作的安全性问题, 一定要使用同步操作。同步有以下两种操作模式:

  • 同步代码块:synchronized(锁定对象){代码};
  • 同步方法:public synchronized 返回值 方法名称() {代码}

需要注意的是,过多的同步操作有可能会带来死锁问题,导致程序进入停滞状态。

2️⃣ 多线程经典案例——生产者与消费者

在开发中线程的运行状态并不固定,所以只能利用线程的名字以及当前执行的线程对象来进行区分。但是多个线程间也有可能会出现数据交互的情况。本节将利用一个线程的经典操作案例来分析线程的交互中存在问题以及问题的解决方案。

在生产者和消费者模型中,生产者不断生产,消费者不断取走生产者生产的产品,如下图所示。

图5 生产者与消费者案例

在图中非常清楚地表示出,生产者生产出信息后将其放到一个区域中,然后消费者从此区域里取出数据,但是在程序中因为牵涉线程运行的不确定性,所以会存在以下两点问题。
(1)假设生产者线程向数据存储空间添加信息的名称,还没有加入该信息的内容,程序就切换到了消费者线程,消费者线程将把该信息的名称和上一个信息的内容联系到一起。
(2)生产者放了若干次的数据,消费者才开始取数据,或者是消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。

//	范例 5: 程序基本模型
package com.xiaoshan.demo;class Message{private String title;	//保存信息的标题 private String content;	//保存信息的内容public void setTitle(String title){this.title = title;}public void setContent(String content){this.content = content;}public String getTitle(){return title;}public String getContent(){return content;}
}class Producer implements Runnable {                       //定义生产者private Message msg = null;public Producer(Message msg){this.msg = msg;}@Overridepublic void run(){for(int x=0; x<8; x++){                      //生产8次数据if(x%2 == 0){this.msg.setTitle("小山");     //设置 title属性try{Thread.sleep(100);                         //延迟操作} catch(InterruptedException  e){e.printStackTrace();}this.msg.setContent("Java专栏作者");	//设置content属性}else{this.msg.setTitle("xiaoshan");              //设置 title 属性try  {Thread.sleep(100);}catch (InterruptedException e){e.printStackTrace();}this.msg.setContent("www.xiaoshan.cn");// 设置content属性}}}
}class Consumer implements Runnable {                          //定义消费者private Message msg = null;public Consumer (Message msg){this.msg = msg;}@Overridepublic void run(){for(int x=0; x<8; x++){                                //取走8次数据try{Thread.sleep(100);                                      //延迟} catch(InterruptedException e){e.printStackTrace();}System.out.println(this.msg.getTitle() + "-->" + this.msg.getContent());}}
}public class TestDemo {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(); 		// 取得消费者线程}
}

程序执行结果:

xiaoshan-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->Java专栏作者
xiaoshan-->www.xiaoshan.cn

通过本程序的运行结果可以发现两个严重的问题:设置的数据错位;数据会重复设置及重复取出。

首先我们来解决数据错乱问题,数据错位完全是因为未同步的操作,所以应该使用同步处理。因为取出和设置是两个不同的操作,所以要想进行同步控制,就需要将其定义在一个类里面完成。

//	范例 6: 加入同步,解决数据错乱问题
package com.xiaoshan.demo;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 void get(){try {Thread.sleep(100);} catch(InterruptedException  e){e.printStackTrace();}System.out.println(this.title + "-->" + this.content);}// setter、getter略
}class Producer implements Runnable {                       //定义生产者private Message msg = null;public Producer(Message msg){this.msg = msg;}@Overridepublic void run(){for(int x=0; x<8; x++){                      //生产8次数据if(x%2 == 0){this.msg.set("小山", "Java专栏作者");     //设置属性}else{this.msg.set("xiaoshan", "www.xiaoshan.cn");              //设置属性}}}
}class Consumer implements Runnable {	//定义消费者private Message msg = null;public Consumer (Message msg){this.msg = msg;}@Overridepublic void run(){for (int x=0; x<8; x++){	//取走8数据this.msg.get();	//取得属性}}
}public class TestDemo  {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();	//取得消费者线程}
}

程序执行结果:

小山-->Java专栏作者
小山-->Java专栏作者
小山-->Java专栏作者
小山-->Java专栏作者
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
xiaoshan-->www.xiaoshan.cn
xiaoshan-->www.xiaoshan.cn

从运行结果可以发现,数据错位问题此时已经因为使用了同步处理而得到了解决。下面我们来解决数据重复问题,要想解决数据重复的问题,需要等待及唤醒机制,而这一机制的实现只能依靠 Object类完成,前面在《【Java基础教程】(十六)面向对象篇 · 第十讲:解读Object类——定义、操作方法、深拷贝和浅拷贝的差异、多线程编程支持及使用场景~》一文中介绍到了在 Object 类中定义了3个方法完成线程的操作,如下所示。

  • public final void wait(throws InterruptedException):线程的等待;
  • public final void notify():唤醒第一个等待线程;
  • public final void notifyAll():唤醒全部等待线程。

可以发现,一个线程可以为其设置等待状态,但是对于唤醒的操作却有两个: notify()notifyAll()。一般来说,所有等待的线程会按照顺序进行排列。如果使用了 notify()方法,则会唤醒第一个等待的线程执行;如果使用了notifyAll() 方法,则会唤醒所有的等待线程。哪个线程的优先级高,哪个线程就有可能先执行,如下图所示。

图6 notify()与notifyAll()的区别

清楚了Object 类中的3个方法作用后,下面就可以利用这些方法来解决程序中的问题。如果想让生产者不重复生产,消费者不重复取走,则可以增加一个标志位,假设标志位为 boolean 型变量。如果标志位的内容为 true, 则表示可以生产,但是不能取走,如果此时线程执行到了,消费者线程则应该等待;如果标志位的内容为 false, 则表示可以取走,但是不能生产,如果生产者线程运行,则应该等待。
操作流程如下图所示。

图7 操作流程

所以要想解决数据重复的问题,只需要直接修改 Message 类即可。在 Message 类中加入标志位,并通过判断标志位完成等待与唤醒的操作。

//	范例 7: 解决程序问题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(200);} catch (InterruptedException e){e.printStackTrace();}this.content = content;this.flag = false;	//已经生产完成,修改标志位super.notify();	//唤醒等待线程}public synchronized void get(){if (this.flag == true){                                    //未生产,不能取走try{super.wait();                                    //等待}catch(InterruptedException e){e.printStackTrace();}}try{Thread.sleep(100);}catch(InterruptedException	e){e.printStackTrace();}System.out.println(this.title + "-->" + this.content);this.flag = true;	//已经取走了,可以继续生产super.notify();		//唤醒等待线程}// setter、getter略
}class Producer implements Runnable {                       //定义生产者private Message msg = null;public Producer(Message msg){this.msg = msg;}@Overridepublic void run(){for(int x=0; x<8; x++){                      //生产8次数据if(x%2 == 0){this.msg.set("小山", "Java专栏作者");     //设置属性}else{this.msg.set("xiaoshan", "www.xiaoshan.cn");              //设置属性}}}
}class Consumer implements Runnable {	//定义消费者private Message msg = null;public Consumer (Message msg){this.msg = msg;}@Overridepublic void run(){for (int x=0; x<8; x++){	//取走8数据this.msg.get();	//取得属性}}
}public class TestDemo  {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();	//取得消费者线程}
}

程序的运行结果:

小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn
小山-->Java专栏作者
xiaoshan-->www.xiaoshan.cn

从程序的运行结果中可以清楚地发现,生产者每生产一个信息就要等待消费者取走,消费者每取走一个信息就要等待生产者生产,这样就避免了重复生产和重复取走的问题。

🔍分析sleep()和wait()的区别?

  • sleep()Thread类定义的 static方法,表示线程休眠,将执行机会给其他线程,但是监控状态依然保持,休眠时间到了会自动恢复;
  • wait()Obiect类定义的方法,表示线程等待,一直到执行了 notify()notifyAll() 后才会被唤醒,结束等待。

🌾 总结

本文主要介绍了多线程编程中的同步与死锁问题,以及经典的生产者与消费者案例,并分析了sleep()wait()方法的区别。

我们首先引出了线程同步问题,解释了多个线程同时访问共享资源时可能导致的数据不一致性和并发安全性问题。为了解决这些问题,我们介绍了synchronized关键字,说明了如何使用它来实现线程的同步操作,以确保只有一个线程可以访问共享资源,从而避免数据的争用和冲突。

接下来,我们讨论了死锁问题,详细说明了死锁是由于多个线程相互等待对方释放资源而无法继续执行的情况。死锁的出现是由于资源竞争和线程之间的依赖所导致的,后面的文章中将会为大家介绍一些避免死锁的常见方法,如避免嵌套锁、按顺序获取资源等,敬请期待。

随后,我们介绍了经典的生产者与消费者案例,展示了多线程协作的实践应用。通过使用wait()notify()notifyAll()方法,我们演示了如何实现生产者与消费者之间的有效通信和资源共享。

最后,我们对比了sleep()wait()方法的区别。sleep()方法是让线程暂停一段指定的时间,不释放锁资源;而wait()方法是让线程进入等待状态,同时释放锁资源,直到被其他线程唤醒并重新获得锁资源。我们强调了在使用wait()方法时需要注意与notify()notifyAll()方法配合使用,以免出现线程无法被唤醒或永久等待的情况。


温习回顾上一篇(点击跳转)
《【Java基础教程】(四十二)多线程篇 · 上:多进程与多线程、并发与并行的关系,多线程的实现方式、线程流转状态、常用操作方法解析~》

继续阅读下一篇(点击跳转)
《【Java基础教程】(四十四)IO篇 · 上:解析Java文件操作——File类、字节流与字符流,分析字节输出流、字节输入流、字符输出流和字符输入流的区别》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/6569.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谷歌插件(Chrome扩展) “Service Worker (无效)” 解决方法

问题描述&#xff1a; 写 background 文件的时候报错了&#xff0c;说 Service Worker 设置的 background 无效。 解决&#xff08;检查&#xff09;方法&#xff1a; 检查配置文件&#xff08;manifest.json&#xff09; 中的 manifest_version 是否为 3。 background 中的…

如何动态修改 spring aop 切面信息?让自动日志输出框架更好用

业务背景 很久以前开源了一款 auto-log 自动日志打印框架。 其中对于 spring 项目&#xff0c;默认实现了基于 aop 切面的日志输出。 但是发现一个问题&#xff0c;如果切面定义为全切范围过大&#xff0c;于是 v0.2 版本就是基于注解 AutoLog 实现的。 只有指定注解的类或…

DataWhale AI夏令营——机器学习

DataWhale AI夏令营——机器学习 学习记录一1. 异常值分析2. 单变量箱线图可视化3. 特征重要性分析 学习记录一 锂电池电池生产参数调控及生产温度预测挑战赛 已配置环境&#xff0c;跑通baseline&#xff0c;并在此基础上对数据进行了简单的分析。 1. 异常值分析 对训练集…

K8S初级入门系列之八-网络

一、前言 本章节我们将了解K8S的相关网络概念&#xff0c;包括K8S的网络通讯原理&#xff0c;以及Service以及相关的概念&#xff0c;包括Endpoint&#xff0c;EndpointSlice&#xff0c;Headless service&#xff0c;Ingress等。 二、网络通讯原理和实现 同一K8S集群&…

PMP 数据收集工具与技术

数据收集工具与技术 (9个) 标杆对照 标杆对照是指将实际或计划的产品、流程和实践与其他可比组织的 做法进行比较&#xff0c;以便识别最佳实践、形成改进意见&#xff0c;并为绩效考核 提供依据。 头脑风暴 头脑风暴是一种数据收集和创意技术&#xff0c;主要用于在短时间…

三维点云中的坐标变换(只讲关键部分)

一、坐标旋转 坐标旋转包含绕x、y、z轴旋转&#xff0c;在右手坐标系中&#xff0c;x-翻滚(roll)&#xff0c;y-俯仰(pitch)&#xff0c;z-航向(yaw)。如果想详细了解&#xff0c;可以网络搜索 在PCL中&#xff0c;从baseLink到map的转换关系为:先绕x轴旋转,在绕y轴旋转,最后绕…

【软件工程中的各种图】

1、用例图&#xff08;use case diagrams&#xff09; 【概念】描述用户需求&#xff0c;从用户的角度描述系统的功能 【描述方式】椭圆表示某个用例&#xff1b;人形符号表示角色 【目的】帮组开发团队以一种可视化的方式理解系统的功能需求 【用例图】 2、静态图(Static …

【数据结构】C--单链表(小白入门基础知识)

前段时间写了一篇关于顺序表的博客&#xff0c;http://t.csdn.cn/0gCRp 顺序表在某些时候存在着一些不可避免的缺点: 问题&#xff1a; 1. 中间 / 头部的插入删除&#xff0c;时间复杂度为 O(N) 2. 增容需要申请新空间&#xff0c;拷贝数据&#xff0c;释放旧空间。会有不…

前端 | ( 十一)CSS3简介及基本语法(上) | 尚硅谷前端html+css零基础教程2023最新

学习来源&#xff1a;尚硅谷前端htmlcss零基础教程&#xff0c;2023最新前端开发html5css3视频 系列笔记&#xff1a; 【HTML4】&#xff08;一&#xff09;前端简介【HTML4】&#xff08;二&#xff09;各种各样的常用标签【HTML4】&#xff08;三&#xff09;表单及HTML4收尾…

2023/07/23

1. 必须等待所有请求结束后才能执行后续操作的处理方式 方式一&#xff1a; async func () {const p1 await api1();const p2 await api2();const p3 await api3();Promise.all([p1, p2, p3]).then(res > {后续操作...}) }方式二&#xff1a;待补充 2. flex 弹性盒子布…

FPGA实现串口回环

文章目录 前言一、串行通信1、分类1、同步串行通信2、异步串行通信 2、UART串口通信1、UART通信原理2、串口通信时序图 二、系统设计1、系统框图2.RTL视图 三、源码1、串口发送模块2、接收模块3、串口回环模块4、顶层模块 四、测试效果五、总结六、参考资料 前言 环境&#xff…

【计算机视觉 | 目标检测】arxiv 计算机视觉关于目标检测的学术速递(7 月 21 日论文合集)

文章目录 一、检测相关(15篇)1.1 Representation Learning in Anomaly Detection: Successes, Limits and a Grand Challenge1.2 AlignDet: Aligning Pre-training and Fine-tuning in Object Detection1.3 Cascade-DETR: Delving into High-Quality Universal Object Detectio…

《Docker与持续集成/持续部署:构建高效交付流程,打造敏捷软件交付链》

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

c语言修炼之指针和数组笔试题解析(1.2)

前言&#xff1a; 书接上回&#xff0c;让我们继续开始今天的学习叭&#xff01;废话不多说&#xff0c;还是字符数组的内容上代码&#xff01; char *p是字符指针&#xff0c;*表示p是个指针&#xff0c;char表示p指向的对象类型是char型&#xff01; char*p"abcdef&q…

使用Plist编辑器——简单入门指南

本指南将介绍如何使用Plist编辑器。您将学习如何打开、编辑和保存plist文件&#xff0c;并了解plist文件的基本结构和用途。跟随这个简单的入门指南&#xff0c;您将掌握如何使用Plist编辑器轻松管理您的plist文件。 plist文件是一种常见的配置文件格式&#xff0c;用于存储应…

7.6Java EE——Bean的生命周期

Bean在不同作用域内的生命周期 Bean的生命周期是指Bean实例被创建、初始化和销毁的过程。在Bean的两种作用域singleton和prototype中&#xff0c;Spring容器对Bean的生命周期的管理是不同的。在singleton作用域中&#xff0c;Spring容器可以管理Bean的生命周期&#xff0c;控制…

vue父组件和子组件数据传递

vue --父组件向子组件传递数据 父组件&#xff1a; <template><div class"parent"><p>父组件&#xff1a;{{ msg }}</p><Child message"Hello, I am parent!"></Child></div> </template><script>…

【Linux】udp客户端windows版以及Tcp服务器的实现

windows版客户端更适合大多数人~ 文章目录 一. udp客户端windows版二.Tcp服务器的实现总结 一、udp客户端windows版 首先我们将上一篇文章中实现的udp大型聊天室的代码进行修改&#xff0c;注意我们只修改服务端代码将代码修改的很简单就好&#xff0c;因为我们只是做一个如何…

【Flume 01】Flume简介、部署、组件

1 Flume简介 Flume是一个高可用、高可靠、分布式的海量日志采集、聚合和传输的系统 主要特性&#xff1a; 它有一个简单、灵活的基于流的数据流结构&#xff08;使用Event封装&#xff09;具有负载均衡机制和故障转移机制一个简单可扩展的数据模型(Source、Channel、Sink) Sou…

Zookeeper的基本概念以及安装

Zookeeper简介 Zookeeper是一个分布式的(多台机器同时干一件事情),开源的分布式应用程序协调服务,是Google公司Chubby产品,是Hadoop和Base重要的组件,.它是一个分布式应用程序提供一致性的服务的软件,提供的功能包括:配置服务,域名服务,分布式同步,组服务等 Zookeeper目…