课程进入了尾声。本周内容主要是线程安全相关。线程错误比一般的错误更加难以发现和修改,甚至加入一条print语句就能改变时间分片,从而导致错误消失。重点介绍了“锁”的机制,在使用时避免对整个方法进行lock,而是对可能发生线程不安全的指令进行lock操作,以免程序性能受到明显影响。同时避免“死锁”现象发生,在使用多个lock时注意顺序。
并发
1.并行:将程序布置在多个CPU上执行。
并发:将任务拆分为多个阶段,在同一个CPU上执行(切片)。
2.并发的两个模型:共享内存(只能用于线程)和消息传递(线程、进程)。
进程和线程
1.进程和线程都是并发模块的类型。进程比较“重量级”,私有空间,彼此隔离;线程“轻量级”,是程序内部的控制机制。一个进程可以形成多个线程。
2.每个应用至少有一个线程,主线程可以创建其它的线程。
3.创建线程的方法:继承Thread;从Runnable接口构造Thread对象。
创建线程需要调用Thread类的start方法,不能运行具体的run方法
交错(Interleaving)和竞争
1.时间分片:虽然有多个线程,但只有一个CPU,每个时刻只能执行一个线程。通过时间分片在多个进程/线程间共享处理器。由操作系统自动调度。
2.共享内存时的竞争情况
不恰当的分片操作会导致错误。右侧A和B发生了竞争/线程干扰。
共享内存多线程的运行结果实例,每条语句可划分为读取-计算-写回三个原子操作
3.消息传递时的竞争情况
由于时间分片,消息传递机制也无法解决竞争问题。
4.调用方法主动影响交错现象
①Thread.sleep(time) 线程休眠time毫秒
②Thread.interrupt() 向线程发出中断信号,例如t.interrupt(),即在其它线程里向t发出中断信号。收到中断信号后t不一定中断,由t本身决定。正常运行期间,即使收到中断信号也不会理会;在休眠时收到中断信号则抛出异常。
③Thread.join() 让线程保持执行,直到其执行结束。执行该操作时,也会检测其它线程发来的中断信号。
t1收到中断信号时,如果一次循环都未执行,则先输入if再输出else;如果执行一次,则先输出else再输出if;如果已经进行两次循环则输出两个else。此外t1和t2的start执行先后不定。
保证线程安全的策略
1.Confinement
要求类中方法不能访问属性(过于严格)。
2.Immutability
使用不可变数据类型和不可变引用,避免多个线程之间的竞争。如果是有益可变性,则需要通过“加锁”保证线程安全。
3.使用线程安全数据类型
如果必须使用mutable数据类型在多个线程之间共享数据,要使用线程安全的数据类型。一般来说JDK为ADT额外提供一个线程安全的类,但性能受影响。List、Map、Set都是线程不安全的,API提供集合类的包装,对每个操作都以原子操作进行。执行其上的某个操作是线程安全的,但如果多个操作放在一起,仍然不安全。//即使在线程安全的集合类上,使用iterator也是不安全的。除非使用lock机制。
4.锁和同步
①程序员负责多线程之间对mutable数据的共享操作,通过同步策略避免多个线程同时访问数据。使用锁机制,获得对数据的独家更改权,其它线程的访问被阻塞。
②Lock是Java的内嵌机制,每个对象都有相关联的lock。
③Lock用以保护共享数据。要实现互斥,则必须使用同一个lock进行保护。
④构造方法默认互斥,不需加锁。除非必要,否则不加锁;如果使用,要尽量缩小范围。因为会给性能带来极大影响。
⑤对静态方法进行加锁,会同时锁住类的所有对象。
⑥1和4正确。A获得了list的锁并不意味着其它对象不能获取/改变list的元素。对同一个mutable对象的操作,必须在各线程里用synchronized全部保护起来。对于该例子即是将B的两条语句锁住。
⑦3和4需要锁住。
⑧使用lock的条件:任何共享的mutable变量/对象在被读/写的时候必须加锁。涉及到多个mutable变量的时候,它们必须被同一个lock保护(例如开始和结束时间)。
死锁
1.多个线程竞争lock,相互等待对方释放lock(必须满足多个线程使用多个锁、访问顺序不同)。
2.避免方法:多个线程使用同一顺序的lock;用单个lock保护多个对象(粗粒度)。
wait(), notify(), notifyAll()
都是object类的方法。需要在synchronized块中调用,要求调用对象和lock的对象相同。
o.wait(): 释放o的锁(阻塞当前线程),进入到o的等待队列中。
o.notify(): 唤醒等待队列的一个线程。
o.notifyAll(): 唤醒等待队列的所有线程。