高并发之多线程总结

1.  传统线程技术回顾

1.1             传统线程创建方式

1.继承Thread类并重写run()方法

 

非匿名实现类

 

匿名实现类

 

2.调用Thread的有参构造器,参数传入Runnable对象

 

 

扩展1

 

线程的start()方法调用后,会把线程交给线程调度器安排时间执行,具体什么时间执行,要看调度器的空闲程度,线程调度器调度线程的时候执行线程的run()方法

 

扩展2

 

查看Thread的run()方法源码,讲解Runnable调用的时机

 

1.2             多线程机制与程序运行效率

多线程机制会提高程序的运行效率吗?

 

多线程机制不一会提高程序的运行效率,可能会降低程序运行效率,

 

以下复制文件有两种方式,哪种速度快?

1.       到原始目录,全选,到目标目录进行粘贴

2.       到原始目录,复制第1个文件,到目标目录进行粘贴,然后立即复制第2个文件,到目标目录进行粘贴,…..,即逐个赋值粘贴,让多个文件同时进行复制粘贴操作

 

第一种速度快

 

 

为什么会有多线程下载速度比较快呢?

 

下载的终端机器本身并没有加快,只是抢占了服务器带宽,假设每个服务20k/s,如果抢到了100个服务,那就是2000k/s

 

1.3             volatile

可见性

 

立即可见

 

防止指令重排

 

前面加内存屏障,memory barrier、memory Fence

 

一个CPU访问内存时,不需要内存屏障

 

多个CPU访问内存时,需要内存屏障来保证一致性

 

volatile修饰的变量,赋值后,多执行了一个lock指令操作

 

不保证原子性

 

 

普通变量内存图

 

 

 

 

volatile变量内存图

 

 

 

1.4             线程状态转换模型

 

java.lang.Thread.State

 

 

 

1.5             练习题

题目:

下图中method4的结果是什么

 

 

 

运行的是子类的run方法

 

结果:

 

原因:

只会运行子类的run()方法,因为根据源码是先看子类有没有run()方法, 如果有,就覆盖父类的run()方法。所以执行子类的run()方法,如果子类没有run()方法,就运行构造器里的Runnable()的run()方法。

 

涉及到的另外一个知识点:匿名内部类对象的构造方法如何调用父类的非默认构造方法。

 

错误案例分享:看起某些程序员,跑到run方法外面去写java语句,所以,线程这一块不容忽视。

 

 

1.6             练习题

为什么大多数程序采用Runnable这种方式创建线程?

 

1.体现了面向对象性, 因为run()方法在Runnable对象里面和Thread对象解耦

 

2.Java不支持多继承,即class Mythread extendsParent1extendsThread是错误的

 

1.7             练习题

 

已知图1代码是正确的,那么图2的代码正确么?

 

1

 

2

 

 

不能,如果父类方法签名上面没有抛异常,当子类覆盖方法时,方法签名上面也不能抛出异常,但是子类的方法体里面可以抛异常

 

原因:java语法规定:子类覆盖父类方法要遵循的原则:子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等

 

 

1.8             练习题

[多选题] 以下程序的输出结果是什么?

 

 

 

 

A

111

222

111

 

 

B

222

111

111

 

 

C

  111

  222

 

 

D

222

111

 

 

 

 

2.  传统定时器技术回顾

2.1             原理:new Timer()后为什么整个程序不退出

背景

 

下图中的test1()方法为什么执行后一直不退出

 

 

Timer原理

1.讲解原理:

 

Timer类似产品经理,不断的通过schedule()方法把任务加到TaskQueue中去;

 

TimerThread类似程序员,从TaskQueue中取任务。

 

schedule()方法内部是同步的synchronized(queue),即程序员先停下手头所有任务,听经理安排完任务在工作,或者程序员先把某个紧急任务干完,再一起听经理布置需求。

 

TimerThread发现没有任务就wait()—这里是死等,直到被notify(),也就是new Timer()不退出的原因

 

2.讲解源码:java.util.Timer

 从Timer的无参构造器开始讲解,然后讲解thread的run()方法----解释new Timer()不退出的原因。

 

接着讲解java.util.Timer.schedule(TimerTask, long)方法----任务加到queue后还notify

 

其次讲解Timer类的TimerThread字段

最后讲解TaskQueue类

 

2.2             如何终止定制器

Timercancel()方法

 

类似经理直接叫员工下班

 

垃圾回收器回收

 

耗时很长

 

 

2.3             间隔N个时间后爆炸

 

 

2.4             N秒后第一次爆炸,然后每隔M秒爆炸

 

 

 

2.5             在时刻time爆炸

 

 

2.6             在时刻firstTime首次爆炸,然后每隔M秒爆炸

 

 

 

2.7             每天凌晨3点定时爆炸

 

schedule(TimerTask task, Date firstTime, long period)

把period改为24 * 60 * 60 * 1000

firstTime指定为凌晨3点

 

2.8             周一到周五凌晨3点爆炸,周六周日不爆炸

思路1:数学功底计算

 

 

思路2:使用开源工具quartz,大约15分钟即可学会

 

2.9             scheduleAtFixedRate

scheduleAtFixedRate和schedule的区别

场景1:如果第一次执行时间被delay

(1)schedule方法:“fixed-delay”;如果第一次执行时间被delay了,随后的执行时间按 照 上一次 实际执行完成的时间点 进行计算

(2)scheduleAtFixedRate方法:“fixed-rate”;如果第一次执行时间被delay了,随后的执行时间按照 上一次开始的 时间点 进行计算,并且为了”catch up”会多次执行任务,TimerTask中的执行体需要考虑同步

 

场景2:如果执行任务的时间大于周期间隔

(1)schedule方法:下一次执行时间相对于 上一次 实际执行完成的时间点 ,因此执行时间会不断延后

(2)scheduleAtFixedRate方法:下一次执行时间相对于上一次开始的 时间点 ,因此执行时间不会延后,存在并发性

 

2.10       练习题

题目

实现以下功能:

2秒钟后爆炸一次,然后4秒钟后爆炸,

接着2秒钟后爆炸,然后继续4秒钟后爆炸,

如此循环下去

 

思路

 

两个TimerTask,互相启动,长江后浪推前浪,后浪消失前把前浪推出去

 

 

3.  传统线程互斥(mutex)技术

3.1             线程安全的重要性

银行转账解释线程安全问题

 

假设原始余额是1000元,即balance = 1000;

扣款200元,即balance =  balance – 200;

如果正在计算count -200并且还没有赋值时,另外一个人给此账户汇款300元,且汇款操作在扣款操作的赋值前就全部完成,那么,汇款完毕后是1300元,扣款完毕后是800元

 

但是在现实生活中,扣款后再汇款的余额应该是1100元

 

演示不安全的代码enable3.TraditionalThreadSynchronized

 

3.2             局部变量锁、实例对象锁、Class对象锁

1.实例对象锁

 

public  synchronized  void  isSyncB() {

         };

synchronized(this){}

 

演示代码enable3.TraditionalThreadSynchronized

 

2.Class对象锁

 

public  static  synchronized  void cSyncB()

synchronized(Clazz){}

 

演示代码enable3.TraditionalThreadSynchronized

 

3.局部变量锁

 

public void println(boolean x) {

         String abc = "123456";

         synchronized(abc) {

       //xxx

}

}

 

演示代码enable3.TraditionalThreadSynchronized

 

3.3             练习题

题目

 

如下图的Something类, 假如有Something类的两个实例a与b,那么下列哪几组方法可以被1个以上线程同时访问呢?并发

 

   a.   x.isSyncA()与x.isSyncB()

   b.   x.isSyncA()与y.isSyncA()

   c.   x.cSyncA()与y.cSyncB()

   d.   x.isSyncA()与Something.cSyncA()

 

   a,互斥 都是使用x对象,且都是实例方法,互斥

   b,不互斥,一个锁定的是x对象,另外一个锁定的是y对象

   c,互斥,都是锁定的字节码对象,与实例对象x和y没有关系,互斥

   d,不互斥,一个锁定的是实例对象x,一个锁定的是字节码对象,不互斥

 

演示代码enable3.Something

 

———此例子来源于日本作者-结成浩的《java多线程设计模式》

 

4.  传统线程同步通信技术

4.1             通信机制—wait-notify机制

 

没轮到自己就等待wait(),死等

接到通知被唤醒notify、notifyAll

 

演示代码enable4.TraditionalThreadCommunicationTest

 

 

4.2             练习题

题目

 

有一条流水线,每天需要处理的产品数量是40个,现在有3个工人,名字分别是thread1、thread2、thread3,每个产品要求thread1先加工10次,然后thread2加工20次,最后thread3加工30次;3个工人都处理完,进行产品验收,验收合格通过才能进行下一个产品的处理,并且只有上一个产品合格时,才能进行下一个产品的处理。

 

现在假设每个产品都验收合格,即thread3处理完,立即进行下一个产品的处理,流程如下图,

请使用3个线程写出以上程序的代码

 

 

要求结果格式如下:   ”===”不算作输出结果的一部分

 

====================
thread1加工第1

thread1加工第2

…..

thread1加工第10

thread2加工第1次

thread2加工第2次

…..

thread2加工第20次

thread3加工第1

thread3加工第2

…..

thread3加工第30

====================

thread1加工第1

thread1加工第2

…..

thread1加工第10

thread2加工第1次

thread2加工第2次

…..

thread2加工第20次

thread3加工第1

thread3加工第2

…..

thread3加工第30

====================

…..

 

演示代码enable4.Flowline3Worker

 

答案亮点 高内聚

 

使用高内聚。类似web程序cookie的加密方法和解密方法放在同一个类中,所以最好把3个线程的处理代码放在同一个类中

 

经验:要用到共同数据(包括同步锁)或共同算法的的若干个方法应该归在同一个类身上,这种设计正好体现了高内聚和程序的健壮性

 

答案亮点 线程通信

 

使用synchronized同步, notifyAll唤醒

 

答案亮点3 防止虚假唤醒(伪唤醒)

 

jdk-api  在循环中使用object.wait()

中断和虚假唤醒都是可能的,所以此方法应该总是使用在循环中

虚假唤醒是指没有被通知的时候被唤醒

 

使用notifyAll进行唤醒的缺点

 

唤醒所有,不能定向唤醒, CPU浪费在线程上下文切换上

 

扩展:eclipse控制台输出到文件

Run As  à  Run Configuration  à  Common à

 Standard  Input  and Output à Output File

5.  线程范围内共享变量的概念与作用

5.1             怎么实现

概念

 

每个线程一份,线程内共享,线程外独立

 

 

实现

见代码enable5.ThreadScopeSharedData

 

5.2             用在何处

 

每个线程1个独立的连接,事务保证,begin()和commit()

 

线程内每个模块是独立的,比如转出模块,转入模块

 

线程内数据是共享的,比如connection1在线程1内是共享的,connection2在线程2内是共享的

 

线程外又是独立的,比如connection1和线程2是独立的,connection2和线程1是独立的,

 

 

5.3             练习题

题目

 

下面程序报NullPointerException的原因是什么?有哪些解决方式

代码见enable5.ThreadScopeSharedDatawithException

提示:

可能遇到的障碍1:不知道new A().get()和new B().get()是连续被同一个线程调用,即不理解Thread.currentThread()的本质

 

解答

threadData.put()的时候两个线程的bucketIndex是同一个值,导致链表中元素个数只有1个,那么肯定有一个线程执行threadData.get()时候拿到的是空值

 

根本原因:HashMap多线程不安全

 

解决方式1

 

使用ConcurrentHashMap代替HashMap

使用java.util.Collections.synchronizedMap代替HashMap

private static Map<Thread, Integer> threadData = new ConcurrentHashMap<Thread, Integer>();

 

解决方式2

依然使用HashMap的基础上,threadData.put()的时候进行同步

6.  ThreadLocal类

6.1             ThreadLocal类

背景

 

需求:每个线程持有一份自己的私有数据,ThreadLocal就实现了这个功能

 

ThreadLocal.set(value)

 

往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。

 

ThreadLocal.get()

 

获取当前线程已经保存的内存

 

ThreadLocal. remove()

 

线程只要死亡,ThreadLocal中保存的内容都会成为垃圾回收对象

 

在线程结束时可以调用ThreadLocal. remove() 方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。

 

问题:

java中有线程死亡时候的回调函数么?

https://www.zhihu.com/question/23152141

 

6.2             封装多个变量

背景

ThreadLocal类只能保存一个变量,如果一个业务需要多个变量,怎么办呢

 

比如需要保存姓名、年龄、邮箱,ThreadLocal是否满足需求?

 

 

常见思路

 

把所需要的变量封装为对象,即ThreadLocal<UserInfo>

 

演示代码enable6.ThreadLocalTest2

 

缺点

 

没有高度封装,直接暴露ThreadLocal给外部,是否有风险?

 

 

6.3             封装多个变量并隐藏实现

 

演示代码enable6.ThreadLocalTest3

 

6.4             源码分析

java.lang.ThreadLocal.set(T)

 

 

java.lang.ThreadLocal.ThreadLocalMap

 

ThreadLocalMap是每个线程都有一个

 

 

6.5             ThreadLocal的应用场景

Strut2框架

例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,

 

对于不同的线程来说,getContext方法拿到的对象都不相同,

 

同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个

 

银行转账

银行转账包含一系列操作:

 把转出帐户的余额减少,

把转入帐户的余额增加,

这两个操作要在同一个事务中完成,

它们必须使用相同的数据库连接对象,

转入和转出操作的代码分别是两个不同的帐户对象的方法。

订单处理

订单处理包含一系列操作:

减少库存量、

增加一条流水台账、

修改总账,

这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,

如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。

 

7.  多个线程之间共享数据的方式

7.1             每个线程代码相同

可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,

 

例如,买票系统就可以这么做。

 

 

 

7.2             每个线程代码不同

 

用不同的Runnable对象,有如下3种方式来实现这些Runnable对象之间的数据共享

 

方式1

将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信

 

+=======================代码示例================================+

 

演示代码enable7.MutilThreadShareData1

 

将共享数据、每个线程对共享数据的操作方法封装在另外一个对象中,

 

将这个对象逐一传递给各个Runnable对象

 

实际调用

 

方式2

将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。

 

+=======================代码示例================================+

 

演示代码enable7.MutilThreadShareData2

 

 

方式3

方式1和方式2两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。

 

+=======================代码示例================================+

 

演示代码enable7.MutilThreadShareData3

 

将共享数据、每个线程对共享数据的操作方法封装在另外一个对象中

 

 

 

总结

 

要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信

 

7.3             练习题

 

设计四个线程,其中2个线程每次对j增加1,另外2个线程每次对j减少1,学出程序。

 

你有几种实现方案?

 

 

8.  并发库之原子类

前7章都是java5之前就有的线程同步功能

java5引入了并发库

8.1             原子类

概念

 

基本类型进行操作,这些基本类型

可以是对象中每个字段【AtomicLongFieldUpdater】,

也可以是局部变量【AtomicInteger】,

也可以是数组中的局部变量【AtomicIntegerArray】,

也可以是引用【AtomicReference】

也可以是对象中的引用【AtomicReferenceFieldUpdater】

 

优点

 

不需要进行同步即可自动时间线程安全

 

底层原理

 

CAS指令集的发展的产物,原子CPU指令

 

compar and swap

 

原子类是并发库的一部分,是jdk1.5引入的,

 

在引入并发库之前,只能使用同步实现线程安全,另外一个原因是早期CPU没有提供具有原子性的CAS指令CPU指令

 

9.  并发库之线程池

9.1             线程池背景

接待客户

 

 

 

9.2             创建线程池

 

使用java.util.concurrent.Executor类

 

固定线程池

 

演示代码enable9.ThreadPoolTest1.test1()

 

缓存线程池

 

演示代码enable9.ThreadPoolTest1.test2()

 

ExecutorService threadPool = Executors.newCachedThreadPool();

 

======jdk解释========

线程不够就自动创建

If no existing thread is available, a new thread will be created and added to the pool.

 

线程超过1分钟不被使用就被终止和移除

Threads that have not been used for sixty seconds are terminated and removed from the cache.

 

保持长时间的空闲不会浪费系统资源

Thus, a pool that remains idle for long enough will not consume any resources.

 

单一线程池(如何实现线程死亡后重新启动)

 

演示代码enable9.ThreadPoolTest1.test2()

 

这里的“线程死亡后重新启动”是指一个线程死掉后,找一个替补,不是真正地又活过来了

 

ExecutorService threadPool = Executors.newSingleThreadExecutor();

 

Note however that if this single thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks.

 

9.3             关闭线程池

 

shutdown:执行完了再关闭

 

shutdownNow

 

 

9.4             线程池启动定时器

N个时间后执行

 

Executors.newScheduledThreadPool(3).schedule

 

演示代码enable9.ThreadPoolTest1.test3()

 

固定时间首次执行,然后每隔M个时间执行

 

Executors.newScheduledThreadPool(3).scheduleAtFixedRate

 

绝对时间和日期

 

不能指定绝对时间和日期执行,不过可以使用如下方式转换

 

schedule(task, futureDate.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)转变

 

==jdk api==

All schedule methods accept relative delays and periods as arguments, not absolute times or dates

 to schedule at a certain future date, you can use: schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)

 

9.5             练习题

 

不使用并发库,如何自己实现线程池?

 

如何自己实现JDBC连接池?

 

http://blog.csdn.net/shijinupc/article/details/7836129

 

 

10.        Callable与Future

10.1       ExecutorService 与Callable、Future的基本使用

Callable Runnable2个接口的区别

 

Runnable是执行工作的独立任务,但是它不返回任何值。

 

在Java SE5中引入的Callable是一种具有类型参数泛型,它的类型参数表的是从方法call()中返回的值,并且必须使用ExecutorServices.submit()方法调用它

 

A task that returns a result and may throw an exception.

 

The Callable interface is similar to Runnable, in that both are designed for classes whose instances are potentially executed by another thread.

 

A Runnable, however, does not return a result and cannot throw a checked exception

 

演示代码enable10.CallAndFutureTest

 

ExecutorService.submit(Callable<T> task)

 

Future.get()

 

同步,一直等,直到有结果

 

Future.get(long timeout, TimeUnit unit)

 

超过时间还没有结果,报超时异常

 

Future.isDone()

 

异步方法

 

也可以不使用isDone(),直接使用get()进行同步获取结果

 

10.2       CompletionService与Callable、Future的基本使用

背景

 

农夫种了10块麦田,丰收的季节临近,农夫等待收割,哪个麦田成熟了收割哪块

 

CompletionService

 

演示代码enable10.CallAndFutureTest2

 

 

10.3       多线程之Promise(承诺))设计模式

promise模式概念

 

Promise模式不属于23中设计模式

 

 

类图(显示系统中的类、接口以及它们之间的静态结构和关系的一种静态模型)

 

 

时序图(显示对象之间交互的图,这些对象是按时间顺序排列的)

 

 

10.4       练习题

题目

 

参考UML中的类图和时序图,使用Promise模式,完成下面的需求:

 

某系统的一个数据同步模块需要将一批本地文件上传到指定的目标FTP服务器上。

这些文件是根据页面中的输入条件查询数据库的相应记录生成的。

在将文件上传到目标服务器之前,需要对FTP客户端实例进行初始化(包括与对端服务器建立网络连接、向服务器发送登录用户和向服务器发送登录密码),

 

提示1:FTP客户端实例初始化这个操作比较耗时间

 

提示2:某些比较耗时间的操作可以使用Thread.sleep()方法进行模拟

 

参考答案

 

演示源码enable10.proimise.CaseRunner

 

需要引入的jar包:commons-net-3.3.jar

CaseRunner.java

DataSyncTask.java

FTPClientUtil.java

 

11.        并发库之线程锁

11.1       lock和synchronized同步的区别

对象范围

 

lock是能是lock的实现类实例对象

 

synchronized(obj)里面obj可以是任意实例对象

 

是否使用finally语句块

 

lock后一般使用finally语句块包住lock.unlock();保证安全

 

synchronized执行到synchronized的范围外,系统会自动释放锁

 

锁嵌套

 

synchronized嵌套只能再次嵌套一个synchronized代码块,效率低,代码不美观

 

lock直接写就行,代码美观

 

 

11.2       传统synchronized和并发库的线程通信的区别

组合方式

 

传统线程通信是关键字synchronized和Object的wait-notify-notifyAll配套实现线程通信

 

并发库舍弃了Object的wait-notify-notifyAll方法,改为Condition对象的await-signal-signalAll,

即Lock和Condition配套使用实现线程通信

 

问题: Condition对象有wait()、notify()、notifyAll()方法么

 

公平锁

 

lock可以实现公平锁,synchronized只能是不公平锁

 

多路路由

 

lock可以实现多路指定唤醒,synchronized要么全部唤醒、要么随机唤醒一个,这个随机就带来了性能问题,比如下面的例子:

 

比如一个队列,4个线程在放数据的方法出排队,5个线程在取数据的方法出排队,如果某个线程出元素后队列为空,这时候应该唤醒的是数据的线程【唤醒异类】,但是如果唤醒的是数据的线程【唤醒同类】,会出现什么问题? 无论是使用notify()和notifyAll(),都会带来CPU的浪费,浪费在线程上下文切换上面

 

读写锁

 

lock可以实现读读锁不互斥、读写锁或写读锁互斥、写写锁互斥

 

synchronized只有互斥锁

 

12.        读写锁

12.1       验证读读不互斥、读写互斥、写写互斥

 

enable12.ReadWriteLockTest

 

 

12.2       Hibernate读写锁

背景

 

hibernate的session.load(id, User.class);和session.get(id, User.class);有什么区别?

 

有兴趣自己去研究:org.hibernate.internal.SessionImpl.load(String, Serializable)

 

Hibernatesession.load()方法讲解

 

User user = session.load(id, User.class);

load()的本质是代理对象。

 

当使用load方法来得到一个对象时,此时hibernate会使用延迟加载的机制来加载这个对象,即:当我们使用session.load()方法来加载一个对象时,此时并不会发出sql语句,当前得到的这个对象其实是一个代理对象,这个代理对象只保存了实体对象的id值,只有当我们要使用这个对象,得到其它属性时,这个时候才会发出sql语句,从数据库中去查询我们的对象。

 

 

load()相当于代理缓存,如果数据库没有直接抛异常

 

Hibernatesession.get()方法讲解

 

get()方法直接去数据库没有查到会返回null,不抛异常

 

相对于load的延迟加载方式,get就直接的多,当我们使用session.get()方法来得到一个对象时,不管我们使不使用这个对象,此时都会发出sql语句去从数据库中查询出来

 

12.3       练习题

题目

 

请你设计一个缓存系统

 

参考答案

 

在enable12.cacheSystem包里面,写了三种参考

CachedData1.java

CachedData2.java

CachedData3.java

 

13.        条件阻塞Condition的应用

13.1       第4章--《传统线程同步通信技术》练习题改进

问题

 

原来的notifyAll()唤醒有什么问题?

 

定向唤醒(多路通知)

 

演示代码enable13.ImprovedFlowline3Worker

 

13.2       多路通知--阻塞队列

背景

jdk-apiCondition的案例,阻塞队列,需求

 

演示代码enable13.BoundedBuffer

 

As an example, suppose we have a bounded buffer which supports put and take methods.

 

If a take is attempted on an empty buffer, then the thread will block until an item becomes available;

 

if a put is attempted on a full buffer, then the thread will block until a space becomes available. We would like to keep waiting put threads and take threads in separate wait-sets so that we can use the optimization of only notifying a single thread at a time when items or spaces become available in the buffer.

 

This can be achieved using two Condition instances

.

ArrayBlockingQueue

 

ArrayBlockingQueue

 

 

13.3       练习题

题目

 

”Condition”和”Lock”必须配套使用么,可以单独使用么?

 

”synchronized”和”wait、notify、notifyAll”, 可以单独使用么?

 

即同步时一定要通信么,通信时一定要同步么。

 

 

答案

 

答案演示代码enable13.ExerciseDemo

 

14.        Semaphere同步工具

14.1       概念

概念

semaphore  [ˈseməfɔ:(r)] 信号灯:

类似同一个资源的许可池,

获取许可【acquire()】,没有就等待

释放许可【release ()】

获取多个许可【acquire(int)】

比如在Windows下可以设置共享文件的最大客户端访问个数

Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource.

公平

 

默认不公平锁

 

14.2       基本使用

 

enable14.SemaphoreTest

 

14.3       Semaphere和资源配合实现资源池

 

enable14.Pool

 

如果不使用Semaphere,这个资源池如何实现?

一般做法是获取资源的时候,如果没有可用资源,线程就等待,然后一旦释放资源就通知等待的线程,即实现线程间的通信,需要我们实现。

 

类似情景:去饭店吃饭,取票排队,座位满了,后来的客人就要排队,前台负责叫号,通知可以进去吃饭了。Semaphere在这里就相当于这个”前台”。

 

14.4       单信号灯和互斥的区别

 

 

A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many Lock implementations), that the "lock"can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such asdeadlockrecovery

 

不需要使用同步锁

不需要调用通知

 

只有单许可信号灯才能让一个线程的许可被另外一个线程释放,多许可信号灯不能做到一个线程是否另外一个线程的许可

 

14.5       底层实现

 

链表队列

 

java.util.concurrent.locks.AbstractQueuedSynchronizer.Node。

 

semaphoreObj.acquire ()方法把调用的线程写在了节点里面

 

 

semaphoreObj.release()方法释放对应线程的许可

 

15.        CyclicBarrier同步工具

15.1       概念

cyclic英[ˈsaɪklɪk]

 

用处

A CyclicBarrier supports an optional Runnable command that is run once per barrier point, after the last thread in the party arrives, but before any threads are released. This barrier action is useful forupdating shared-state before any of the parties continue

15.2       基本使用

enable15.CyclicBarrierTest

15.3       并行设计

enable15.Solver

16.        CountDownLatch同步工具

16.1       概念

CountDownLatch(int count)

根据指定的数量进行初始化

countDown()可以使当前的count减去1

await()方法会是线程阻塞,直到当前count是0的时候,自动唤醒

 

用途

当做开关或者门,所有的线程在门前面调用await()方法阻塞,当countDown()使当前count达到0时,门自动打开

16.2       应用1—裁判吹哨开始、评分

enable16.CountdownLatchTest

enable16.demo.Driver

16.3       应用2--任务划分

enable16.demo.Driver2

 

17.        Exchanger同步工具

17.1       概念和用法

概念

买卖双方互换内容,类似购买武器,买方用钱换武器,卖方用武器换钱,在一个地点接头,

完成交换,各自离开

 

用法

enable17.ExchangerTest

18.        阻塞队列的应用

18.1       队列分类

固定长度和不固定长度

固定长度,满了就报错或阻塞

 

阻塞队列和非阻塞队列

阻塞不报错,等待

非阻塞报错,不等待

 

QueueDeque

Queue就是普通的队列

Deque是double-ended queue双端队列

18.2       方法分类

 

 

不要把精力放在记忆这三种方法的区别上去,需要的时候去查文档

 

 

poll 美[poʊl] 得到

take 拿到

 

18.3       生产者-消费者

 

enable18.ProducerConsumerTest

 

如果不使用阻塞队列去实现生产者和消费者,需要做如下工作:

 

剩余产品不为空时,消费者就消费,消费到剩余产品为空时,消费者就等待,并唤醒生产者;

 

剩余产品为空时,生产者就生产,生产完了通知消费者,剩余产品过剩时,生产者就等待

 

 

借助阻塞队列实现线程间通信就省去了线程间通信的处理

 

18.4       练习题

 

Ø  使用Lock和Conditon实现生产者和消费者模型

 

Ø  使用synchronized和wait-notify-notifyAll实现生产者和消费者模型

 

19.        同步集合类的应用

19.1       同步集合类

线程不安全的传统集合类

 

HashSet

HashMap

ArrayList

 

没有并发库之前,使用java.util.Collections.synchronizedMap(Map<K, V>)保证线程安全

synchronizedMap实现同步的原理:把不安全的HashMap包装起来,效率低

 

同步集合类

 

ConcurrentHashMap

ConcurrentSkipListSet—有序,需要传入比较器

 

ConcurrentModificationException并发修改异常

 

enable19.CollectionModifyExceptionTest.test1()

 

ArrayList的java.util.AbstractList.Itr.hasNext()

 

出现异常的原因是什么?怎么避免

 

CollectionModifyException

 

CopyOnWriteArrayList可以避免ArrayList的移除时候的问题,意思是在写操作的时候进行复制,保证了安全性

 

源码见java.util.concurrent.CopyOnWriteArrayList.remove(Object, Object[], int)

在移除的时候复制了一下

19.2       练习题

问题

 

HashSet和HashMap的联系是什么? 

提示:问的是联系,不是区别,问的是底层实现有什么关联点

 

答案

 

HashSet底层使用HashMap实现的,只用了Key,没有使用Value

 

 

20.        java虚拟机实现多线程的模型

20.1       模型1--内核线程实现(1:1)

内核(kernel)

 

这里所说的内核是指操作系统内核,下文也一样,不是指CPU内核

 

分为单线程内核 和 多线程内核(Mutli-Threads kernel)

 

内核线程(Kernel Level ThreadKLT

 

距离CPU最近的线程,下面直接就是CPU了

 

调度器(scheduler)

 

对KLT进行调度

把线程的任务映射到各个处理器上

 

轻量级进程(Light Weight Process,LWP

 

LWP就是我们通常意义上所讲的线程;

内核线程的一种高级接口;

每个LWP对应一个KLT,1:1对应关系;

 

原理图

 

 

 

优点

实现简单,易理解

缺点

 

1.各种线程操作,比如创建、析构以及同步都需要进行系统调用,系统调用的代价相对较高,

需要在内核态和用户态中来回切换;

 

2.LWP的数量是有限的,因为每个LWP对应一个内核资源,内核资源是有限的

 

总结

 

基本完全依赖内核线程实现

 

 

20.2       模型2--用户线程实现(1:N)

用户线程概念

 

广义:一个线程只要不是内核线程,都是用户线程(User Thread,UT)

狭义:完全建立在用户空间的线程库上

 

优点

不需要内核支持,优点也是缺点

缺点

没有内核支持

原理图

 

 

总结

 

完全依赖用户程序自己实现的一种模式

 

20.3       模型2--用户线程 +轻量级进程混合实现(N:M)

混合的实现

 

N个用户线程对应M个LWP,N > M,

比如5个用户线程对应2个LWP

 

原理图

 

 

20.4       Java虚拟机线程的实现

JAVA虚拟机规范

 

没有强制规定

 

sun JDK

 

Windows和Linux都是一对一的,即每个java线程映射一个LWP

 

为什么只提供一对一模型?

因为Windows和Linux操作系统只提供了这种一对一的模型

 

Solaris

 

多对多(N:M): -XX:+UseLWPSynchronization(默认值)

 

一对一(1:1): -XX:+UseBoundThreads

 

21.        线程和进程基本知识

21.1       进程

 

进程是操作系统中除CPU外进行的资源分配和保护的基本单位

 

进程是操作系统对一个正在运行的程序的一种抽象。

 

在一个系统上可以同时运行多个进程,而每个进程都好像在独占的使用硬件

 

在多线程OS中,进程不是一个可执行的实体

 

21.2       线程

 

线程是CPU调度和分派的基本单位。

 

线程只能归属于一个进程并且它只能访问该进程所拥有的资源。

当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程

 

线程调度机制:抢占式调度模型、协同式线程调度(协同式已经被废弃)

 

CPU只会执行指令,本身不会进行上下文切换,无论是进程上下文切换还是线程上下文切换,都是操作系统内核把CPU需要的数据准备好,然后向CPU发送执行指令。

 

对CPU本身来说是没有程和线程之分的,进程和线程是操作系统级别的概念。

 

抢占式调度模型的理念是给线程分配时间片,不是CPU给线程分配,而是操作系统在下一个时间片就强制改变CPU的数据和执行指令了,是操作系统分配给CPU的时间。

 

 

 

22.        硬件效率与一致性

22.1       概念术语

Moore's Law (摩尔定律)

 

处理器晶体管数量与运行效率

 

当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度

 

Amdahl's Law(阿姆达尔定律)

 

 

阿姆达尔曾致力于并行处理系统的研究。

 

对于固定负载情况下描述并行处理效果的加速比S,阿姆达尔经过深入研究给出了如下公式:

 

S = 1/(1-a+a/n)

 

其中,a为并行计算部分所占比例,n为并行处理结点个数。

这样,

 

当1-a=0时,(即没有串行,只有并行)最大加速比s=n;

 

当a=0时(即只有串行,没有并行),最小加速比s=1;

 

当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限。

例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。

这一公式已被学术界所接受,并被称做“阿姆达尔定律”,也称为“安达尔定理”(Amdahl law)

 

====简单描述=====

 

并行化与串行化比重

 

并发处理的广泛应用是使得Amdah1定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类“压榨”计算机运算能力的最有力武器。

====问题===

 

根据Amdahl's Law,并行(多线程)一定会提高效率么?

 

TPSTransactions Per Second

 

衡量服务器性能好坏

 

QPS(Query Per Second)

 

衡量规定时间内所处理流量多少

 

 

22.2       高速缓存(Cache)

 

 

22.3       处理器

 

 

 

22.4       缓存一致性协议

 

 

 

22.5       主内存

 

 

 

22.6       总图

 

 

 

22.7       乱序执行优化

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值