并发安全
一、类的线程安全定义
Doug Lee:如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。
类的线程安全表现为:
- 操作的原子性
- 内存的可见性
不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。
二、怎么才能做到类的线程安全?
1.栈封闭
所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态。
2.无状态
没有任何成员变量的类,就叫无状态的类
public int service(int a,int b) {
return a*b;
}
3.让类不可变
让状态不可变,两种方式:(JDK中,String,所有基本类型包装类)
1、加final关键字,对于一个类,所有的成员变量应该是私有的,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
2、不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值
第一种
public class ImmutableFinalRef {
private final int a;
private final int b;
private final User user;//这里,就不能保证线程安全啦
public ImmutableFinalRef(int a, int b) {
super();
this.a = a;
this.b = b;
this.user = new User(2);
}
public int getA() {
return a;
}
public int getB() {
return b;
}
public User getUser() {
return user;
}
public static class User{
private final int age;//这里也要加final,才是安全的
public User(int age) {
super();
this.age = age;
}
public int getAge() {
return age;
}
}
public static void main(String[] args) {
ImmutableFinalRef ref = new ImmutableFinalRef(12,23);
User u = ref.getUser();
//u.setAge(35);
}
}
第二种
public class ImmutetableToo {
private List<Integer> list = new ArrayList<>(3);
public ImmutetableToo() {
list.add(1);
list.add(2);
list.add(3);
}
public boolean isContains(int i) {
return list.contains(i);
}
}
4.volatile
保证类的可见性,最适合一个线程写,多个线程读的情景
5.加锁和CAS
请看第三章CAS
6.安全的发布
类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。
public class UnsafePublish {
//要么用线程的容器替换
//要么发布出去的时候,提供副本,深度拷贝
private List<Integer> list = new ArrayList<>(3);
public UnsafePublish() {
list.add(1);
list.add(2);
list.add(3);
}
//讲list不安全的发布出去了
public List<Integer> getList() {
return list;
}
//也是安全的,加了锁--------------------------------
public synchronized int getList(int index) {
return list.get(index);
}
public synchronized void set(int index,int val) {
list.set(index,val);
}
}
7.TheadLocal
8.Servlet
不是线程安全的类:周期:请求的时候创建,返回时销毁
1、在需求上,很少有共享的需求。
2、接收到了请求,返回应答的时候,都是由一个线程来负责的。
9.死锁(关于死锁和活锁我单独写了一篇 死锁和活锁)
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
死锁的根本成因:获取锁的顺序不一致导致。
10.动态的
动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。
解决:
- 通过内在排序,保证加锁的顺序性
- 通过尝试拿锁,也可以。
11.活锁
尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
12.线程饥饿
低优先级的线程,总是拿不到执行时间。
三、性能和思考
使用并发的目标是为了提高性能,引入多线程后,引入额外的开销。衡量应用的程序性能:服务时间,延迟时间,吞吐量,可伸缩性,服务时间,延迟时间(多快),吞吐量(处理能力的指标,完成工作的多少),多快和多少,完全独立,甚至是相互矛盾。、
对服务器应用来说:多少(可伸缩性,吞吐量)这个方面比多快更受重视。
1、先保证程序正确性,确实达不到要求的时候,再提高速度。(黄金原则)
2、一定要以测试为基础。
一个应用程序,串行的部分是永远都有的。
Amdahl定律
四、影响性能的因素
上下文切换
5000~10000个时钟周期,几微秒
内存同步
加锁时增加额外的指令
阻塞
挂起,包括两次额外的上下文切换
五、减少锁的竞争
缩小锁的范围
对锁的持有,快进快出,尽量缩短持有锁的时间(只锁需要锁的代码)
减少锁的粒度
使用锁的时候,锁所保护的对象是多个,多个对象其实是独立变化的时候,不如用多个锁来保护不同的对象。但是要注意死锁
锁分段
ConcurrentHashMap(第五章)
替换独占锁
1、使用读写锁
2、自旋CAS
3、使用系统的并发容器
六、线程安全的单例模式
懒汉式-双重检查
public class SingleDcl {
private int id;
private UserAccount userAccount;// 域可能没有赋值到对象中
private volatile static SingleDcl singleDcl;// volatile可以解决线程安全
private SingleDcl(){
}
public static SingleDcl getInstance(){
if(singleDcl==null) {
synchronized (SingleDcl.class) {//类锁
if(singleDcl==null) {// 防止第一个判断后,其他线程实例化
singleDcl = new SingleDcl();
}
}
}
return singleDcl;
}
}
懒汉式-类初始化模式
类初始化模式,也叫延迟占位模式
public class SingleInit {
private SingleInit(){}
//定义一个私有类,来持有当前类的实例
private static class InstanceHolder{
public static SingleInit instance = new SingleInit();
}
public static SingleInit getInstance(){
return InstanceHolder.instance;
}
}
饿汉式
在jvm中,对类的加载和类的初始化,由虚拟机保证线程安全
public class SingleEHan {
public static SingleEHan singleEHan = new SingleEHan();
private SingleEHan(){}
}