第十章--第一节:并发与线程安全
第十章:并行与分布式程序设计
第一节:并发与线程安全
问题一:并行程序设计两种模式
1.共享内存
- 两个处理器,共享内存
- 同一台机器上的两个程序,共享文件系统
- 同一个Java程序内的两个线程,共享Java对象
-
2.消息传递
- 网络上的两台计算机,通过网络连接通讯
- 浏览器和Web服务器,A请求页面,B发送页面数据给A
- 即时通讯软件的客户端和服务器
- 同一台计算机上的两个程序,通过管道连接进行通讯
问题二:进程、线程、时间切片
1.进程:私有空间,彼此隔离
- 拥有整台计算机的资源
- 多进程之间不共享内存
- 进程之间通过消息传递进行协作
- 一般来说,进程==程序==应用
- 但一个应用中可能包含多个进程
- JVM通常运行单一进程,但也可以创建新的进程。
2.线程:程序内部的控制机制
- 进程=虚拟机;线程=虚拟CPU
- 程序共享、资源共享,都隶属于进程
- 很难获得线程私有的内存空间
3.线程与进程的区别
问题四:线程的开发
- 每个应用至少有一个线程
- 主线程,可以创建其他的线程
创建线程的第一种方法:
1.从Thread类派生子类:(Seldom used) Subclassing Thread
方法:用Thread类实现了Runnable接口,但它其中的run方法什么都没做,所以用一个类做Thread的子类,提供它自己实现的run方法。用Thread.start()来开始一个新的线程。
创建线程的第二种方法:
2.用一个类来实现Runnable接口,实现run的方法
3.使用匿名内部类的方式创建一个线程
问题三:交错和竞争(Interleaving and Race Condition)
1.时间分片(Time slicing)
场景:①虽然有多线程,但只有一个核,每个时刻只能执行一个线程。②即使是多核CPU,进程/线程的数目也往往大于核的数目
方法:通过时间分片,在多个进程/线程之间共享处理器。(时间分片是由OS自动调度的)
2.竞争条件(Race Condition)
注意:单行、单条语句都未必是原子的。(是否为原子,有JVM决定)
*例:下列程序并发的执行时,可能会出现什么结果?
(答案:5、6、10、15、30、)
问题四:干扰线程自动交织的若干操作
1.线程的休眠操作
用法:Thread.sleep(time)(time的单位为毫秒)
实例:
- 将某个线程休眠,意味着其他线程得到更多的执行机会
- 进入休眠的线程不会失去对现有monitor或锁的所有权
2.线程的中断
用法:Thread.interrupt()
①通过线程的实例来调用interrupt()函数,向线程发出中断信号
②在其他线程里向t发出中断信号 t.interrupt()
③检查线程t是否被中断 t.isInterrupted()
- 当某个线程被中断后,一般来说应停止其run()中的执行,取决于程序员在run()中处理
- 一般来说,线程在收到中断信号时应该中断,直接终止;但是,线程收到其他线程发来的中断信号,并不意味着一定要“停止”…
实例:用sleep()接收外界传来的中断信号
实例:用函数来接收外部传来的中断信号
3.线程的放弃
方法:Thread.yield()
作用:使用该方法,线程告知调度器:我可以放弃CPU的占用权,从而可能引起调度器唤醒其他线程。(尽量避免在代码中使用)
实例:
4.线程的加入
方法:Thread.join()
作用:让当前线程保持执行,直到其执行结束(一般不需要这种显式指定线程执行次序)
实例:
问题五:线程的安全
含义:ADT或方法在多线程中要执行正确
四种线程安全的方式:
- 限制数据共享
- 共享不可变数据
- 共享线程安全的可变数据
- 同步机制共享共享线程不安全的可变数据,对外即为线程安全的ADT
问题六:线程安全的实现策略
1.策略一:约束(Confinement)
核心思想:线程之间不共享mutable数据类型
方法:
①将可变数据限制在单一线程内部,避免竞争
②不允许任何线程直接读写该数据
示例:
(避免使用全局变量)
2.策略二:不变性
方法:使用不可变数据类型和不可变引用,避免多线程之间的race condition。
- 不可变数据通常是线程安全的
- 如果ADT中使用了beneficent mutation(有益不变量),必须要通过“加锁”机制来保证线程安全
对线程安全中不变性的定义:
- 没有mutator的方法
- 所有的区域都是private和final的
- 没有表示外泄
- No mutation whatsoever of mutable objects in the rep – not even beneficent mutation
3.策略三:使用线程安全的数据类型
方法:如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。(在JDK中的类,文档中明确指明了是否threadsafe)(一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。原因:threadsafe的类一般性能上受影响)。
- List,Set,Map等集合类都是线程不安全的。
- Java API为这些集合类提供了进一步的decorator
-
-
- ***在使用synchronizedMap(hashMap)之后,不要再把参数hashMap共享给其他线程,不要保留别名,一定要彻底销毁.(可以用private static Map cache =Collections.synchronizedMap(new HashMap<>());的方式实例化集合类)
- 即使在线程安全的集合类上,使用iterator也是不安全的。(解决办法用lock机制,
)。
- ****需要注意用java提供的包装类包装集合后,只是将集合的每个操作都看成了原子操作,也就保证了每个操作内部的正确性,但是在两个操作之间不能保证集合类不被修改,因此需要用lock机制,例如
如果在isEmpty和get中间,将元素移除,也就产生了竞争。
例:判断下列变量是不是线程安全的
(buildingName在任何情况下都是安全的;companyNames则不一定)。
【前三种策略的核心思想:避免共享 --> 即使共享,也只能读/不可写(immutable) -->即使可写(mutable),共享的可写数据应自己具备在多线程之间协调的能力,即“使用线程安全的mutable ADT】
4.策略四:Locks and Synchronization
方法:Synchronization 同步和锁,程序员来负责多线程之间对mutable数据的共享操作,通过“同步”策略,避免多线程同时访问数据。
- 使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问
- Lock是Java语言提供的内嵌机制,每个object都有相关联的lock
- 注意:对可变数据类型的访问线程要互斥,必须使用同一个lock进行保护
- 当你要锁住一个类的时候,用ADT自己做lock是最方便的(synchronized(this).)(Monitor模式:ADT所有方法都是互斥访问)
- synchronized 等价于 synchronized(this)。synchronized 作为关键字加入到方法的签名中。锁住的是这个类的实例化对象,当调用有synchronized 标签的方法时,别的线程不能用this对象。(this指的是当前调用的类的对象)
- 对synchronized的方法,多个线程执行它时不允许interleave,也就是说“按原子的串行方式执行”。
- synchronized 与 synchronized(this)的区别
- 任何共享的mutable变量/对象必须被lock所保护
- 涉及到多个mutable变量的时候,它们必须被同一个lock所保护
例:回答下列问题
①:
②
③
问题七:应该在哪些地方用synchronized
- 同步机制给性能带来极大影响
- When you don’t need synchronization, don’t use it. 除非必要,否则不要用。Java中很多mutable的类型都不是thread safe就是这个原因
- 尽可能减小lock的范围
- Synchronized不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑lock。
- 所有关于threadsafe的设计决策也都要在ADT中记录下来。
问题八:死锁(Deadlock)、饥饿(Starvation)、活锁(Livelock)
1.死锁(Deadlock)
含义:多个线程竞争lock,相互等待对方释放lock
模式图:
2.饥饿(Starvation)
含义:因为其他线程lock时间太长,一个线程长时间无法获取其所需的资源访问权(lock),导致无法往下进行。
3.活锁(Livelock)
含义:....