文章目录
一、如何保证线程安全
1.1、并发原子类
java.util.concurrent.atomic包中的类
1.2、加锁机制
内置锁synchronized虽然可以防止并发但是导致的结果是性能会比之前下降很多。因为在加锁的类,对象,代码块,在同一个时间只能由一个线程持有,其他都得阻塞等待。
一种解决办法就是拆分代码块,只针对需要共享变量的地方进行同步锁。
重入性意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方法,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时候,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时候,JVM记下锁的持有者,并将获取值设为1.如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块,计数器会相应的减少。当计数值为0,锁被释放。
1.3、信号量同步共享数据
信号量是一个线程同步结构,可以用来在线程之间发送信号以避免丢失同步信息,也可以像使用锁一样保护关键代码
CountDownLatch,Semaphore。CyclicBarrier是基于同步到达某个点的信号量触发机制。
无论性能还是安全性,尽可能使用并发包中的信号同步类,避免使用对象wait()和notify()方式来进行同步。
简单的实现:
public class Semaphore {
private boolean signal = false;
public synchronized void take() {
this.signal = true;
this.notify();
}
public synchronized void release() throws InterruptedException{
while(!this.signal) wait();
this.signal = false;
}
}
上面提到了3种解决办法:
原子操作数据类型和同步锁。那么混用会如何呢?一般不建议混用,混用两种锁机制,结果不会给性能或者安全性带来任何提升。
对于每一个共享和可变的变量都应该只有一个锁来保护,从而使维护人员知道是哪一个锁。
避免使用锁的场景执行时间较长的计算或者可能无法快速完成的操作时候(例如网络I/O或控制台I/O)一定不要持有锁
1.4、封闭线程,不共享数据
1、栈封闭—》基本数据类型(由栈管理),线程内部使用或者线程局部使用,只能通过局部变量才能访问对象。他们位于执行线程的栈中,其他线程无法访问这个栈。
2、Ad-hoc线程封闭(链接–点击这里)
维护线程封闭性的职责完全由程序实现类承担。尽量用栈封闭或者ThreadLocal类来替代它。
3、ThreadLocal类提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时候设置的最新值。
4、可变对象变为不可变对象。
5、用final关键字
6、维护容器对象包含多个状态变量的不变性,并使用volatile维护可见性
7、静态初始化
1.5、发布与逸出
发布:实例化一个对象。其他地方对当前的对象引用
逸出:当已经实例化的对象,被重新引用覆盖,这种情况就是逸出。—解决这个问题就是封装
eg:特殊案例:
安全的对象构造----this引用在构造函数中逸出。
常见的错误就是构造函数中启动一个线程,当对象在器构造函数中创建一个线程,无论显式还是隐式,this引用都会被新创建的线程共享。在对象尚未构造完,新的线程就可以看见。
解决办法:私有构造函数和一个公共的工厂方法。
在静态初始化函数中初始化一个对象引用。(public static Holder holder =new Holder(42))----静态初始化器由JVM在类的初始化阶段执行,由于在JVM内部存在同步机制
将对象的引用保存到volatile类型的域或者AtomicReferance对象
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中
二、设计线程安全的类
2.1、前提要素
找出构成对象状态的所有变量。(变量)
找出约束状态变量的不变性条件。(共享变量可能会并发)
建立对象状态的并发访问管理策略。(如何确保并发安全)
原子性和封装性是帮助满足状态变量的有效值或状态转换上的约束条件。
通过现有库中的类阻塞队列Blocking Queue或信号量Semaphore来实现依赖状态的行为。
2.2、无限制的创建线程有哪些问题
1、线程生命周期的开销
2、资源消耗
3、稳定性
线程池的使用如何确定线程池的最合适大小: