一、线程安全

概述

1、定义

线程安全是指代码在多线程环境下执行时,仍然可以表现出现正确行为。

那代码会在多线程的情况下,出现线程不安全问题呢?这是因为存在共享变量。可以参考Java内存模型

2、线程不安全行为

(1)共享变量

因为共享变量而出现的线程不安全问题,主要是因为对共享变量的操作是非原子性的,经典的有俩种get-do-setcheck-then-act

如何解决因为共享变量而出现的线程不安全问题呢?

  • 不可变对象,比如String
  • 各种锁,比如synchonizedLock
  • 并发工具包,即juc

(2)死锁问题

死锁问题的定义就是俩个线程已经各自持有一个锁了,并且等待另一个线程释放锁的现象。

了解死锁的解决方案,以及预防死锁。

背景知识

1、java内存模型

Java Memory Model简称JMM,即java内存模型。下面有俩张图介绍了:

1574598682868

每个线程都有一个方法栈;堆中存放共享的变量。就是这些堆中变量而引发线程安全问题。

但是为了进一步减少CPU与内存之间速度得差异,公有变量会在每一个线程中存在私有得副本。有时候代码会因此出现很诡异得情况。

1574598764309

每个线程都会有一个工作内存,其存储的是公有变量的副本。它会定时的与主内存去同步。这也是为了提高CPU的运行效率。 这里就是导致一个问题:一个线程以为自己修改了公有变量,另一个会反应过来。但可能并没有。比如说:

public class Demo {

    static boolean cancelled = false;

    public static void main(String[] args) throws InterruptedException {
        Thread time = new Thread(() -> {
                while (true){

                    if (cancelled){
                        // 取消自己做的事情
                        break;
                    }

                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        e.printStackTrace();
                    }
                    // 做一些定时器相关的工作

                }
        });

        cancelled = true;
        time.start();
    }
}

比如说这个子线程在获取共享变量cancelled,假设它只从自己的工作内存读取这个cancelled,这就完了它直接执行定时器工作,但是其实我们业务上也取消这个。

2、volatile

上面的问题可以使用volatile这个关键字来解决的。它提供了: (1)可见性,并非原子性

可见性是指线程在操作共享变量得时候,会直接去主内存去操作,不会去线程得工作内存操作。可见,见的是线程对主内存中的共享变量。

标明volatile的变量会直接操作主内存变量,不会去处理线程工作内存的变量值。

(2)禁止指令重排

  • 什么是指令重排?

编译器和处理器都可能对指令进行重排,导致问题 。指令重排是在cpu运行指令时发生的操作,它会原来将某些不影响执行顺序的指令进行重新排列,以此来优化代码执行效率。比如:

public class Demo {

    static boolean initializationFinished  = false;

    public static void main(String[] args) {
        init();
        initializationFinished = true;
    }
}

当编译器认为这俩条语句直接顺序并没有直接关系,所以它可能把他们优化成:

initializationFinished = true;
init();

但这对业务来说,就是有问题,可能会产生莫名奇妙的错误。比如:

public class Demo {

    static boolean initializationFinished  = false;

    public static void main(String[] args) {
        init();
        initializationFinished = true;

        if (initializationFinished){
            //执行初始化之后的操作    
        }
    }
}

由于指令重排是在一个线程上的操作,如果主线程先修改了initializationFinished变量,子线程执行的逻辑顺序就不对了。这里就发生主线程还没执行完初始化方法,子线程就已经在做初始化后的方法了。

(3)注意

当使用其他同步手段的话,比如:synchronizedLock。这时候就没必要使用volatitlesynchronized、Lock属于更强的同步手段、它会产生可见性和禁止指令重排的效果

线程安全问题

1、非线程安全的操作

针对于共享变量而引出的非线程安全操作,都是由于对共享变量的操作是非原子性的。大概有俩种模式:

  • get-do-set
  • check-then-act

(1)get-do-set问题

    private static int globalI = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> globalI++).start();
        }
    }

这里的问题是globalI++这个操作不是原子的,它会分成三步完成。

(2)check-then-act问题

    private static Map<String, Object> values = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                if (!values.containsKey("key")){
                    values.put("key","value");
                }
            }).start();
        }
    }

看看,如果操作是非原子的话,即使是线程安全的容器也不管用。这里就是典型的check-then-do,尽管线程安全的容器的所有方法都是原子的,但俩个原子在一起就不是原子的了。这里可以使用putIfAbsent()方法。

2、如何解决

解决这个问题很简单,就是让非原子操作变成原子操作。这里有俩种方式:

  • 使用不可变对象???这是什么意思,final关键字也有作用,惊了
  • 使用线程安全容器。比如juc包下的类
  • 使用锁(synchonizedLock

死锁问题

1、定义

俩个线程都持有一个锁,并且都在等到对方释放锁的状态。称之为死锁。如何产生一个死锁:

public class Demo {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(5000);

                    synchronized (lock2){

                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        }).start();

        synchronized (lock2){
            try {
                Thread.sleep(5000);

                synchronized (lock1){

                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
    }
}

2、碰到死锁问题如何排查

程序死锁的时候,程序会处于静止的状态。它什么都做不了,跟死了似的。排查:

  • jps + jstack
  • 结合源代码
  • Object.wait() + Object.notify()等待唤醒机制
  • LockCondition

3、如何避免死锁

其实死锁产生的所有原因都是多个线程在获取锁的时候,没有以相同的顺序获取锁。所以解决方案也很简单,获取相同锁的线程要以相同顺序获取锁

但是实际开发中,由于代码过于复杂,一般无法在编码和代码审查阶段就避免这个问题。只有等报错的时候,再去处理这个问题。

results matching ""

    No results matching ""