Java并发笔记
2018-08-15 本文已影响0人
shaYanL
1. 多线程相对于单线程的优势:
- 提高资源利用率【减少CPU空闲时间】
- 在特定情况下【线程间很少共享数据】,编码简洁、容易
- 程序响应更快
2. 多线程的代价
- 可能会转让代码更复杂,调试更困难
- 线程的切换伴随着上下文切换,增加额外耗时
- 增加资源消耗
3. 几种并发编程模型
- 并行工作者模型:容易理解,但涉及共享状态【内存、数据库】会显得复杂;且工作者无法在内部保存共享状态,每次使用需重新读取,这种状况也称为无状态;任务执行的先后顺序也是不确定的
- 流水线并发模型:无共享并行模型/反应器系统/事件驱动系统。通过事件传递作业,形成各种执行链条,就像流水线一样;大多优劣处恰与并行工作者模型相反
4. 创建及运行Java线程:
- 创建Thread类的子类,并重写run()方法;实例化Thread类的子类并调用start()方法
- 创建一个Thread类的匿名子类的实例,并调用start()方法
- 实现了java.lang.Runnable接口的类,并实现run()方法;在Thread类的构造函数中传入实现了Runnable接口的类的实例,并调用start()方法
- 在Thread类的构造函数中传入实现了Runnable接口的匿名类的实例,并调用start()方法
5. 竞态条件 & 临界区
- 如果两个线程竞争同一个资源【内存区,系统,文件】,此时如果对资源的访问顺序敏感【不同的顺序产生不同的结果,主要是写操作】,就称存在竟态条件
- 导致竟态条件发生的代码的区域称为临界区
- 避免竟态条件:对临界区代码使用同步机制
6. 线程安全与共享资源
- 允许被多个线程同时执行的代码称作线程安全的代码;局部变量存在在Java虚拟机栈中是私有的,线程安全,但局部变量中的引用类型指向的对象在Java堆中,如果被其他线程访问到则是线程不安全的
- 线程控制逃逸规则:如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
- "即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了"
7. 线程安全及不可变性
- 引用不是线程安全的, 一个不可变的类的引用不是线程安全的
- 实现一个不可变共享资源value,一旦ImmutableValue定义,value不可再改变
public class ImmutableValue{
private int value = 0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
- 不变与只读:“可将不变比作生日,把只读比如年龄;生日不会变化,年龄却会改变”
8. Java同步关键字synchronzied
- 可用于实例方法及实例方法内
- 可用于类方法及类方法内
- 直接用于方法,则在方法定义前加 synchronzied
public synchronized void add(long value) {
count += value;
System.out.println(Thread.currentThread().getName() + ":====" +count);
}
- 作用域方法内部,则使用同步块
public void add2(long value) {
synchronized (this) {
count += value;
System.out.println(Thread.currentThread().getName() + ":====" + count);
}
}
9. 线程通信
- 在共享对象的变量里设置信号值
- 忙等待(Busy Wait) 持续访问特定信号值,直至信号值改变
- wait(),notify()和notifyAll() 实现等待-唤醒机制:一旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块才可以,因为:被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用
- 丢失的信号(Missed Signals)没有等待的线程,但调用了notify;可增加通知信号避免丢失信号
- 假唤醒 线程有可能在没有调用过notify()和notifyAll()的情况下醒来
- 在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象作为管程
10. TheadLocal-提供线程内的局部变量
- 各线程私有,初始默认值为null
- 继承TheadLocal的子类复写initialValue()方法可构造统一初始值
- ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,gc时容易内存泄露:在调用get,set时将key为null的这些Entry都删除;手动调用ThreadLocal的remove函数;将ThreadLocal对象定义为static的,保持强引用
11. 死锁
- 多个线程同时但以不同的顺序请求同一组锁的时候,形成死锁
12. 避免死锁
- 加锁顺序:所有线程都按照一定的顺序加锁
- 限制加锁时间:线程在规定时间内未获得需要的锁,则放弃尝试获取锁且放弃已获得的锁,之后再重试--这种超时和重试机制,在线程过多的情况下,可能出现连续死锁的情况
- 死锁检测: 通过记录线程和线程持有的锁及请求的锁,检测是否有死锁的情况发生,若发生则可利用释放所有锁,再重试,或者释放特定的锁,再优先级高的线程先运行
13. 饥饿和公平
- 饥饿:一个线程得不到CPU时间来执行的状态
- 公平:解决饥饿的方案
- 提高等待线程的公平性:使用锁方式代替同步块
- 公平锁实现:加锁方法只是将线程放入队列,解锁后,只允许队列的第一个线程获得公平锁实例;各个线程只会唤醒各自的wait()方法,因为每个线程都有自己的排队对象,等待唤醒都在各自对象上进行,互无影响,只是靠公平锁来统一调用
14. 嵌套管程锁死
- 与死锁很像:都是线程最后被一直阻塞着互相等待
- 死锁中,二个线程都在等待对方释放锁;嵌套管程锁死中,线程1持有锁A,同时等待从线程2发来的信号,线程2需要锁A来发信号给线程1。
15. Java中的锁
- 可重入:线程可以进入任何一个它已经拥有的锁所同步着的代码块
- finally语句中调用unlock():保证当临界区抛出异常时Lock对象可以被解锁
16. Java中的读/写锁
- 读-读能共存,读-写不能共存,写-写不能共存
- 读取: 没有线程正在做写操作,且没有线程在请求写操作
- 写入: 没有线程正在做读写操作
- 读锁重入:要么满足获取读锁的条件(没有写或写请求),要么已经持有读锁(不管是否有写请求);重入后的读锁比写锁优先级高
- 写锁重入:已经持有写锁,才允许写锁重入(再次获得写锁)
17. 重入锁死
- 如果一个线程持有某个管程对象上的锁,那么它就有权访问所有在该管程对象上同步的块。这就叫可重入
- 当一个线程重新获取不可重入的同步器时,就可能发生重入锁死
- 避免重入锁死:编写代码时避免再次获取已经持有的锁;使用可重入锁
18. 信号量
- 用于在线程间传递信号,以避免出现信号丢失
- 信号量的数量上限是1时,Semaphore可以被当做锁来使用
19. 阻塞队列
- 阻塞队列:从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞
- 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来
- 实现类似于带上限的Semaphore的实现
- 可解决生产者、消费者速率不一致的问题
20. 线程池
- 限制应用程序中同一时刻运行的线程数
- 实现,可用阻塞队列维持需要执行的任务,让后让线程池的线程一直从阻塞队列中取任务、执行任务,直至线程停止
21. Java并发编程之CAS
- 一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值
22. 同步器
- 状态:是用来确定某个线程是否有访问权限
- 访问条件:决定改变状态的方法的线程是否可以对状态进行设置;通常是放在一个while循环里,以避免虚假唤醒
- 状态变化:一个线程获得了临界区的访问权限,它得改变同步器的状态,让其它线程阻塞
- 通知策略: 通知其它等待的线程状态已经变,3种策略(all, one, the one)
- Test-and-Set方法:检查访问条件,如若满足,该线程设置同步器的内部状态来表示它已经获得了访问权限
- set方法:仅是设置同步器的内部状态,而不先做检查
23. 非阻塞算法
- 阻塞算法会阻塞线程直到请求操作可以被执行。非阻塞算法会通知请求线程操作不能够被执行,并返回。
- volatile变量:非阻塞的;适用于单线程写的情况:只有一个【不一定是固定一个】线程在执行一个 raed-update-write 的顺序操作,其他线程都在执行读操作,将不会发生竞态条件
- 传统的锁:会使用同步块或其他类型的锁阻塞对临界区域的访问,可能会导致线程挂起
- 乐观锁:允许所有的线程在不发生阻塞的情况下创建一份共享内存的拷贝,通过 CAS操作就可以把变化写入共享内存,线程获得它们想修改的数据的拷贝并做出修改;用于共享内存竞用不是非常高的情况,也用于拷贝时间短的情况
- 共享预期的修改:可以共享预期的修改,但变相添加了锁,阻止其他线程提交预期修改
- A-B-A问题:一个变量被从A修改到了B,然后又被修改回A的一种情景。其他线程对于这种情况却一无所知
- A-B-A问题的解决方案:不再仅仅替换指向一个预期修改对象的指针,而是指针结合一个计数器,然后使用一个单个的CAS操作来替换指针 + 计数器
24. 阿姆达尔定律
T(N) = N个线程(并行因子为N)下执行的总时间
B = 不可以并行的总时间
T- B = 并行部分的总时间
- T(N) = B + (T(1) – B) / N
- T(O, N) = B / O + (1 - B / O) / N;不可并行部分通过一个因子O来优化
- 增速比: Speedup = T / T(O , N);
- 用于量化程序优化的效果