当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

1. 线程封闭

就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。
ad-hoc 线程封闭:这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。Ad-hoc 线程封闭非常脆弱,应该尽量避免使用。
栈封闭:栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

2. 无状态的类

没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。

3. 让类不可变

  1. 加final 关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final 关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的
  2. 根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。
    并发编程 - 线程的并发安全

4. volatile

并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。

5. 加锁和CAS

我们最常使用的保证线程安全的手段,使用synchronized 关键字,使用显式锁,使用各种原子变量,修改数据时使用CAS 机制等等。

6. 安全的发布

类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本

7. TheadLocal

ThreadLocal 是实现线程封闭的最好方法。ThreadLocal 内部维护了一个Map,Map 的key 是每个线程的名称,而Map 的值就是我们要封闭的对象。每个线程中的对象都对应着Map 中一个值,也就是ThreadLocal 利用Map 实现了对象的线程封闭。

8. Servlet 辨析

不是线程安全的类,为什么我们平时没感觉到:
1、在需求上,很少有共享的需求,
2、接收到了请求,返回应答的时候,一般都是由一个线程来负责的。
但是只要Servlet 中有成员变量,一旦有多线程下的写,就很容易产生线程安全问题。

9. 死锁

是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

现象
顾名思义也是和获取锁的顺序有关,但是比较隐蔽,不像简单顺序死锁,往往从代码一眼就看出获取锁的顺序不对。

危害
1、线程不工作了,但是整个程序还是活着的
2、没有任何的异常信息可以供我们检查。
3、一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对生产平台的程序来说,这是个很严重的问题。

时间不定,不是每次必现;一旦出现没有任何异常信息,只知道这个应用的所有业务越来越慢,最后停止服务,无法确定是哪个具体业务导致的问题;测试部门也无法复现,并发量不够。

解决
定位: 通过jps 查询应用的id,再通过jstack id 查看应用的锁的持有情况
并发编程 - 线程的并发安全
修正
关键是保证拿锁的顺序一致
两种解决方式
1、内部通过顺序比较,确定拿锁的顺序;
2、采用尝试拿锁的机制。

10. 活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间。

11. 线程饥饿

低优先级的线程,总是拿不到执行时间

12. 如何减少锁的竞争

减少锁的粒度
使用锁的时候,锁所保护的对象是多个,当这些多个对象其实是独立变化的时候,不如用多个锁来一一保护这些对象。但是如果有同时要持有多个锁的业务方法,要注意避免发生死锁

缩小锁的范围
对锁的持有实现快进快出,尽量缩短持由锁的的时间。将一些与锁无关的代码移出锁的范围,特别是一些耗时,可能阻塞的操作

避免多余的锁
两次加锁之间的语句非常简单,导致加锁的时间比执行这些语句还长,这个时候应该进行锁粗化—扩大锁的范围。

锁分段
ConcurrrentHashMap 就是典型的锁分段。

替换独占锁
1、使用读写锁,
2、用自旋CAS
3、使用系统的并发容器

懒汉式
类初始化模式,也叫延迟占位模式。在单例类的内部由一个私有静态内部类来持有这个单例类的实例。

延迟占位模式还可以用在多线程下实例域的延迟赋值。

饿汉式
在声明的时候就new 这个类的实例,因为在JVM 中,对类的加载和类初始化,由虚拟机保证线程安全。

或者使用枚举

并发编程 - 线程的并发安全

相关文章: