并发安全

作者: xiaolyuh | 来源:发表于2019-08-20 19:27 被阅读3次

    在并发的情况下使用和调度一个类,这个类总是能表现出正确的行为,那么我们就说这个类是并发安全的类。

    类线程安全的表现为: 操作的原子性和内存的可见性。

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

    栈封闭

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

    无状态

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

    让类不可变

    我们常见的不可变类有:String,包装类,LoacalDateTime。

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

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

    volatile

    volatile可以保证内存的可见性,在一个线程写,多个线程读的情况下可以保证类的安全。

    安全的发布

    当一个类的成员变量不是线程安全的,并且其他类可以通过get方法获取到这个成员变量,然后对这个成员变量做修改,这样会导致并发问题。特别是成员变量是一个对象引用的时候,这个问题表象更加明显。

    解决办法:

    • 用线程安全的容器替换不安全的容器
    • 不直接返回对象引用而是返回对象副本,比如深拷贝
    • 加锁

    TheadLocal

    ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。因为只在线程内部有效,所以是线程安全的。

    加锁和CAS

    我们在解决线程安全问题用得最多的就是加锁或者CAS。

    线程不安全引发的问题

    死锁

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

    死锁产生的根本原因是:加锁的顺序不一致

    动态顺序死锁

    即使我们在方法中保证了加锁顺序,但是由于外部调用问题,导致无法保证加锁顺序,进而产生死锁。如下:

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        User a = new User(), b = new User();
        ThreadTaskUtils.run(() -> lockTest.deadlock(a, b));
        ThreadTaskUtils.run(() -> lockTest.deadlock(b, a));
    }
    
    private static void deadlock(User a, User b) {
        synchronized (a) {
            sleep(1000);
            synchronized (b) {
                sleep(1000);
                System.out.println(Thread.currentThread().getName() + " 死锁示例");
            }
        }
    }
    

    动态顺序死锁有两种解决办法:

    1. 在方法内部排序,保证加锁顺序
    2. 使用tryLock尝试拿锁,拿锁失败以后,休眠随机数,以避免活锁

    在方法内部排序,保证加锁顺序:

    Object lock = new Object();
    
    /**
     * 死锁解决办法,通过内在排序,保证加锁的顺序性
     *
     * @param a a
     * @param b b
     */
    private void lock1(User a, User b) {
        // 使用原生的HashCode方法,防止hashCode方法被重写导致的一些问题,
        // 如果能确保use对象中的id是唯一且不会重复,可以直接使用userId
        int aHashCode = System.identityHashCode(a);
        int bHashCode = System.identityHashCode(b);
    
        if (aHashCode > bHashCode) {
            synchronized (a) {
                sleep(1000);
                synchronized (b) {
                    sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 死锁示例");
                }
            }
        } else if (aHashCode < bHashCode) {
            synchronized (b) {
                sleep(1000);
                synchronized (a) {
                    sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 死锁示例");
                }
            }
        } else {
            // 引入一个外部锁,必须先获取到该锁
            synchronized (lock) {
                synchronized (a) {
                    sleep(1000);
                    synchronized (b) {
                        sleep(1000);
                        System.out.println(Thread.currentThread().getName() + " 死锁示例");
                    }
                }
            }
        }
    }
    
    1. 使用原生的HashCode方法,防止hashCode方法被重写导致的一些问题,如果能确保use对象中的id是唯一且不会重复,可以直接使用userId
    2. 引入一个外部锁,解决hash冲突问题,概率很小。

    使用tryLock尝试拿锁:

    Random random = new Random();
    /**
     * 使用tryLock尝试拿锁
     *
     * @param a a
     * @param b b
     */
    private void lock2(User a, User b) {
        while (true) {
            try {
                if (a.getLock().tryLock()) {
                    try {
                        if (b.getLock().tryLock()) {
                            System.out.println(Thread.currentThread().getName() + " 死锁示例");
                            break;
                        }
                    } finally {
                        b.getLock().unlock();
                    }
                }
            } finally {
                a.getLock().unlock();
            }
            // 拿锁失败以后,休眠随机数,以避免活锁
            sleep(random.nextInt());
        }
    }
    

    拿锁失败以后,休眠随机数,以避免活锁

    查看死锁的方法

    1. 通过jps 查询应用的 id,如:
    D:\workspace\spring-boot-student>jps
    15088
    21072 KotlinCompileDaemon
    13236 Jps
    20632 LockTest
    17436 Launcher
    17612 RemoteMavenServer
    908 Launcher
    
    1. 再通过jstack id 查看应用的锁的持有情况
    D:\workspace\spring-boot-student>jstack 20632
    2019-08-20 18:09:38
    Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.112-b15 mixed mode):
    
    "DestroyJavaVM" #15 prio=5 os_prio=0 tid=0x0000000002f56000 nid=0x3e94 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "ThreadPoolTaskExecutor-2" #14 prio=5 os_prio=0 tid=0x000000001ead8000 nid=0x42b8 waiting for monitor entry [0x000000001f1bf000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.xiaolyuh.LockTest.deadlock(LockTest.java:38)
            - waiting to lock <0x000000076ce0d1b0> (a com.xiaolyuh.User)
            - locked <0x000000076ce0d1c0> (a com.xiaolyuh.User)
            at com.xiaolyuh.LockTest.lambda$main$3(LockTest.java:31)
            at com.xiaolyuh.LockTest$$Lambda$2/668210649.run(Unknown Source)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
            at java.lang.Thread.run(Thread.java:745)
    
    "ThreadPoolTaskExecutor-1" #13 prio=5 os_prio=0 tid=0x000000001ebd8800 nid=0x481c waiting for monitor entry [0x000000001f0bf000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.xiaolyuh.LockTest.deadlock(LockTest.java:38)
            - waiting to lock <0x000000076ce0d1c0> (a com.xiaolyuh.User)
            - locked <0x000000076ce0d1b0> (a com.xiaolyuh.User)
            at com.xiaolyuh.LockTest.lambda$main$2(LockTest.java:30)
            at com.xiaolyuh.LockTest$$Lambda$1/1165897474.run(Unknown Source)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
            at java.lang.Thread.run(Thread.java:745)
    
    "Service Thread" #12 daemon prio=9 os_prio=0 tid=0x000000001dc8e000 nid=0x2d40 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C1 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x000000001dc2f000 nid=0x5538 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x000000001dc2d800 nid=0xd4c waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x000000001dc2d000 nid=0x1ac8 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x000000001c590000 nid=0x520c runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x000000001c58c800 nid=0x42c4 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x000000001c589800 nid=0x5698 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001c561000 nid=0x4e00 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001d953800 nid=0xa08 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    

    其他安全问题

    活锁

    尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
    解决办法:每个线程休眠随机数,错开拿锁的时间。

    线程饥饿

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

    相关文章

      网友评论

        本文标题:并发安全

        本文链接:https://www.haomeiwen.com/subject/lamrjctx.html