你创建的线程安全吗?谈谈多线程的安全和性能问题

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

点击关注公众号,利用碎片时间学习

前言

本文主要介绍多线程的安全和性能问题,包括几个线程不安全的例子和解决办法

线程安全

当多线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

即 不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要做任何额外的处理(也就是像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。

1 运行结果错误

1.1 a++的例子

创建两个线程,让这两个线程对同一个数进行++操作,执行10000次

public class MultiThreadError implements Runnable {
    int index = 0;
    static MultiThreadError instance = new MultiThreadError();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("打印的结果是:" + instance.index);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            index++;
        }
    }
}

打印结果:

你创建的线程安全吗?谈谈多线程的安全和性能问题结果不是20000原因:

你创建的线程安全吗?谈谈多线程的安全和性能问题在线程1将index+1后,由于线程2已经读到了index被线程1 ++前的数据,因此,相当于线程2拿到了一个假数据,线程1对index的++无效。

1.2 a++的改正:打印出错误的地方和次数

要让一个线程变成安全的就要付出代价,包括运行速度、设计成本等,在设计程序时,要考虑程序对线程安全的需求,比如程序是否要完全确保线程安全。

改进程序的思路:

  • 要打印错误的位置,可以添加一个标志,当一个线程修改这个位置的数据后,就把它设置为true,当第二个线程读到true时候,就说明冲突了。

  • 针对1的问题:线程可能在设置成true之前就已经冲突了,因此添加一个synchronized 锁,保证依次读到。

  • 然而,会出现这样的情况:线程1将要执行mark[index] = true; ,而线程2正在执行index++,这样就会导致在线程1标志的位置出错,因此要确保线程1 在执行synchronized代码块时,线程2已经执行完index++,在等待。所以在index++后面添加了CyclicBarrier,确保两个线程执行到synchronized代码块前的位置。

  • 而在线程1执行synchronized代码块时,有可能线程2 在index++,因此在index++前面也要用CyclicBarrier确保位置。

public class MultiThreadError implements Runnable {
    int index = 0;
    static MultiThreadError instance = new MultiThreadError();
    final boolean[] mark = new boolean[100000];
    static AtomicInteger realCount = new AtomicInteger();
    static AtomicInteger wrongCount = new AtomicInteger();
    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);    //参数为要等待几个线程
    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            try {
                cyclicBarrier2.reset();
                cyclicBarrier1.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            index++;
            try {
                cyclicBarrier1.reset();
                cyclicBarrier2.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            realCount.incrementAndGet();
            synchronized (instance) {
                if (mark[index] == true && mark[index - 1] == true) {
                    wrongCount.incrementAndGet();
                    System.out.println("在" + index + "发生错误");
                } else {
                    mark[index] = true;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
//        thread1.setDaemon(true); //设置为守护线程
//        thread2.setDaemon(true);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("打印的结果是:" + instance.index);
        System.out.println("真正运行次数:" + realCount.get());
        System.out.println("错误次数:" + wrongCount.get());
    }
}

输出结果:

你创建的线程安全吗?谈谈多线程的安全和性能问题

你创建的线程安全吗?谈谈多线程的安全和性能问题可以准确找到出错位置.

1.3 题外话——守护线程

在另一篇博客中https://blog.csdn.net/qq_44357371/article/details/108344885,谈到了守护线程:将用户线程设置为守护线程,会变得危险

这句话就可以在这里得到印证,假若我们将创建的两个子线程设置为守护线程,就会导致主线程会在子线程之前就结束,导致无法将20000打印完。

2 活跃性问题:死锁

你创建的线程安全吗?谈谈多线程的安全和性能问题死锁就是两个线程互相等待对方持有的资源。

public class DeadLockMultiThreadError implements Runnable {

    int flag = 1;
    static Object object1 = new Object();
    static Object object2 = new Object();

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (object1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println("1 结束了");
                }
            }
        }
        if (flag == 0) {
            synchronized (object2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println("2 结束了");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLockMultiThreadError deadLockMultiThreadError1 = new DeadLockMultiThreadError();
        DeadLockMultiThreadError deadLockMultiThreadError2 = new DeadLockMultiThreadError();
        deadLockMultiThreadError1.flag = 1;
        deadLockMultiThreadError2.flag = 0;
        new Thread(deadLockMultiThreadError1).start(); //让线程1获取object1,等待资源object2
        new Thread(deadLockMultiThreadError2).start(); //让线程2获取object2,等待资源object1
    }
}

两个线程都需要获得到对方持有的资源才可以结束,因此死锁,程序永远无法结束。

你创建的线程安全吗?谈谈多线程的安全和性能问题

3 对象发布和初始化的时候的安全问题

3.1 方法内返回了一个private对象

用private创建一个表,不让外部访问,但是在下面的getStates方法中,将private对象发布了出去,使其丧失了原本的属性。这就导致外部可以对private数据随意的篡改,使线程不安全。

public class EscapeMultiThreadError {
    private Map<String,String> states;
    public EscapeMultiThreadError(){
        states = new HashMap<>();
        states.put("1","周一");
        states.put("2","周二");
        states.put("3","周三");
        states.put("4","周四");
    }

    public Map<String, String> getStates() {
        return states;  //这里将private对象states发布了出去
    }
    public static void main(String[] args) {
        EscapeMultiThreadError escapeMultiThreadError = new EscapeMultiThreadError();
        Map<String ,String> states = escapeMultiThreadError.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
    }
}
3.2 解决逸出:返回副本

在上面发布的时候,是将原有的private数据直接发布,造成不安全。可以创建一个副本,发布时候只发出副本,而不会对原数据造成影响。

public class EscapeMultiThreadError {
    private Map<String,String> states;
    public EscapeMultiThreadError(){
        states = new HashMap<>();
        states.put("1","周一");
        states.put("2","周二");
        states.put("3","周三");
        states.put("4","周四");
    }

    public Map<String, String> getStates() {
        return states;  //这里将private对象states发布了出去
    }
    public Map<String, String> getStatesImproved() {
        return new HashMap<>(states);  //这里将private对象states发布了出去
    }
    public static void main(String[] args) {
        EscapeMultiThreadError escapeMultiThreadError = new EscapeMultiThreadError();
        Map<String ,String> states = escapeMultiThreadError.getStates();
//        System.out.println(states.get("1"));
//        states.remove("1");
//        System.out.println(states.get("1"));
        Map<String ,String> statesImproved = escapeMultiThreadError.getStatesImproved();
        System.out.println(statesImproved.get("1"));
        statesImproved.remove("1");
        System.out.println(states.get("1"));
    }
}

4 构造函数中未初始化完就this赋值

public class EscapeMultiThreadError2 {
    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        //这里会随时间的不同导致结果不一样
//        Thread.sleep(1000);
        if (point != null) {
            System.out.println(point);
        }
    }
}

class Point {
    private final int x, y;

    public Point(int x, int y) throws InterruptedException {
        this.x = x;
        EscapeMultiThreadError2.point = this;   //未初始化完毕就构造赋值
        Thread.sleep(100);
        this.y = y; //这里才给y赋值
    }

    @Override
    public String toString() {
        return x + "," + y;
    }
}

class PointMaker extends Thread {
    @Override
    public void run() {
        try {
            new Point(11);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
你创建的线程安全吗?谈谈多线程的安全和性能问题
你创建的线程安全吗?谈谈多线程的安全和性能问题

5 监听器模式中的隐式逸出

public class ObserverMultiThreadError {
    int count;

    public ObserverMultiThreadError(MySource source) {
        source.registerListener(new EventListener() {   //这里注册的监听器其实可以直接获取到外部的count,所以当count没有赋值完成时,它会直接打印出0
            @Override
            public void onEvent(Event e) {
                System.out.println("n我得到的数字是" + count);
            }
        });
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }

    public static void main(String[] args) {
        MySource mySource = new MySource();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10); //当这里的休眠时间很短时,由于前面打印数字的任务还没有结束,对count的赋值还没有完成,所有,后面直接运行了eventCome,跳到onEvent中,将count=0打印了出来。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mySource.eventCome(new Event() {
                });
            }
        }).start();
        ObserverMultiThreadError multiThreadError = new ObserverMultiThreadError(mySource);
    }

    static class MySource {
        private EventListener eventListener;

        void registerListener(EventListener eventListener) {
            this.eventListener = eventListener;
        }

        void eventCome(Event e) {
            if (eventListener != null) {
                eventListener.onEvent(e);
            } else {
                System.out.println("还未初始化完毕");
            }
        }
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}
5.1 对监听器中隐式逸出的修正

思路:要保证对count的赋值已完成才能打印

方法:用工厂模式,先去得到一个ObserverMulyiThreadErrorFix的实例,但这时并没有真正的注册创建,在实例获取到之后,在对实例进行注册

ObserverMulyiThreadErrorFix safeListener = new ObserverMulyiThreadErrorFix(mySource);   //这里只是创建了出来,还没有真正的注册上去
mySource.registerListener(safeListener.eventListener);  //这里才真正的注册进去,这时的count值才真正生效
public class ObserverMulyiThreadErrorFix {
    int count;
    private EventListener eventListener;

    private ObserverMulyiThreadErrorFix(ObserverMulyiThreadErrorFix.MySource source) {
        eventListener = new ObserverMulyiThreadErrorFix.EventListener() {   //这里注册的监听器其实可以直接获取到外部的count,所以当count没有赋值完成时,它会直接打印出0
            @Override
            public void onEvent(ObserverMulyiThreadErrorFix.Event e) {
                System.out.println("n我得到的数字是" + count);
            }
        };
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }

    public static ObserverMulyiThreadErrorFix getInstance(MySource mySource) {
        ObserverMulyiThreadErrorFix safeListener = new ObserverMulyiThreadErrorFix(mySource);   //这里只是创建了出来,还没有真正的注册上去
        mySource.registerListener(safeListener.eventListener);  //这里才真正的注册进去,这时的count值才真正生效
        return safeListener;
    }

    public static void main(String[] args) {
        ObserverMulyiThreadErrorFix.MySource mySource = new ObserverMulyiThreadErrorFix.MySource();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mySource.eventCome(new ObserverMulyiThreadErrorFix.Event() {
                });
            }
        }).start();
        ObserverMulyiThreadErrorFix multiThreadError = new ObserverMulyiThreadErrorFix(mySource);
    }

    static class MySource {
        private ObserverMulyiThreadErrorFix.EventListener eventListener;

        void registerListener(ObserverMulyiThreadErrorFix.EventListener eventListener) {
            this.eventListener = eventListener;
        }

        void eventCome(ObserverMulyiThreadErrorFix.Event e) {
            if (eventListener != null) {
                eventListener.onEvent(e);
            } else {
                System.out.println("还未初始化完毕");
            }
        }
    }

    interface EventListener {
        void onEvent(ObserverMulyiThreadErrorFix.Event e);
    }

    interface Event {
    }
}

总结

需要考虑线程安全的情况

你创建的线程安全吗?谈谈多线程的安全和性能问题

线程性能

当可用性的线程数大于CPU数时,会发生线程调度,而在线程调度时,需要上下文来保存线程(寄存器里面暂存的内容,比如线程状态)。

当某一个线程运行到Thread.sleep(),调度器会将线程阻塞,然后让另一个等待CPU的线程进入到runnable状态,这样的动作就是上下文切换。

上下文切换的步骤:

  • 挂起当前线程
  • 将线程状态存储在内存中

因此上下文切换的开销是非常大的,包括时间开销和缓存开销,这会极大的影响线程的性能。

来源:blog.csdn.net/qq_44357371/article/

details/108363848

推荐:

主流Java进阶技术(学习资料分享)

你创建的线程安全吗?谈谈多线程的安全和性能问题
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!  

原文始发于微信公众号(Java笔记虾):你创建的线程安全吗?谈谈多线程的安全和性能问题