深圳网站建设机构深圳工程建设交易服务中心网站
news/
2025/10/2 18:22:05/
文章来源:
深圳网站建设机构,深圳工程建设交易服务中心网站,wordpress上传mp4,天津城市建设管理职业学院网站Java基础教程之多线程 下 #x1f539;本节学习目标1️⃣ 线程的同步与死锁1.1 同步问题的引出2.2 synchronized 同步操作2.3 死锁 2️⃣ 多线程经典案例——生产者与消费者#x1f50d;分析sleep()和wait()的区别#xff1f; #x1f33e; 总结 #x1f539;本节学习目标… 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 x0; x20; 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卖票ticket5
窗口B卖票ticket5 (错误的数据因为不同步所引起)
窗口D卖票ticket4
窗口C卖票ticket3
窗口D卖票ticket2
窗口C卖票ticket0
窗口B卖票ticket-1 (错误的数据因为不同步所引起)
窗口A卖票ticket1此程序模拟了一个卖票程序的实现其中将有4 个线程对象共同完成卖票的任务为了保证每次在有剩余票数时实现卖票操作在卖票前增加了一个判断条件 (if (this.ticket0)), 满足此条件的线程对象才可以卖票不过根据最终的结果却发现这个判断条件的作用并不明显。
从上边范例的操作代码可以发现对于票数的操作有如下步骤。 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 x0; x20; x){synchronized(this){ //定义同步代码块if(this.ticket0){ //判断当前是否还有剩余票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 x0; x20; x){ this.sale();}}//卖票操作public synchronized void sale(){ //同步方法if(this.ticket0){ //判断当前是否还有剩余票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卖票ticket60
窗口A卖票ticket59
窗口A卖票ticket58
窗口A卖票ticket57
窗口A卖票ticket56
窗口A卖票ticket55
窗口A卖票ticket54
窗口A卖票ticket53
窗口A卖票ticket52
窗口A卖票ticket51
窗口A卖票ticket50
窗口A卖票ticket49
窗口A卖票ticket48
窗口A卖票ticket47
窗口A卖票ticket46
窗口A卖票ticket45
窗口A卖票ticket44
窗口A卖票ticket43
窗口A卖票ticket42
窗口A卖票ticket41
窗口D卖票ticket40
窗口D卖票ticket39
窗口D卖票ticket38
窗口D卖票ticket37
窗口D卖票ticket36
窗口D卖票ticket35
窗口D卖票ticket34
窗口D卖票ticket33
窗口D卖票ticket32
窗口D卖票ticket31
窗口D卖票ticket30
窗口D卖票ticket29
窗口D卖票ticket28
窗口D卖票ticket27
窗口D卖票ticket26
窗口D卖票ticket25
窗口D卖票ticket24
窗口C卖票ticket23
窗口C卖票ticket22
窗口C卖票ticket21
窗口C卖票ticket20
窗口C卖票ticket19
窗口C卖票ticket18
窗口C卖票ticket17
窗口C卖票ticket16
窗口C卖票ticket15
窗口B卖票ticket14
窗口B卖票ticket13
窗口B卖票ticket12
窗口B卖票ticket11
窗口B卖票ticket10
窗口B卖票ticket9
窗口B卖票ticket8
窗口B卖票ticket7
窗口B卖票ticket6
窗口B卖票ticket5
窗口B卖票ticket4
窗口B卖票ticket3
窗口B卖票ticket2
窗口B卖票ticket1此时利用同步方法同样解决了同步操作的问题。但是在此处需要说明一个问题加入同步后明显比不加入同步慢许多所以同步的代码性能会很低但是数据的安全性会高或者可以称为线程安全性高。
那么在了解了以上知识后同步和异步有什么区别呢在什么情况下分别使用它们呢
如果一块数据要在多个线程间进行共享。例如正在写的数据以后可能被另一个线程读到或者正在读的数据可能已经被另一个线程写过了那么这些数据就是共享数据必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法并且不希望让程序等待方法的返回时就应该使用异步编程在很多情况下采用异步途径往往更有效率。
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 x0; x8; 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 x0; x8; 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 x0; x8; 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 x0; x8; 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 x0; x8; 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 x0; x8; 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/925179.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!