怎么才能做到线程安全
- 无状态的类
没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。因为当多个线程使用同一个对象时,这个对象的成员变量被多个线程共享,而局部变量会在每个线程的本地内存拷贝一份,不被其他线程共享。 - 让类不可变
有两种方式让类不可变:一是给基本类型加入final关键字(如果是引用类型,对象内部数据可变),二是不给成员变量提供修改的方法。 - volatile
仅能保证在一写多读的场景中是线程安全的。 - 加锁和CAS
内部使用synchronization或显示锁(CAS机制)修改数据达到线程安全,但是需要预防死锁。 - 安全的发布
成员变量使用线程安全的类,如CurrentHashMap。 - ThreadLocal
没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。
Servlet
不是线程安全的类,为什么平时感知不到线程不安全:
1、在需求上,很少有共享的需求。
2、接收到了请求,返回应答的时候,一般都是由一个线程来负责的。
但是只要Servlet中有成员变量,一旦有多线程下的写,就很容易产生线程安全问题。
死锁
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
产生死锁的原因
死锁的发生必须具备以下四个必要条件:
- 互斥条件:资源同一时间只能被一个进程占用。
- 持有和请求条件:一个进程在持有至少一个资源的同时请求另外一个资源。
- 不剥夺条件:其他进程如果请求已被占有的资源,只能等待该资源被释放。
- 环路等待条件:即A线程持有线程A1,同时等待B1,B线程持有线程B1,同时等待A1。
其他
避免死锁常见的算法有有序资源分配法、银行家算法。
mysql数据库在同时操作多个表时,可能产生死锁,只能通过外力kill掉其中的死锁线程才能解决这个问题。
解决死锁
死锁的产生可能是偶然情况下触发的,重现难度大,也没有报错日志。
- 定位死锁
JDK提供jps查询到JVM上的应用和对应的id,再通过jstack id查看应用的死锁持有情况。IDEA有一个工具”get Thread dump“可以查看当前应用的死锁。 - 解决方法
a)保证加锁顺序一致。
b)使用活锁。使用ReentrantLock的tryLock()方法尝试拿锁,如果拿锁失败,释放已经获得的锁,并休眠一点时间,减少再次发生资源交叉竞争的概率。
怎么减少锁的竞争
3. 尽量少的使用锁
4. 减少锁粒度
5. 使用类似CurrentHashMap的段锁机制
线程安全的单例模式
下面程序需要给SingleDcl单例对象用volatile修饰,否则可能会发生对象逃逸现象。即线程A给singleDcl对象分配了内存后,还没来得及给singleDcl对象的成员变量初始化,另外一个线程B就获得该单例对象,最终导致发生程序错误。
除了上述的懒汉式单例模式外,还有其他线程安全的单例模式:
1) 饿汉式
2)使用枚举
多线程带来的性能问题
引入多线程的目的是为了发挥cpu多核心的优势,使得程序性能更好。但是,如果使用不当,反而会降低系统性能。因为引入多线程会涉及到上下文的切换,内存同步、阻塞问题。
1) 上下文切换:线程切换涉及到上下文的切换,需要消耗50-10000个时钟周期,可能是操作系统消耗最大的一个动作了。
2) 内存同步:多线程环境往往会涉及到volatile操作,volatile会给程序加入内存屏障,抑制编译器的优化。还可能会导致其他线程的缓存失效。这都增加了时间开销。
3) 阻塞:多线程对锁的竞争会导致竞争失败的线程挂起。