Java并发
2016-11-15 07:48:31 0 举报
AI智能生成
Java并发是Java编程语言的一个特性,它允许多个线程同时执行。这种并行处理能力可以大大提高程序的性能和响应速度。Java并发主要通过线程来实现,每个线程都有自己的程序计数器、栈和局部变量等资源。Java提供了丰富的并发API,如Thread类、Runnable接口、synchronized关键字、Lock接口等,用于创建和管理线程,实现线程间的通信和同步。然而,并发编程也带来了一些挑战,如数据竞态、死锁等问题,需要程序员仔细设计和调试。总的来说,Java并发是一个强大而复杂的工具,正确使用可以提高程序的效率和质量。
作者其他创作
大纲/内容
多线程风险
安全性问题
执行顺序不可预测
原子性
子主题
共享变量的可见性问题
可见性
活跃性问题
单线程中无限循环
死锁
饥饿
活锁
长时间持有锁
阻塞IO
解决:长时间操作不持有锁
性能问题
频繁的上下文切换
同步机制抑制某些编译器的优化
基础概念
原子性
要么都做,要么都不做。是最小单位的操作
确保程序执行的时序不会错乱
竞态条件
定义:由于不恰当的执行时序而出现不正确的结果
常见类型
先检查后执行
典型例子
单例模式延迟初始化
目标:只初始化一个唯一实例
结果:初始化多个实例,两个线程分别获得不同的两个实例
原因:状态前后不一致
检查完毕后,检查的状态因为被另外一个线程修改而导致失效
解决:原子化,串行化
将一组复合操作原子化,使得状态得以保持一致。因为需要更新的状态会一起被更新
加锁机制
内置锁
每个Java对象都可以用做一个实现同步的锁
重入
对象状态
对象状态是指存储在状态变量(例如实例域或静态域)中的数据
活跃性与性能
缩小同步代码块作用的范围
共享可变对象
只有共享可变对象才需要考虑线程安全
修复共享可变对象的线程安全问题有三种方法
不在线程间共享可变的状态变量
线程封闭
将状态变量修改为不可变
在访问状态变量时使用同步
同步
共享对象
线程封闭
解决多线程共享对象的问题,就是不共享。哈哈
定义
不共享对象,仅在单线程内访问数据。(也就类似于串行访问)
例子
就跟Android只在UI线程更新界面一样,这样就不用考虑线程安全问题了。所有的UI都要在主线程更新
注意
需要程序员自己实现保证,因为Java语言并没有机制来强制对象只能在某个线程中使用
java语言提供的维持封闭的机制
局部变量(也叫栈封闭)
ThreadLocal类
多线程问题
可见性
原因:与Java内存模型有关
解决:锁,volatile
刷新工作内存
逸出
不应该发布的对象被发布了
例子:构造过程中this逸出
在构造函数中创建匿名内部类
术语解释
发布
对象能在当前作用域之外的代码中使用
逸出
不应该发布的对象被发布了
例子:构造过程中this逸出
在构造函数中创建匿名内部类
不可变对象
定义(条件)
对象创建以后其状态就不能修改
对象的所有域都是final类型的
对象是正确创建的(也就是对象在创建期间,this引用没有逸出)
发布
使用Volatile发布不可变对象,确保可见性
子主题
final域
修改和读取
不需要同步
可变对象
发布
安全发布
Final域
锁
修改和读取
修改和访问
可变
需要同步
不可变
不需要同步
事实不可变
不需要同步
设计线程安全的类
步骤
找出构成对象状态的所有变量
就是那些可变的域(成员变量,成员变量的域也属于状态的一部分)
找出约束状态变量的不变性条件
也就是状态有效性的条件,比如不能小于0,或者a要比b大
需要明确不变性,后验性条件,先验性条件
建立对象状态的并发访问管理策略
通过该类的同步方法来访问
方法
实例封装(Java监视器模式)
将数据封装在对象内部,可以将数据的方法限制在对象的方法上
委托给线程安全的类
对象的各个状态之间不能有联系
对象没有不变性条件
在现有的安全类上添加功能
直接修改源码,如果可以的话
客户端加锁
必须与原有的安全类使用同一个锁
组合
将需要扩展的安全类作为成员
使用自身的内置锁来封装所有对安全类的操作和自身增加的复合操作,并且与该安全类实现同样的接口
文档
说明该类的安全性保证
说明同步策略
Java提供的基础并发模块
同步容器类
典型
Vector
Hashtable
缺点
复合操作存在竞态条件
需要额外加锁
要注意隐藏的迭代
将所有对容器状态的访问都串行化。严重降低了并发性。当多个线程竞争容器的锁时,吞吐量将严重降低
并发容器类
优点
提高并发性
ConcurrentHashMap
使用更细粒度的锁--分段锁
有额外的原子操作
若没有则添加
CopyOnWriteArrayList
写入时会拷贝副本
阻塞队列
所有权转移,只需要安全发布即可
阻塞和中断
同步工具类
闭锁
CountDownLatch
FutureTask
信号量(Semaphore)
栅栏
CyclicBarrier
Exchanger
线程池使用
Executor框架
配置线程池大小
配置ThreadPoolExecutor
优点
解决无限制创建线程的缺点
不使用线程池,无限制创建线程的缺点
线程生命周期的开销非常高
资源消耗
如果可运行线程数量多于可用处理器的数量,那么有些线程将闲置。大量空闲线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他的性能的消耗。如果你已经拥有足够多的线程使得所有CPU保持忙碌状态,那么再创建更多的线程反而会降低性能
稳定性
可创建线程的数量存在一个上限。这个上限会跟平台的不同而不同,并且受多个因素制约,包括JVM启动的参数、Thread构造函数中请求的栈的大小,以及底层操作系统对线程的限制等。如果超出了这个上限,那么就很可能抛出OutOfMemoryError异常
要点
如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁
只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖关系,那么有界的线程池或队列就可能导致“饥饿”死锁的问题。此时应该使用无界的线程池,例如newCachedThreadPool(可以使用有界线程池+SynchronousQueue工作队列+调用者运行饱和策略)
运行较长时间的任务
出现的问题
使得线程池堵塞
影响较短任务的服务时间
解决
限定资源等待的时间,而不要无限制等待。当等待超时将任务中止,或者重新放回队列
管理队列任务
无界队列
如果任务持续快速到达,超过了线程处理速度,那么无界队列就会无限制增加。这将会导致资源耗尽的情况发生
有界队列
相比无界队列,有界队列更为稳妥。可以避免资源耗尽的情况发生
填满后,饱和策略将会生效
同步移交队列(SynchronousQueue)
避免排队等待,直接递交任务。并不是一个真正的队列
如果递交失败,也就是线程池无法处理递交的任务。就会触发饱和策略
饱和策略
终止(AbortPolicy)
默认的饱和策略
抛出未检查的RejectedExecutionException
调用者运行(CallerRunsPolicy)
在提交任务的线程执行
如果在主线程提交任务,那么这样就会让主线程在执行该提交任务期间不会在提交新的任务
DiscardPolicy
抛弃无法保存到已满的队列的新任务
DiscardOldestPolicy
将抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果队列是优先级队列,那么抛弃最旧策略将导致抛弃优先级最高的任务,因此最好不要将抛弃最旧的饱和策略与优先级队列一起使用。
取消和关闭
线程中断
中断处理
响应中断
调用线程处理中断
传递中断异常
传递给上层调用,让他们来处理
恢复被中断的状态
传递不了的,可以恢复该中断,然后让调用该方法的线程,处理中断标志位
关闭线程池
JVM关闭
处理不可中断的阻塞
同步的socket I/O
对套接字的读取和写入
关闭底层套接字,使得执行read或write等方法抛出SocketException
异步I/O
nio
阻塞队列
注意:要将生产者和消费者都中断
方法
isShutDown && taskCount==0
毒丸对象
无界队列
放入一个终结对象,表示结束
处理非正常的线程终止
原因
未捕获异常导致线程终止
解决
工作者线程结构
使用UncaughtExceptionHandler统一处理所有线程抛出的未捕获异常
活跃性、性能与测试
避免活跃性危险
死锁
死锁种类
锁顺序死锁
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现顺序死锁问题
动态的锁顺序死锁
通过参数传递两个对象,并将两个对象作为锁对象。同时获取两个锁,这时候就需要增加响应的逻辑给锁排序,使之以相同的顺序获取锁。避免死锁。可以使用唯一的值来比较,比如帐号。或者使用“加时赛”锁,也就是多加一把锁在两个锁之前。
协作对象之间发生死锁
两个对象互为调用对方的方法,而这些方法都做了同步,那就会导致获取锁的顺序不一致
解决
开放调用
也就是说在互相调用其他对象的方法时,不使用同步
资源死锁
单线程的Executor中的任务依赖
死锁的避免
使用支持定时的锁
只有在同时获取两个锁时才有效。因为在嵌套的方法中请求多个锁,那么即使你已经持有了外层的锁,也无法释放它
通过线程转储信息来分析死锁
通过JVM生成
饥饿
当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿
CPU时钟周期(线程优先级)
糟糕的响应性
活锁
不阻塞,但是线程不断的重复执行。一般都是由过度的恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。
在重试机制中加入随机性
性能与可伸缩性
可伸缩性
当增加计算机资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能响应增加
也就是将任务划分成多个不同的部分,并分别在多个线程执行。充分利用计算机资源。相比单线程执行的任务,性能必然会有损失(传递任务的网络延迟,任务排队、线程协调以及数据复制等存在的开销)
多线程的开销
上下文切换
内存同步
阻塞
如何提高可伸缩性
减少锁竞争
减少锁持有时间
降低锁的请求频率,使用更多的锁,减少锁的粒度
锁分解
分段锁
子主题
性能
并发程序测试
基准测试,编写测试程序
代码审查
静态分析工具
FindBug
Lint
分析与监测工具
java profiler
显示锁
Lock
ReenterantLock
优点
可实现非阻塞
可中断
synchronized与ReentrantLock之间选择
选择ReentrantLock
轮询锁
定时锁
可中断
选择synchronized
在代码块中释放,简化编码工作(就是使用简单)
读写锁
Java内存模型
0 条评论
下一页