并发安全

目录

1.类的线程安全定义

2.怎么才能做到类的线程安全?

2.1 栈封闭

2.2 无状态

2.3 让类不可变

2.4 Volatile

2.5 加锁和CAS

2.6 安全的发布

2.7 TheadLocal

2.8 关于Servlet(生命周期;是否安全)

3.线程不安全引发的问题

3.1 死锁

3.1.1 普通的死锁

3.1.2 动态顺序死锁

3.2 活锁

3.3 线程饥饿

4.性能和思考

4.1 衡量指标

4.2 影响性能的因素

4.2.1 上下文切换(合理分配线程数)

4.2.2 内存同步(减少锁的竞争)

4.2.3 阻塞(减少锁的竞争)

4.3 提高性能的方法

4.3.1 减少锁的竞争

4.3.2 减少锁的粒度

4.3.3 缩小锁的范围

4.3.4 避免多余的缩减锁的范围

4.3.5 锁分段

4.3.6 替换独占锁

5.线程安全的单例模式

5.1 双重检查锁定的问题

5.2 懒汉式

5.3 饿汉式

参考


1.类的线程安全定义

如果多线程下使用这个类,不管多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。

类的线程安全表现为:

  • 操作的原子性
  • 内存的可见性

不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。

2.怎么才能做到类的线程安全?

2.1 栈封闭

所有的变量都是在方法内部声明和使用,这些变量都处于栈封闭状态。

2.2 无状态

没有任何成员变量的类,就叫无状态的类

public class StatelessClass {
    
    public int service(int a,int b) {
        return a*b;
    }
    
    //...public void t(){}

}

2.3 让类不可变

Java中有:

  • String
  • 包装类
  • LoacalDateTime

让状态不可变,两种方式:

  • 1)加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
public class ImmutableFinal {   
    private final int a;
    private final int b;
    
    public ImmutableFinal(int a, int b) {
        super();
        this.a = a;
        this.b = b;
    }

    public int getA() {
        return a;
    }

    public int getB() {
        return b;
    }
}

如下就不能保证安全了!

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 int age;

        public User(int age) {
            super();
            this.age = age;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }       
    }
    
    public static void main(String[] args) {
        ImmutableFinalRef ref = new ImmutableFinalRef(12,23);
        User u = ref.getUser();
        //u.setAge(35);
    }
}

  • 2)根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值
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);
    }
}

另外参考:AKKA

2.4 Volatile

保证类的可见性,最适合一个线程写,多个线程读的情景。
如果有多个线程写,可以使用锁(如synchronized)等效于一个线程写,ConcurrentHashMap就是这么做的。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {

2.5 加锁和CAS

锁等问题请参考《现代操作系统》——第2章 2.3 进程间通信

2.6 安全的发布

类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。

方法:

  • 用线程安全的容器替换
  • 返回副本,深拷贝
  • 要么加锁

2.7 TheadLocal

2.8 关于Servlet(生命周期;是否安全)

不是线程安全的类。
为什么我们平时没感觉到:

  • 1)在需求上(http请求),很少有共享的需求
  • 2)接收到了请求,返回应答的时候,都是由一个线程来负责的

3.线程不安全引发的问题

死锁等问题请参考《现代操作系统》——第6章 死锁

3.1 死锁

资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。
死锁的根本成因:获取锁的顺序不一致导致。

3.1.1 普通的死锁

参考死锁代码NormalDeadLock.java

    private static void fisrtToSecond() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueFirst) {
            System.out.println(threadName+" get first");
            SleepTools.ms(100);
            synchronized (valueSecond) {
                System.out.println(threadName+" get second");
            }
        }
    }

    //先拿第二个锁,再拿第一个锁
    private static void SecondToFisrt() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueSecond) {
            System.out.println(threadName+" get first");
            SleepTools.ms(100);
            synchronized (valueFirst) {
                System.out.println(threadName+" get second");
            }
        }
    }

检查死锁方法:

  • 通过jps 查询进程 id
  • 再通过jstack id 查看应用的锁的持有情况

解决办法:保证加锁的顺序性

3.1.2 动态顺序死锁

参考银行账号转账产生的活锁以及解决方法

动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。
活锁产生TransferAccount.java

    public static void main(String[] args) {
        PayCompany payCompany = new PayCompany();
        UserAccount zhangsan = new UserAccount("zhangsan",20000);
        UserAccount lisi = new UserAccount("lisi",20000);
        // ITransfer transfer = new TransferAccount();
        // ITransfer transfer = new SafeOperate();
        ITransfer transfer = new SafeOperateToo();
        TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi"
                ,zhangsan,lisi,2000,transfer);
        TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan"
                ,lisi,zhangsan,4000,transfer);
        zhangsanToLisi.start();
        lisiToZhangsan.start();
    }
public class TransferAccount implements ITransfer {

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
        synchronized (from){//先锁转出
            System.out.println(Thread.currentThread().getName()
                    +" get"+from.getName());
            Thread.sleep(100);
            synchronized (to){//再锁转入
                System.out.println(Thread.currentThread().getName()
                        +" get"+to.getName());
                from.flyMoney(amount);
                to.addMoney(amount);
            }
        }
    }
}

解决方法:

  • 1)通过内在排序,保证加锁的顺序性(identityHashCode原生的hashhode,按照这个的顺序制定加锁顺序;如果哈希值相等,使用tieLock);
    可以使用任何唯一化的ID。
    统一加锁顺序SafeOperate.java
public class SafeOperate implements ITransfer {
    private static Object tieLock = new Object();//加时赛锁

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {

        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //先锁hash小的那个
        if(fromHash<toHash) {
            synchronized (from){
                System.out.println(Thread.currentThread().getName()
                        +" get"+from.getName());
                Thread.sleep(100);
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()
                            +" get"+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }
        }else if(toHash<fromHash) {
            synchronized (to){
                System.out.println(Thread.currentThread().getName()
                        +" get"+to.getName());
                Thread.sleep(100);
                synchronized (from){
                    System.out.println(Thread.currentThread().getName()
                            +" get"+from.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }
        }else {//解决hash冲突的方法
            synchronized (tieLock) {
                synchronized (from) {
                    synchronized (to) {
                        from.flyMoney(amount);
                        to.addMoney(amount);
                    }
                }
            }
        }

    }
}
public class SafeOperateToo implements ITransfer {

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
        Random r = new Random();
        while(true) {
            if(from.getLock().tryLock()) {
                try {
                    System.out.println(Thread.currentThread().getName()
                            +" get "+from.getName());
                    if(to.getLock().tryLock()) {
                        try {
                            System.out.println(Thread.currentThread().getName()
                                    +" get "+to.getName());
                            //两把锁都拿到了
                            from.flyMoney(amount);
                            to.addMoney(amount);
                            break;
                        }finally {
                            to.getLock().unlock();
                        }
                    }
                }finally {
                    from.getLock().unlock();
                }
            }
            SleepTools.ms(r.nextInt(10));
        }
    }
}

3.2 活锁

尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
没有那个随机休眠,就会产生不同程度的活锁!

public class SafeOperateToo implements ITransfer {

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
        Random r = new Random();
        while(true) {
            if(from.getLock().tryLock()) {
                try {
                    System.out.println(Thread.currentThread().getName()
                            +" get "+from.getName());
                    if(to.getLock().tryLock()) {
                        try {
                            System.out.println(Thread.currentThread().getName()
                                    +" get "+to.getName());
                            //两把锁都拿到了
                            from.flyMoney(amount);
                            to.addMoney(amount);
                            break;
                        }finally {
                            to.getLock().unlock();
                        }
                    }
                }finally {
                    from.getLock().unlock();
                }
            }
            //SleepTools.ms(r.nextInt(10));
        }
    }
}

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

3.3 线程饥饿

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

4.性能和思考

4.1 衡量指标

线程调度请参考《现代操作系统》——第2章 2.4节 调度
使用并发的目标是为了提高性能,引入多线程后,其实会引入额外的开销,如线程之间的协调、增加的上下文切换,线程的创建和销毁,线程的调度等等。过度的使用和不恰当的使用,会导致多线程程序甚至比单线程还要低。
衡量应用的程序的性能:服务时间,延迟时间,吞吐量,可伸缩性等等,其中服务时间,延迟时间(多快),吞吐量(处理能力的指标,完成工作的多少)。多快和多少,相对独立,甚至是相互矛盾的。

对服务器应用来说:多少(可伸缩性,吞吐量)这个方面比多快更受重视。

我们做应用的时候:
1、 先保证程序正确,确实达不到要求的时候,再提高速度。(黄金原则)
2、 一定要以测试为基准。

一个应用程序里,串行的部分是永远都有的。
Amdahl定律 : 1/(F+(1-N)/N) F:必须被串行部分,程序最好的结果, 1/F。
多线程算法请参考《算法导论》——第27章 多线程算法

4.2 影响性能的因素

4.2.1 上下文切换(合理分配线程数)

是指CPU 从一个进程或线程切换到另一个进程或线程。一次上下文切换花费5000~10000个时钟周期,几微秒。在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

4.2.2 内存同步(减少锁的竞争)

一般指加锁,对加锁来说,需要增加额外的指令,这些指令都需要刷新缓存等等操作。

4.2.3 阻塞(减少锁的竞争)

会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作】。很明显这个操作包括两次额外的上下文切换。

4.3 提高性能的方法

4.3.1 减少锁的竞争

4.3.2 减少锁的粒度

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

public class FinenessLock {
    
    public final Set<String> users = new HashSet<String>();
    public final Set<String> queries = new HashSet<String>();
    
    public void addUser(String u) {
        synchronized (users) {//只对使用的对象加锁
            users.add(u); 
        }
    }
    
    public void addQuery(String q) {
        synchronized (users) {//只对使用的对象加锁
            queries.add(q);
        }
    }
}

4.3.3 缩小锁的范围

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

    private Map<String,String> matchMap = new HashMap<>();
    
    public synchronized boolean isMatch(String name,String regexp) {
        String key = "user."+name;
        String job = matchMap.get(key);
        if(job == null) {
            return false;
        }else {
            return Pattern.matches(regexp, job);//很耗费时间
        }
    }

变为:

    public  boolean isMatchReduce(String name,String regexp) {
        String key = "user."+name;
        String job ;
        synchronized(this) {
            job = matchMap.get(key);
        }
    
        if(job == null) {
            return false;
        }else {
            return Pattern.matches(regexp, job);
        }
    }

4.3.4 避免多余的缩减锁的范围

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

synchronized(this) {
  job = matchMap.get(key);
}
job = job + "sc"; // 时间太短,而加锁是会产生额外的指令的
synchronized(this) {
  job = matchMap.get(key);
}

合并后变为:

synchronized(this) {
  job = matchMap.get(key);
  job = job + "sc";
  job = matchMap.get(key);
}

4.3.5 锁分段

ConcurrrentHashMap就是典型的锁分段。
缺点:访问整个容器比较麻烦。

4.3.6 替换独占锁

在业务允许的情况下:

  • 1) 使用读写锁
  • 2)用自旋CAS
  • 3) 使用系统的并发容器

5.线程安全的单例模式

单例模式代码

5.1 双重检查锁定的问题

正确代码请参考: 双重检查SingletonDoubleCheck.java
双重检查锁定在ConcurrentHashMap中也有用到!

解决办法:加volatile关键字

5.2 懒汉式

懒汉式SingletonLazy.java
延迟初始化的其他应用
类初始化模式,也叫延迟占位模式。在单例类的内部由一个私有静态内部类来持有这个单例类的实例。
延迟占位模式还可以用在多线程下实例域的延迟赋值。

有个问题:static 类和 static成员变量在最初应该就初始化好了,那为什么叫做懒汉式,是不是因为new这个操作延迟?
Java中静态内部类可以访问其外部类的成员属性和方法,同时,静态内部类只有当被调用的时候才开始首次被加载,利用此特性,可以实现懒汉式,在静态内部类中静态初始化外部类的单一实例即可。

public class SingletonLazy {
    private SingletonLazy(){}

    //定义一个私有类,来持有当前类的实例
    private static class InstanceHolder{
        public static SingletonLazy instance = new SingletonLazy();
    }

    public static SingletonLazy getInstance(){
        return InstanceHolder.instance;
    }
}

延迟初始化有很多应用:

public class InstanceLazy {
    private Integer value;
    private Integer val ;//可能很大,如巨型数组1000000;

    public InstanceLazy(Integer value) {
        super();
        this.value = value;
    }

    public Integer getValue() {
        return value;
    }

    private static class ValHolder {
        public static Integer vHolder = new Integer(1000000);
    }

    public Integer getVal() {
        return ValHolder.vHolder;
    }
}

5.3 饿汉式

饿汉式SingletonHungry.java

  • 在声明的时候就new这个类的实例,因为在JVM中,对类的加载和类初始化,由虚拟机保证线程安全。
public class SingletonHungry {
    public static SingletonHungry singletonHungry = new SingletonHungry();
    private SingletonHungry(){}
}
  • 或者使用枚举。

参考

  • 1)享学课堂Mark老师笔记
  • 2)锁以及调度 请参考《现代操作系统》第2章、第6章
  • 3)多线程算法 请参考《算法导论 第3版》 第27章
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值