文章目录

  • 8.6 多线程
    • 1、不相关的线程
    • 2、相关但无须同步的线程
    • 3、同步线程
    • 4、交互式线程

8.6 多线程

大多数网络应用程序一般都有多个线程同时运行,与多进程不同,由于多线程在同一个进程内运行,不必考虑共享内存的问题,不需要类似进程间通信的各种IPC ( InterProcess Communication)机制,使线程间通信问题简化了许多。但多线程与多进程类似,也是并发执行的,线程并发是指多个线程同时处理多个不同的操作。并发线程间存在一定的制约关系,为了协调它们之间的关系,合理共享资源,并发线程也需要实现同步。

**同步( Synchronization)是操作系统术语,是指存在竞争资源的情况下,并发进程之间的协调,以达到资源共享和进程合作。**线程同步与进程同步的原理相同,而且进程同步的处理机制同样也适用于线程同步。例如一个网络棋牌游戏服务器产生多个线程来响应多个在线玩家的请求,每个线程都需要访问到共享棋牌资源,某一时刻一个玩家线程出牌时,其余的玩家线程保持等待,这就需要线程同步来协调玩家线程之间的关系。根据线程间协调的程度以及使用线程的难度,多线程编程大致可以分为下列4个级别:不相关的线程、相关不需要同步的线程、同步线程、交互式同步线程。

1、不相关的线程

不相关的线程是一种最简单的线程程序,线程之间没有任何交互关系,各自执行不同的任务而已。如之前举例中的线程Lamb和Wolf就是两个毫无关系的线程,各自执行自己的操作,相互之间没有任何联系,这类线程的实际应用价值不大。

2、相关但无须同步的线程

相关但无须同步的线程表示线程间存在某种关系,却相互独立,没有依赖关系。这种情况生活中很多,比如超市里往往在顾客多时多开若干收银台,多个收银台同时为顾客提供服务,收银台之间没有影响,互不干涉,共同完成收银这项工作。

这种线程往往作用于同一任务的不同数据部分,线程之间没有交互关系,没有竞争资源,无须同步。这种关系适用于多个线程共同完成同-一个任务,以提高工作效率为目的。比如从网上下载歌曲,10个线程同时的下载速度就比单线程下载要快得多,10个线程各自运行,它们之间没有相互影响,这个线程下载量多一点, 那个下载量少一点,都没有关系,共同完成同一项下载任务。

举例:模拟三个线程异步执行500个顾客的收银任务

class Casher implements Runnable{int customer = 500;//将共享的资源@Overridepublic void run() {while (customer>0){//仍有顾客System.out.println(Thread.currentThread().getName()+"is still open"+customer--);}}
}
public class Count {public static void main(String[] args) {Casher t = new Casher();new Thread(t).start();new Thread(t).start();new Thread(t).start();//开启三个线程}
}

此程序产生三个相同的线程同时处理500个客户的收银事物,有效的提高了效率

3、同步线程

同步(synchronization)的目的在于协调线程之间的制约关系,保证多线程活动步调一致。主要是针对在多线程中存在共享竞争资源的环境下,某一时间段两个或多个线程都想使用同一资源,从而造成资源冲突。好比一间教室是一个 共享资源,为防止使用上发生冲突,排课时把不同的班级安排在不同的时间段上课,这就需要同步机制来协调。与前面的两种线程编程方式相比,同步程序变得相对复杂些。以“生产者/消费者”问题为例,该问题是操作系统典型的进程同步问题,简单表述即生产者和消费者都是多个并发进程,生产者负责产
生产品,存放到共享存储区中,消费者从存储区取出产品,只要存储区没有满,生产者就可以持续放产品,反之,只要存储区没有空,消费者就可以持续取出产品,因此生产者和消费者是可以并行执行的,它们之间是一种供求 关系。操作系统提供了信号量(semaphore) 机制来解决生产者和消费者同步问题,该机制同样也适用于同步线程,Java API提供了相应的方来实现线程之间的同步。

线程(或进程)同步可以用互斥( mutual exclusive) 控制的方式来解决,互斥意味着一个线程访问共享资源时,其他线程不允许访问,因此,生产者和消费者不能够并行执行。在后面的高级并发中,将介绍另一种线程同步的实现方式,采用阻塞队列来实现生产者与消费
者的真正并行运行。这里从程序的角度,考虑简化的“生产者/消费者”理想模式。在下例中,存储区有一定容量,规定只能先放后取,生产者在存储区空时放产品,放满后,消费者从存储区取出产品,它们之间的关系如图所示。

示例:

理想模式下的生产者/消费者示例:生产者Producer(Producer.java)生产出产品Product(Product.java),存放在一个存储区Storage(Storage.java)里,消费者Consumer(Consumer.java)从Storage对象里取出产品,Storage 提供了相应的存放产品的方法push()和取出产品的方法pop(), Producer 和Consumer通过访问Storage对象来完成存放和取出产品操作。Producer 与Consumer不允许同时访问Storage,首先存储区是空的,Producer 放入产品,等存储区满后,Consumer 再取出产品。

代码实现如下:

首先创建产品类Product.java

public class Product {int id;String name;public Product(int id, String name) {this.id = id;this.name = name;}@Overridepublic String toString() {return "Product{" +"id=" + id +", name='" + name + '\'' +'}';}
}

然后定义Storage类,类中定义存取产品的两个方法push()和pop()

public class Storage {Product[] products = new Product[10];int top = 0;boolean available = false;//生产者往仓库中放入产品public synchronized void push(Product product){//把产品放进仓库products[top++] = product;System.out.println(Thread.currentThread().getName()+" creates "+product);available = true;}//消费者从仓库中取出产品public synchronized Product pop(){//从仓库中取产品--top;Product p = new Product(products[top].id,products[top].name);products[top]=null;System.out.println(Thread.currentThread().getName()+" buy "+p);available = false;//仓库空了return p;}
}

定义Producer线程,往仓库中放入产品

public class Producer implements Runnable {private Storage storage;public Producer(Storage storage) {this.storage = storage;}@Overridepublic void run() {for (int i = 0; i <storage.products.length ; i++) {Product product = new Product(i,"ipad"+i);storage.push(product);}}
}

定义Consumer线程,从仓库中取出产品

public class Consumer implements Runnable {Storage storage;public Consumer(Storage storage) {this.storage = storage;}@Overridepublic void run() {//consumer让Producer先运行try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}for (int i = 0; i <storage.products.length ; i++) {storage.pop();}}
}

定义测试类

public class ProducerAndConsumer {public static void main(String[] args) {Storage storage = new Storage();Thread producer = new Thread(new Producer(storage));producer.setName("producer");Thread consumer = new Thread(new Consumer(storage));consumer.setName("constomer");producer.start();consumer.start();}
}

运行结果如下:

producer creates Product{id=0, name='ipad0'}
producer creates Product{id=1, name='ipad1'}
producer creates Product{id=2, name='ipad2'}
producer creates Product{id=3, name='ipad3'}
producer creates Product{id=4, name='ipad4'}
producer creates Product{id=5, name='ipad5'}
producer creates Product{id=6, name='ipad6'}
producer creates Product{id=7, name='ipad7'}
producer creates Product{id=8, name='ipad8'}
producer creates Product{id=9, name='ipad9'}
constomer buy Product{id=9, name='ipad9'}
constomer buy Product{id=8, name='ipad8'}
constomer buy Product{id=7, name='ipad7'}
constomer buy Product{id=6, name='ipad6'}
constomer buy Product{id=5, name='ipad5'}
constomer buy Product{id=4, name='ipad4'}
constomer buy Product{id=3, name='ipad3'}
constomer buy Product{id=2, name='ipad2'}
constomer buy Product{id=1, name='ipad1'}
constomer buy Product{id=0, name='ipad0'}

此例测试类中只定义了一个Producer线程生产并放入10个产品,一个Consumer线程取走10个产品,为使Producer先运行,Consumer 通过调用sleep0 让出了CPU。Storage.java中的pop()和push()方法前面均有关键字synchronized,这里使用到了“对象互斥锁"。Java中的互斥是通过引入“对象互斥锁" ( Mutual Exclusive Lock)来实现的,通过给某个共享数据对象加锁来保证一次只能有一个线程访问该对象,只有当该线程结束任务,释放了“锁"后,其他线程才能获得“锁”访问对象,从而实现不同线程对共享数据的同步操作。

对象互斥锁的概念就好比公共房间的一把锁,几个人共用一把锁, 任何一人开启了锁,使用房间时,其余的人就不能再用。直到他用完了,返还了锁,其他人才能继续使用。对象互斥锁使得多个线程能够在同一个共享数据上操作而互不干扰,某时刻只能有一个线程访问共
享数据。

Java提供了两种创建互斥锁的方式:同步方法和同步语句。

1)同步方法(SynchronizedMethod)为方法加锁。

在对共享数据进行操作的方法或代码段前加上关键字synchronized,这些方法或代码段称为临界区( Critical Section),临界区表示区内的代码只能被-一个线程执行,其声明格式如下:

synchronized 方法名{方法体...
}

上例中采用同步方式来实现,Storage类的push()方法和pop()方法前加了关键字synchronized,这两个方法即为临界区。当Producer使用到共享数据Storage对象,并通过该对象调用storage.push()时,Storage 对象就加上了锁,保证带有Synchronized的push()方法
在同一时刻只能被一个线程执行,此时,仅Producer线程可以访问Storage,执行存放产品的操作。而Consumner需等待Producer 的操作完毕,获取互斥锁后,进行取出产品的操作。同样道理,当线程Consumer通过Storage对象取出产品时,将调用临界区的pop()法,Storage对象又一-次被加上 “锁”,Consumer从Storage中取出产品,直到pop()执行完毕,Storage互斥锁才会被解开。

2)同步语句( Synchronized Statement)。

与同步方法不同的是,synchronized 关键字的放置位置,这里将需要互斥的语句段(如pop()push()代码段)放入synchronized(对象)
{}语句框中,即synchronizced直接锁定对象。声明格式如下:

synchronized (对象名){互斥语句...
}

示例:采用同步语句方式修改Storage.java,实现理想模式的生产者消费者示例,其余文件代码不变

public class Storage {Product[] products = new Product[10];int top = 0;boolean available = false;//生产者往仓库中放入产品public void push(Product product){synchronized (this){//把产品放进仓库products[top++] = product;System.out.println(Thread.currentThread().getName()+" creates "+product);available = true;}}//消费者从仓库中取出产品public Product pop(){synchronized(this){--top;Product p = new Product(products[top].id,products[top].name);products[top]=null;System.out.println(Thread.currentThread().getName()+" buy "+p);available = false;//仓库空了return p;}//从仓库中取产品}
}

4、交互式线程

生产者消费者模型采用对象互斥锁实现了线程同步,生产者访问共享资源时,消费者或其他线程就不能访问,只有生产者操作完成后,消费者才能继续操作。这里有一个问题,消费者怎么才能知道生产者何时完成操作,消费者总不能间歇地询问生产者,你什么时候做完
啊?所以希望生产者和消费者之间能够有所交流,消费者只需耐心等待,一旦生产者完成操作,马上通知消费者,好了,该你执行啦。

线程之间能够相互交流来传递信息,这就是交互式线程。当一个线程需要等待其他线程提供数据,而数据尚未就绪时,此线程需要处于等待状态而暂停执行,而当数据准备就绪时,再通知其他线程接收数据。Java 提供了典型的wait/notify 机制来实现进程交互,从而在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题,更有效地协调线程的同步工作。

下面在生产者消费者问题程序中添加wait/notify,实现生产者和消费者的交互。当生产者获取了互斥锁往仓库里放产品时,消费者处于wait状态,一旦生产者放满之后,将主动notify消费者可以来取货了,同时释放互斥锁;处于wait状态的消费者得到通知后,获取对象锁,从仓库里取货,此时生产者处于wait状态,待消费者操作完毕后,通知生产者可以存放产品,同时释放互斥锁。

wait/notify是Object类定义的方法,包括4个主要方法,其定义如表所示。

**示例:**修改例11-9的Storage.java代码,用wait/notify机制实现典型的生产者消费者模型,使生产者消费者之间相互通信。此例中测试类ProducersAndConsumers.java也更改为两个Consumer线程和一个Producer线程运行测试,删除Consumer.java中的sleep()语句,其余的代码没有变化。

代码如下:

首先创建产品类Product.java

public class Product {int id;String name;public Product(int id, String name) {this.id = id;this.name = name;}@Overridepublic String toString() {return "Product{" +"id=" + id +", name='" + name + '\'' +'}';}
}

然后定义Storage类,类中定义存取产品的两个方法push()和pop()

public class Storage {Product[] products = new Product[10];static int top = 0;//生产者往仓库中放入产品public synchronized void push(Product product){while (top==products.length){try {System.out.println("storage is full, producer is waiting");wait();} catch (InterruptedException e) {System.out.println("producer failed to wait");e.printStackTrace();}}//把产品放进仓库products[top++] = product;System.out.println(Thread.currentThread().getName()+" creates "+product);notify();//唤醒等待线程}//消费者从仓库中取出产品public synchronized Product pop(){while (top==0){try{System.out.println("storage is empty,consumer is waiting");wait();//仓库空,等待} catch (InterruptedException e) {System.out.println("consumer failed to wait");e.printStackTrace();}}//从仓库中取产品--top;Product p = new Product(products[top].id,products[top].name);products[top]=null;System.out.println(Thread.currentThread().getName()+" buy "+p);notify();return p;}
}

定义Producer线程,往仓库中放入产品

public class Producer implements Runnable {private Storage storage;public Producer(Storage storage) {this.storage = storage;}@Overridepublic void run() {for (int i = 0; i <storage.products.length ; i++) {Product product = new Product(i,"ipad"+i);storage.push(product);}}
}

定义Consumer线程,从仓库中取出产品

public class Consumer implements Runnable {Storage storage;public Consumer(Storage storage) {this.storage = storage;}@Overridepublic void run() {for (int i = 0; i <storage.products.length ; i++) {storage.pop();}}
}

定义测试类

public class ProducerAndConsumer {public static void main(String[] args) {Storage storage = new Storage();Thread producer = new Thread(new Producer(storage));producer.setName("producer");Thread consumer1 = new Thread(new Consumer(storage));Thread consumer2 = new Thread(new Consumer(storage));consumer1.setName("constomer1");consumer2.setName("constomer2");consumer1.start();consumer2.start();producer.start();}
}

每次的运行结果不一定相同,其中一次运行结果如下:

storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=0, name='ipad0'}
constomer1 buy Product{id=0, name='ipad0'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=1, name='ipad1'}
constomer1 buy Product{id=1, name='ipad1'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=2, name='ipad2'}
constomer1 buy Product{id=2, name='ipad2'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=3, name='ipad3'}
constomer1 buy Product{id=3, name='ipad3'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=4, name='ipad4'}
constomer1 buy Product{id=4, name='ipad4'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=5, name='ipad5'}
constomer1 buy Product{id=5, name='ipad5'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=6, name='ipad6'}
constomer1 buy Product{id=6, name='ipad6'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=7, name='ipad7'}
constomer1 buy Product{id=7, name='ipad7'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=8, name='ipad8'}
constomer1 buy Product{id=8, name='ipad8'}
storage is empty,consumer is waiting
storage is empty,consumer is waiting
producer creates Product{id=9, name='ipad9'}
constomer1 buy Product{id=9, name='ipad9'}
storage is empty,consumer is waiting

此例中,一个Producer生产并放入10个产品,两个Consumner总共需要取走20个产品,以上运行结果显示当Storage为空时,Consumer 处于等待状态,Producer 放入产品后,Consumer1 或Consumer2取走,由于三个线程的运行次序是随机的,所以放入产品和取走的次序也是不确定的。当两个Consumer相续取走了所有的10个产品之后,Storage 为空,这时两个Consumer总共还缺少10个产品,因而持续等待,程序没有退出。因此在waitnotify机制控制下的Producer和Consumer,不必遵循先放后取的规定,Producer和
Consumer的数量和运行先后次序也不受限制,这个例子基于互斥锁机制,完整地实现了典型的生产者消费者模型。

与上例相比,此例对Storagejava作了改进,在push(), pop()两个方法里添加了wait()notify()两个方法,这两个方法都需要对异常进行捕获或处理。当Producer准备往存储区中放入产品时,首先检查库存情况,如果是满的,调用wait()进入等待状态;反之,则放人产品,放完产品后立刻调用notify()通知Consumer可以取出产品了。同样的道理,当Consumer准备从存储区取出产品时,也先检验库存情况,如果是空的,调用wait()进人等待状态;反之,则直接取出产品。操作完毕后,调用notify()通知Producer可以放人产品了。如果有多个线程处于等待状态,则在通知时,使用notifyAll()代替notify(),以唤醒当前等待的所有线程。

在线程调用wait()方法时会自动释放互斥锁,并让出CPU给其他线程继续使用,这样不至于长久占住锁,导致其他线程出现“饥饿”,或者产生死锁现象; sleep() 方法则不同,在使用时仅仅让出CPU给其他线程,线程是不会释放互斥锁的。

典型生产者消费者模型的实现,应该明确以下几点:

1)若千生产者和若干消费者采用对象互斥锁来实现进程同步:synchronized和wait()notify() 机制配合使用。

2)生产者仅在存储区未满时放人产品,放满了则等待;消费者仅在存储区未空时才能取出产

4)若等待的生产者或消费者线程数量多,应采用一定 的数据结构,如队列来管理多线程的等待次序,依次轮流获取互斥锁。

编提示:需要注意的是, synchronized具有自动“上锁”和“解锁”的功能,当调用临界区代码时,线程自动上锁,其他线程无法使用共享数据,操作结束后,又可以自动“解锁",把互斥锁释放出来,使其他线程能够使用共享数据。

[ XJTUSE ]JAVA语言进阶知识—— 8.6 多线程相关推荐

  1. [ XJTUSE ]JAVA语言基础知识——2.2 Java基本数据类型

    文章目录 2.2 Java基本数据类型 1.布尔型 2.字符类型 3.整型 4.浮点数类型 5.数据类型转换 2.2 Java基本数据类型 Java提供的数据类型如下: 1.布尔型 布尔型只有true ...

  2. [ XJTUSE ]JAVA语言基础知识——7.12 JTable实现表格

    文章目录 7.12 JTable实现表格 7.12.1 创建简单表格 7.12.2 TableModel和监听器 7.12.3 TableColumnModel和监听器 7.12.4 实现列排序 7. ...

  3. Java工程师进阶知识完全扫盲, 太全了!!

    项目简介 本期介绍的开源项目名称叫做:advanced-java 中文名:互联网 Java 工程师进阶知识完全扫盲,该项目主要是为Java开发工程师提供进阶知识讲解,从而提升Java工程师技术与能力! ...

  4. 计算机java语言教程,计算机JAVA教程二讲Java语言基础知识.doc

    计算机JAVA教程二讲Java语言基础知识 2.1简单数据类型 2.1.1 标识符和保留字 1.标识符 程序员对程序中的各个元素加以命名时使用的命名记号称为标识符(identifier).Java语言 ...

  5. Java语言进阶:Channel(通道)

    Java语言进阶:Channel(通道) Channel概述 Channel(通道):Channel是一个接口,可以通过它读取和写入数据, 可以把它看做是IO中的流,不同的是:Channel是双向的, ...

  6. Java语言进阶:网络编程入门

    Java语言进阶:网络编程 网络编程入门 C/S C/S结构 :全称为Client/Server结构,是指客户端和服务器结构.常见程序有QQ.迅雷等软件. 特点: 客户端和服务器是分开的,需要下载客户 ...

  7. java入门学习笔记(二)—— Eclipse入门学习之快捷键、java语言基础知识之各类关键字及其用法简析

    一.Eclipse入门学习 1. 快捷键 对于一个编辑器,快捷键必不可少,是十分好用且有效的工具. 对于一个初学者,首先掌握了如下快捷键. (很多通用的快捷键不多说) Ctrl + / -- 注释当前 ...

  8. Java语言基础知识(一)

    前言 Java是一门高级计算机语言,由美国Sun公司(Stanford University Network)在1995年推出(现Sun公司已经被Oracle公司收购 ).要想学好Java,基础是至关 ...

  9. Java学习进阶知识篇

    系列文章目录 提示:....................... 文章目录 系列文章目录 前言 一.类和对象 面向对象基本介绍 类的基本使用属性 构造方法和析构方法 对象 二. 总结 前言 提示:这 ...

最新文章

  1. [Machine Learning with Python] Data Visualization by Matplotlib Library
  2. linux怎样创建硬链接,Linux下创建软、硬链接
  3. android 线程方式打印log到sd卡
  4. 6.mybatis异常:SQL Mapper Configuration,Error parsing Mapper XML,Could not resolve type alias
  5. 用VS2010构建MASM的编程环境
  6. HTML5 Web SQL
  7. linq 分组求和的一般方法
  8. 面板大小调整_3天学会premiere完全自学教程-更改剪辑大小
  9. php 当前页面停留时间,PHP 记录页面停留时间
  10. 在CentOS中安装和部署nacos配置中心
  11. centos 升级 glibc和glibcxxx ,解决error: Failed dependencies等问题
  12. ZOJ 1315【Excuses, Excuses!】------2015年2月9日
  13. ZeroMQ接口函数之 :zmq_ipc – ZMQ本地进程间通信传输协议
  14. echart的基本使用方法
  15. LayaBox---TypeScript---基础数据类型
  16. php极光短信接口接入
  17. 审计一波appcms-持续更新。
  18. mysql 部分汉字乱码_一次mysql部分汉字乱码解决过程
  19. 分词 - 准确率评测
  20. vue2.0支持compiler

热门文章

  1. 微博登录显示服务器解析失败怎么办,微博air登录失败, air无法登录的原因 -电脑资料...
  2. 新绝代双骄3终极全攻略6
  3. HEIC文件怎么打开,如何将HEIC格式转换为JPG格式
  4. vite的搭建与使用
  5. uni-app 学习笔记
  6. Mac使用NATAPP完成内网穿透
  7. 卷积神经网络的形象理解
  8. 谷歌高质量外链怎么做?Google网站买英文外链可行吗?
  9. Firefox 禁止中国用户!!
  10. Prim 算法的实现