博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
多线程学习系列 - 1 - Single Threaded Execution Pattern
阅读量:4145 次
发布时间:2019-05-25

本文共 8608 字,大约阅读时间需要 28 分钟。

目录

在android里面多线程是非常普遍的

之前工作中并不涉及太多多线程的问题,所以也就一直没有系统学习过

这个系列的学习选的书为《java多线程设计模式》-结城浩 著,博硕文化 译

一提到设计模式,大家可能有种种想法,但是不管大家怎么想,我希望您能先简单读读这本书

我觉得它讲的通俗易懂,例子也比较有代表性,分析的很全面,还有课后习题及答案

最重要的一点,它确实很实用,废话不多说了

学习过程中笔记也肯定会有些不严谨的地方,希望大家在踩我的时候能加上几句指导的话语,在此笔者感激不尽

书中最开始讲了一些简单的线程知识,这里也就不做整理了

书中以例子为主,我觉得效果还不错,所以我的笔记也以书中例子为主

第一章:Single Threaded Execution Pattern

考虑这样一个问题:

去某些公司面试的时候进出需要安检,一个门一次只能允许一个人通过,通过的时候工作人员对你进行身份识别。

如果多个人一起,那么工作人员所掌握的信息很有可能变得混乱。下面我们来看看工作人员的抱怨

假设有三个Alice,Boddy,Chris,他们分别来自Alaska,Brazil,Canada

那么我们先以一种简单的方式来做一个简单的检查,如果名字的首字母=国家的首字母,那么我们就认为检查通过

public static class Gate{	private int counter = 0;	private String name;	private String address;		public void pass(String name, String address){		this.counter++;		this.name = name;		this.address = address;		check();	}		private void check(){		if(this.name.charAt(0) != this.address.charAt(0)){			System.out.println("broken:" + toString());		}	}		public String toString(){		return "No. " + this.counter + ":" + this.name + "," + this.address;	}}

  大门对通过的人员进行信息记录并检查,如果出错则会打印错误信息,注意这里的pass方法可以改变Gate一些属性的状态

下面的类是捣乱的人

public static class UserClass extends Thread{	private final Gate gate;	private final String name;	private final String address;		public UserClass(Gate gate, String name, String address){		this.gate = gate;		this.name = name;		this.address = address;	}		public void run(){		while(true){			gate.pass(this.name, this.address);		}	}}

 这个人会不断反反复复的经过大门,同时告诉大门自己的信息

最后来看看程序运作

public static void main(String[] args) {	Gate gate = new Gate();	new UserClass(gate, "Alice", "Alaska").start();	new UserClass(gate, "Boddy", "Brazil").start();	new UserClass(gate, "Chris", "Canada").start();}

 执行,结果让人很不满意

基本上马上就会有错误信息提示,log我就不贴了。。。

原因其实很简单,我觉得书中的表格不错

所以还是借来

线程Alice 线程Bruce this.name的值 this.address的值

this.counter++;

this.name = name;

this.address = address;

check();

this.counter++;

this.name = name;

this.address = address;

check();

(之前的值)

"Boddy"

"Alice"

"Alice"

"Alice"

"Alice"

broken...

(之前的值)

(之前的值)

(之前的值)

"Alaska"

"Brazil"

"Brazil"

上面是其中一种情况

工作人员智商有些问题,它一次只能记住一个名字和地址

Alice和Boddy都来到了门前,Bruce告诉工作人员他的名字,

工作人员记录名字:boddy

Alice来凑热闹把自己的名字和地址告诉了工作人员,这时候工作人员记录名字:Alice,地址Alaska

Boddy呢,这人他给忘了。。。

然后Boddy又告诉他自己的地址,于是工作人员脑中是这样记录的:名字Alice,地址Brazil

然后Alice和Boddy都等着工作人员核对(check()),于是悲剧发生了,工作人员认为这两个人都在骗他,所以发出了警告

发生这种情况的原因是Alice和Boddy非要争抢过安检,如果安检门弄小点,让他们一次最多过来一个,那就不会发生这种情况了

在这个例子中,也就是说:gate的pass方法一次只让一个线程调用,不允许Alice正在pass中呢,别人再凑过来

很幸运,java中用synchronized关键字就可以保证一次只有一个线程来执行这个方法

修改后的代码

public static class Gate{	private int counter = 0;	private String name;	private String address;		public synchronized void pass(String name, String address){		this.counter++;		this.name = name;		this.address = address;		check();	}		private void check(){		if(this.name.charAt(0) != this.address.charAt(0)){			System.out.println("broken:" + toString());		}	}		public synchronized String toString(){		return "No. " + this.counter + ":" + this.name + "," + this.address;	}}

 这时候再运行,则不会再出差错了

上面的代码只是给pass方法和toString方法加上了synchronized关键字

上面pass前面说过了

那么toString为什么也加上了synchronized关键字,为什么check没有加(课后习题3)

首先解释check的问题

check是private的,所以只能gate自己访问的到,在外面无法被调用。

gate里面pass调用了它,因为pass已经加上了synchronized,所以没有必要再给check函数设置此关键字

关于toString,由于它是public的,所以在外面可以被访问。

如果现在Alice正在调用toString,this.name = Alice,此时Boddy执行pass方法,把address改掉了,这时候Alice继续执行toString,那么悲剧发生了

所以,toString加上synchronized是必要的,这样执行toString方法时就不会被pass函数干扰了

在这个例子原始状态中,打印到出错的log时候counter已经到1000+了,如何让程序更快的看的错误log呢

其实不难,延长pass方法即可

public void pass(String name, String address) {    this.counter++;    this.name = name;    try {        Thread.sleep(1000);    } catch (InterruptedException e) {    }    this.address = address;    check();}

 pass方法执行时间变长了,出错的几率也就变高了(课后习题1)

关于Single Threaded Execution模式适用性,需要满足下面的条件:

1.多线程时

2.数据可被多个线程访问

3.状态可能改变

4.需要确保数据安全性

这些都很容易理解,不再加以解释

生命性与死锁

考虑下面的一种情况:

假设Alice和Boddy都是个吃货,他们打算享用意大利面,但是只有一套餐具:一把叉子一把汤匙

吃面的时候同时需要叉子和汤匙,Alice眼疾手快拿到了叉子,Boddy不甘示弱抢到了汤匙,这时候尴尬了

Alice等着Boddy放下汤匙,Boddy等着Alice放下叉子,然后他们就这么一直等下去,地老天荒,海枯石烂。。。

这时候就是所谓的死锁。

Single Threaded Execution满足那些条件时可能发生死锁呢?

1.具有多个参与者(SharedResource)

2.线程锁定一个SharedResource时,没有解除锁定就去锁定另一个SharedResource

3.获取SharedResource参与者的顺序不固定(和SharedResource参与者是对等的)

现在我们再看看吃货这个例子

1.多个参与者(SharedResource)相当于叉子和汤匙

2.Alice拿到了汤匙不放手就要去拿叉子

3.拿汤匙和叉子地位相同,不要求先拿谁

只要1,2,3其中一条被破坏了,那么就可以避免死锁的发生

下面用代码来描述这个例子(习题6)

餐具

public static class Tool{	private final String name;		public Tool(String name){		this.name = name;	}		public String toString(){		return "[" + name + "]";	}}

吃货

public static class EaterThread extends Thread{	private String name;	private Tool leftHand;	private Tool rightHand;		public EaterThread(String name, Tool leftHand, Tool rightHand){		this.name = name;		this.leftHand = leftHand;		this.rightHand = rightHand;	}		public void run(){		while(true){			eat();		}	}		public void eat(){		synchronized (leftHand) {			System.out.println(name + " takes up " + leftHand + "(left.)");			synchronized (rightHand) {				System.out.println(name + " takes up " + rightHand + "(right.)");				System.out.println(name + " is eating now ,yam yam!");				System.out.println(name + " put down " + rightHand + "(right.)");			}			System.out.println(name + " put down " + leftHand + "(left.)"); 		}	}}

public static void main(String[] args) {    Tool spoon = new Tool("Spoon");    Tool fork = new Tool("Fork");    new EaterThread("Alice", spoon, fork).start();    new EaterThread("Bobby", fork, spoon).start();}

 看看运行情况,果然他们没吃几口,就停住了

Alice takes up [Spoon](left.)

Alice takes up [Fork](right.)
Alice is eating now ,yam yam!
Alice put down [Fork](right.)
Alice put down [Spoon](left.)
Alice takes up [Spoon](left.)
Bobby takes up [Fork](left.)

原因也很简单,一个人先拿起一个餐具不放(synchronized (leftHand))的同时去拿另一个餐具,在他还没拿起来下一个餐具的时候,另一个人也打算eat,执行到这里的时候synchronized (leftHand),因为两个人拿到的是不同的餐具,所以leftHand指向不同的对象,代码并不相互影响所以程序继续执行,然后他们就发现无法获得另一把餐具,因为在对方手里

之前提到了Single Threaded Execution发生死锁的3个要素

我们来挨个尝试一下

首先破坏

1.具有多个参与者(SharedResource)

那么我们只提供叉子好了,你们都用一只手,有个吃相

public static class EaterThread extends Thread{	private String name;	private Tool hand;		public EaterThread(String name, Tool hand){		this.name = name;		this.hand = hand;	}		public void run(){		while(true){			eat();		}	}		public void eat(){		synchronized (hand) {			System.out.println(name + " takes up " + hand);			System.out.println(name + " is eating now ,yam yam!");			System.out.println(name + " put down " + hand);		}	}}public static void main(String[] args) {    Tool fork = new Tool("Fork");    new EaterThread("Alice",  fork).start();    new EaterThread("Bobby", fork).start();}

如果破坏

2.线程锁定一个SharedResource时,没有解除锁定就去锁定另一个SharedResource

习题答案的方式实际和上面差不多,如果只有一个工具那就不会打架了

下面引入一个新的类Pair

public static class Pair{	private Tool rightHand;	private Tool leftHand;	public Pair(Tool rightHand, Tool leftHand){		this.rightHand = rightHand;		this.leftHand = leftHand;	}		public String toString(){		return "[" + leftHand + "," + rightHand + "]";	}}

 改造EaterThread

public static class EaterThread extends Thread{	private String name;	private Tool leftHand;	private Tool rightHand;		public EaterThread(String name, Pair pair){		this.name = name;		this.leftHand = pair.leftHand;		this.rightHand = pair.rightHand;	}		public void run(){		while(true){			eat();		}	}		public void eat(){		synchronized (leftHand) {			System.out.println(name + " takes up " + leftHand + "(left.)");			synchronized (rightHand) {				System.out.println(name + " takes up " + rightHand + "(right.)");				System.out.println(name + " is eating now ,yam yam!");				System.out.println(name + " put down " + rightHand + "(right.)");			}			System.out.println(name + " put down " + leftHand + "(left.)");		}	}}

public static void main(String[] args) {    Tool spoon = new Tool("Spoon");    Tool fork = new Tool("Fork");    Pair p = new Pair(spoon, fork);    new EaterThread("Alice", p).start();    new EaterThread("Bobby", p).start();}
 

这样他们也会交替的你吃一会我吃一会了

最后来破坏

3.获取SharedResource参与者的顺序不固定(和SharedResource参与者是对等的)

这只需要让他们按着相同的顺序来拿餐具即可

public static void main(String[] args) {    Tool spoon = new Tool("Spoon");    Tool fork = new Tool("Fork");    new EaterThread("Alice", spoon, fork).start();    new EaterThread("Bobby", spoon, fork).start();}

笔记截图

至此这章内容就基本结束了

在书中的进阶说明里有一些关于synchronized的讨论

关于synchronized,我们需要思考

1.synchronized在保护什么

确定下来要保护的内容之后,要思考一下,其他地方有没有需要保护的,为什么

2.该以什么单位保护

比如我需要同时获得叉子和汤匙,这是一个完整的动作不能分割(当然也不是不能分割,分割后就会出现大眼瞪小眼的情况),那么

public synchronized void setName(String name){	this.name = name;}public synchronized void setAddress(String address){	this.address = address;}

 这种形式就没有意义,并不安全(要把它当做一整体,可以像Pair那样封装一下)

3.获取谁的锁定来保护

调用synchronized方法是获得实例this的锁定,如果实例不同,锁也就不同,所以不同实例可同时执行synchronized的相同的方法。使用synchronized块的时候也是需要获得对象的锁,所以需要考虑好获得谁的锁。书中给出一个比喻:获得错误的锁就好比想要保护自己的家,却锁上了邻居的门

转贴请保留以下链接

本人blog地址

转载地址:http://aycti.baihongyu.com/

你可能感兴趣的文章
C++模板
查看>>
【C#】如何实现一个迭代器
查看>>
【C#】利用Conditional属性完成编译忽略
查看>>
VUe+webpack构建单页router应用(一)
查看>>
(python版)《剑指Offer》JZ01:二维数组中的查找
查看>>
Spring MVC中使用Thymeleaf模板引擎
查看>>
深入了解php底层机制
查看>>
PHP中的stdClass 【转】
查看>>
XHProf-php轻量级的性能分析工具
查看>>
OpenCV gpu模块样例注释:video_reader.cpp
查看>>
就在昨天,全球 42 亿 IPv4 地址宣告耗尽!
查看>>
Mysql复制表以及复制数据库
查看>>
Linux分区方案
查看>>
如何使用 systemd 中的定时器
查看>>
git命令速查表
查看>>
linux进程监控和自动重启的简单实现
查看>>
OpenFeign学习(三):OpenFeign配置生成代理对象
查看>>
OpenFeign学习(四):OpenFeign的方法同步请求执行
查看>>
OpenFeign学习(六):OpenFign进行表单提交参数或传输文件
查看>>
Ribbon 学习(二):Spring Cloud Ribbon 加载配置原理
查看>>