线程安全性
线程安全性是指在多线程程序设计中,当多个线程同时访问和操作共享数据时,程序仍能保证数据的准确性、一致性和完整性,不会因为线程交错执行而导致数据的混乱和其他意外。
线程封闭
线程封闭(Thread Confinement)是一种简单的并发设计技术,它通过确保对象只能由一个线程访问来避免并发问题,从而不需要额外的同步机制。常有的线程封闭有两种:局部变量的栈封闭和和使用ThreadLocal变量。
栈封闭
当一个对象被声明为局部变量的时候,它就自然被封闭在调用该方法的线程中,因为局部变量储存在线程的栈上,其他线程无法访问。
public int sumArray(int[] numbers) {
int sum = 0; // 这个sum变量是栈封闭的,因为它是在方法内部声明的局部变量
for (int number : numbers) {
sum += number;
}
return sum;
}
在这个例子中,sum变量只在sumArray方法的执行上下文中存在,且只被该方法内的代码访问。由于局部变量的特性,不同的线程调用此方法时,每个线程都会有自己的sum变量副本,因此不存在并发访问问题。
ThreadLocal
ThreadLocal提供了一种更灵活的方式来实现线程封闭,它可以为每个线程提供一个独立的变量副本,即使在静态字段或实例字段中也能实现线程间的隔离。
问题1:数据竞争
因没有适当的同步机制控制对共享机制的访问。当两个或更多线程访问同一块数据,并且至少有一个线程是写操作的,可能会导致不可预知的结果。
解决方法:使用synchronized关键字或者java.util.concurrent.locks包下的锁。
问题2:死锁
发生在两个或更多的线程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力干涉,这些线程将无法推进下去。
死锁产生的四个必要条件:
- 互斥条件:即在某一段时间内,某资源只能由一个线程占用。如果此时还有其他线程请求资源,则请求者只能等到知道占有者释放资源。
- 请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占用,则此时请求阻塞,但有对自己获得的资源保持不放。
- 不剥夺条件:指线程已获得的资源,在未使用完之前不能被剥夺,只能在使用完时由自己释放。 4.环路等待条件:存在一种环路等待链,链中的每一个线程都在等待下一个线程所占有的资源。
解决方法:
- 避免循环等待:按顺序申请资源,避免形成环路等待条件。
- 使用超时:尝试获取锁时加上超时限制,超时后释放已持有的锁并重试。
- 死锁检测与恢复:运行时系统定期检查死锁条件,发现死锁后采取措施恢复,如中断某些线程。
- 锁顺序:总是以相同的顺序获取锁,可以避免循环等待。
问题3:活锁与饥饿
活锁指线程因不断重试而无法继续执行;饥饿指某个线程无法获得所需资源而长时间无法执行。因不当的线程调度策略或资源分配导致问题产生。
解决方法:使用公平锁策略,合理设计线程优先级,设置超时机制。
问题4:上下文切换开销
因线程过多或线程执行时间过短。导致操作系统在不同线程间切换执行环境的过程中消耗CPU资源。
解决办法:
减少线程数量,使用线程池,优化任务分配。
问题5:线程泄露
因忘记取消任务、关闭线程池等。导致不再使用的线程未能正确终止,占用系统资源。
解决办法:使用try-with-resources或finally块确保资源释放。
问题6:内存泄露
因静态变量持有线程、线程局部变量未清理等。导致线程持有对不再使用的对象的引用,阻止垃圾回收。
解决办法:确保线程内使用的对象生命周期管理得当。避免在静态上下文中直接创建或引用线程实例。
问题7:竞态条件
因缺少同步控制。导致多个线程访问共享资源时,执行结果依赖于线程调度的时机。
解决办法:使用原子操作、锁或其他同步机制。
问题8:死循环
因逻辑错误或条件判断失误。导致线程进入无法自行退出的循环。
解决办法:审查代码逻辑,确保有退出循环的条件。
问题9:过度同步
因对不需要同步的代码也进行了同步处理。可能导致性能瓶颈。
解决办法:仅对必要的代码块进行同步,使用更细粒度的锁。