多线程会导致的问题——安全、性能


前言

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

线程安全

当多线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
即 不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要做任何额外的处理(也就是像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。

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++的改正:打印出错误的地方和次数

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

改进程序的思路:

  1. 要打印错误的位置,可以添加一个标志,当一个线程修改这个位置的数据后,就把它设置为true,当第二个线程读到true时候,就说明冲突了。
  2. 针对1的问题:线程可能在设置成true之前就已经冲突了,因此添加一个synchronized 锁,保证依次读到。
  3. 然而,会出现这样的情况:线程1将要执行mark[index] = true; ,而线程2正在执行index++,这样就会导致在线程1标志的位置出错,因此要确保线程1 在执行synchronized代码块时,线程2已经执行完index++,在等待。所以在index++后面添加了CyclicBarrier,确保两个线程执行到synchronized代码块前的位置。
  4. 而在线程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(1, 1);
        } 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状态,这样的动作就是上下文切换。

上下文切换的步骤:

  1. 挂起当前线程
  2. 将线程状态存储在内存中

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


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值