JAVA并发设计模式学习笔记(一)—— JAVA多线程编程
这个专题主要讨论并发编程的问题,所有的讨论都是基于JAVA语言的(因其独特的内存模型以及原生对多线程的支持能力),不过本文传达的是一种分析的思路,任何有经验的朋友都能很轻松地将其扩展到任何一门语言。
注:本文的主要参考资料为结城浩所著《JAVA多线程设计模式》。
线程的英文名Thread,原意指“细丝”。在多线程程序中,若要追踪各个线程的轨迹,就会派生出一系列错综复杂的乱线团。假设在运行过程中,如果有人问到“请问现在执行到代码的哪一部分了?”,你需要多个手指头才能指出正确的地方。
当应用程序的规模、复杂程度达到一定程度时,并发设计是一个必将考虑到的问题,以下是一些常见的应用:
- GUI:以word为例,我们正在编辑一份大型的文档,此时执行“查找”操作;当word进行查找时,同时会出现一个“停止查找”的按钮,用户可以随时停止。此时就用到了多线程,其中一个线程在后台执行查找,另一个线程显示“停止查找”的按钮,一旦按下,则立即停止查找。两个操作交由不同的线程来处理,各线程可以专心负责自己的功能,因此也是模块化设计思想的一种体现。
- 比较耗时的I/O处理:由于磁盘、网络的IO操作消耗的时间远大于内存处理,如果在此段时间内,程序仅仅是等待而无法执行其它处理,性能会大打折扣。如果事先能将I/O处理和非I/O处理区分开来,这样就能够利用进行I/O处理时CPU空闲的间隙,进行其它处理了。
- 一个Server与多个Client:大部分Server都要求能够同时服务于1个以上的Client。Server本身并不知道何时会有Client接入,并且在Server中直接引入多个Client的设计,并不是十分优雅的方案;因此不妨设计成一旦有Client连接到Server,就会生成自动出来迎接这个Client的线程。这样一来,Server端的程序就可以简单地设计成好像只服务一个Client。当然,从J2SE 1.4开始,已经加入了新的NIO类库,不必利用线程也能进行兼具性能和扩充性的I/O处理,详情可参考JDK。
至于JAVA中线程的编码方式,无非是继承自抽象类Thread或者实现Runnable接口,想必各位读者都很熟悉了,这里就不复述了。
在多线程程序里,多个线程既然可以自由操作,当然就可能同时操作到同一实例。这个情况又是会造成不必要的麻烦。例如经典的银行取款问题,其“确认可用余款”这一部分的代码应该该为:
- if(可用余额大于欲提取金额)
- {
- 从可用余额中减掉欲提取金额
- }
这段逻辑本身并没有问题。先确认可用余额,再检查是否允许提取输入金额,如果系统决定可以领取,则从可用余额中减掉此金额,保证不会发生可用余额变为负数的情况。
但是,同时若有2个以上的线程执行这个程序的代码,可用余额可能会变成负数。比如:
- 初始化……
- 可用余额 = 1000元
- 欲提取金额 = 1000元
- 线程A——可用余额大于欲提取金额?
- 线程A——是
- <<<此时切换成线程B>>>
- 线程B——可用余额大于欲提取金额?
- 线程B——是
- 线程B——从可用余额中减掉欲提取金额
- 线程B——可用余额变为0元
- <<<此时切换成线程A>>>
- 线程A——从可用余额中减掉欲提取金额
- 线程A——可用余额变为-1000元
我们发现,由于时间差,可能会发生线程B夹在线程A的“确认可用余额”和“减去可用余额”之间的情况,这就会导致出现金额为负数的情况。
JAVA中使用synchronized来实现共享互斥,这就好比十字路口的红绿灯处理一样;当直向行车时绿灯时,另一边的横向车灯一定是红灯。synchronized也采用类似的“交通管制”的方式来实现线程间的互斥。
上述银行存取款的互斥实现如下
- public class Bank
- {
- private int money;
- private String name;
- public Bank(String name, int money)
- {
- this.money = money;
- this.name = name;
- }
- // 存款
- public synchronized void deposit(int m)
- {
- money += m;
- }
- // 取款
- public synchronized void withdraw(int m)
- {
- if (money >= m)
- {
- money -= m;
- return true; // 已取款
- }
- else
- {
- return false; // 余额不足
- }
- }
- public String getName()
- {
- return name;
- }
- }
当一个线程正在执行Bank实例的deposit或withdraw方法时,其他线程就不能执行同一实例的deposit以及withdraw方法。欲执行的线程必须排队等候。
也许会注意到,Bank类里还有一个非synchronized的方法——getName。无论其它线程是否正在执行同一实例的deposit、withdraw或者getName方法,都不妨碍它的执行。
synchronized阻挡的几种使用方式,如下
synchronized局部阻挡:如果需要“管制”的不是整个方法,而是方法的一部分,就使用此类阻挡,代码如下
- synchronized(表达式)
- {
- ……
- }
synchronized实例方法阻挡:如果需要“管制”的是整个实例方法,而是方法的一部分,就使用此类阻挡,代码如下
- synchronized void method()
- {
- ……
- }
这段代码在功能上与如下代码有异曲同工之妙
- void method()
- {
- synchronized(this)
- {
- ……
- }
- }
synchronized类方法阻挡:如果需要“管制”的是类方法,就使用此类阻挡,代码如下
- class Something
- {
- static synchronized void method()
- {
- ……
- }
- }
这段代码在功能上与如下代码有异曲同工之妙
- class Something
- {
- static void method()
- {
- synchronized(Something.class)
- {
- ……
- }
- }
- }
从上面可以看出,synchronized类阻挡其实质是用类对象作为锁定的对象去进行互斥的。
线面讲讲线程的协调。上面所说的是最简单的共享互斥,只要有线程再执行就乖乖地等候;现实工作中,我们需要的往往不止于此,比如:
- 若空间有空闲则继续写入,若没有则等候。
- 空间有空闲时,及时通知等待线程。
线程协调的具体实现将在下一章中介绍。
JAVA中提供了wait、notify、notifyAll以支持此类条件处理。这里要注意到以下几点:
- 如欲执行某一实例的wait方法,则首先必须获得该实例的锁;一旦进入wait set(线程的休息间),则自动释放该锁。
- 使用notify方法时,会从锁定实例的wait set中唤醒一个线程。同样的,线程必须首先获得锁定,才能调用notyfy方法。被唤醒的线程并不是立即就可以执行的,因为在此刻,notify的线程还一直占有锁。另外,假设执行notify时,wait set里有多于一个的线程在等待,具体选择那个线程是无法得知的,因此应用程序最好不要写成会因所选线程而有所变动的方式。
- notifyAll与notify基本相同,唯一区别在于它是唤醒所有线程而非一个。一般来说,使用notifyAll写的程序会更健壮一点。
转载:http://grunt1223.iteye.com/blog/876245
JAVA并发设计模式学习笔记(一)—— JAVA多线程编程的更多相关文章
- JAVA并发设计模式学习笔记(二)—— Single Threaded Execution Pattern
注:本文的主要参考资料为结城浩所著<JAVA多线程设计模式>. 单线程执行模式(Single Threaded Execution Pattern)是最简单的多线程设计模式,几乎所有其他的 ...
- (转)《深入理解java虚拟机》学习笔记1——Java内存结构
java虚拟机规范规定的java虚拟机内存其实就是java虚拟机运行时数据区,其架构如下: 其中方法区和堆是由所有线程共享的数据区. Java虚拟机栈,本地方法栈和程序计数器是线程隔离的数据区. (1 ...
- [转]C#学习笔记15——C#多线程编程
一.基本概念进程:当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源.而一个进程又是由多个线程所组成的.线程:线程是程序中的一个执行流,每个线程都有自己的专有寄存 ...
- GOF 的23种JAVA常用设计模式 学习笔记 持续更新中。。。。
前言: 设计模式,前人总结下留给后人更好的设计程序,为我们的程序代码提供一种思想与认知,如何去更好的写出优雅的代码,23种设计模式,是时候需要掌握它了. 1.工厂模式 大白话:比如你需要一辆汽车,你无 ...
- (转)《深入理解java虚拟机》学习笔记4——Java虚拟机垃圾收集器
Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法,年老代主要使用标记-整理垃圾回收算法,因此java虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器,JDK ...
- (转)《深入理解java虚拟机》学习笔记7——Java虚拟机类生命周期
C/C++等纯编译语言从源码到最终执行一般要经历:编译.连接和运行三个阶段,连接是在编译期间完成,而java在编译期间仅仅是将源码编译为Java虚拟机可以识别的字节码Class类文件,Java虚拟机对 ...
- (转)《深入理解java虚拟机》学习笔记5——Java Class类文件结构
Java语言从诞生之时就宣称一次编写,到处运行的跨平台特性,其实现原理是源码文件并没有直接编译成机器指令,而是编译成Java虚拟机可以识别和运行的字节码文件(Class类文件,*.class),字节码 ...
- (转)《深入理解java虚拟机》学习笔记2——Java内存溢出实例
通过简单的小例子程序,演示java虚拟机各部分内存溢出情况: (1).java堆溢出: Java堆用于存储实例对象,只要不断创建对象,并且保证GC Roots到对象之间有引用的可达,避免垃圾收集器回收 ...
- Java设计模式学习笔记(五) 单例模式
前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 使用单例模式的原因 以Windows任务管理器为例,在Windows系统中,任务管理器是唯 ...
随机推荐
- HTML5的离线存储有几种方式?
localStorage长期存储数据,浏览器关闭后数据不丢失: sessionStorage数据在浏览器关闭后自动删除.
- 为什么stm32有的外设在进行初始化的时候需要将寄存器重设为缺省值?不设置会怎么样?
首先,缺省值就是默认值的意思,默认值可以理解为设计芯片的人认为用这个参数,比较适中,起码不可能耽误你对某一模块进行驱动.然后,为什么除了默认值(缺省值),还有这么多其他的参数可以进行选择呢,那就要看你 ...
- Java知识总结----队列的使用
首先我们要知道使用队列的目的是什么?一般情况下,如果是一些及时消息的处理,并且处理时间很短的情况下是不需要使用队列的,直接阻塞式的方法调用就可以了.但是,如果在消息处理的时候特别费时间,这个时候如果有 ...
- 小米手机Root 刷机
需要备份的资料: miui系统资料:电话.短信.便签,有小米云账号wifi下自己会备份好的. 应用数据:微信.qq 聊天记录: UC浏览器收藏夹. root后, rootexplorer,选择某个文件 ...
- Squid Linux 代理服务器
简介: Squid 是 Linux/Unix 平台下最为流行的高性能免费应用层代理服务器,它具有权限管理灵活.性能高和效率快的特点. 代理服务器可以提供文件缓存.复制和地址过滤等服务,充分利用有限的出 ...
- STL - Vector迭代器简单应用之计算元素和
Description 用vector向量容器装入10个整数,然后,使用迭代器iterator和accumulate算法统计出这10个元素的和 Solution #include "stda ...
- EXPLAIN sql优化方法(2) Using temporary ; Using filesort
优化GROUP BY语句 默认情况下,MySQL对所有GROUP BY col1,col2...的字段进行排序.这与在查询中指定ORDER BY col1,col2...类似.因此,如果显式包括一 ...
- Hash表从了解到深入(浅谈)
· Hasn表,将一个数据进行Value化,再进行一个映射关系到Key直接进行访问的一个数据结构,这样可以通过直接的计算进行数据的访问和插入.关于Hash表的基本概念这里就不一一叙述,可以通过百度了解 ...
- 2018-2019第一学期Java助教心得
随着期末考试落下了帷幕,本学习也结束了回顾本学期的历程,对我影响最深的还是这学期很幸运的成为代老师的助教,这也是我第一次接触助教工作.刚开始的时候我心里也有很多的担心,怕自己胜任不了这份工作,但随着时 ...
- C# Equals
[C# Equals] 1.Object.Equals() The type of comparison between the current instance and the obj parame ...