美文网首页
Java线程系列——线程安全

Java线程系列——线程安全

作者: 禺沫 | 来源:发表于2020-03-02 18:03 被阅读0次

    一、什么是线程安全:

    《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:
    “当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。”

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

    那为什么不能把代码都写成线程安全的呢?其实是涉及到一些运行速度,设计成本等的考虑。不能过度设计。

    二、什么情况下会出现线程安全问题,怎么避免?

    1. 数据征用,两方同时去写,其中一方数据要么丢弃,要么写入错误错误。

    运行结果错误:例如 a++多线程下出现消失的请求现象

    public class MultiThreadError implements Runnable {
    
        static MultiThreadError instance = new MultiThreadError();
        int index = 0;
    
        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++;
            }
        }
    }
    

    运行结果:(每次不同)

    13048

    运行结果不确定,到底是在哪里出的问题?

    public class MultiThreadError1 implements Runnable {
    
        static MultiThreadError1 instance = new MultiThreadError1();
        int index = 0;
        static AtomicInteger realIndex = new AtomicInteger();
        static AtomicInteger wrongIndex = new AtomicInteger();//cas
    
        static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
        static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
        final boolean[] marked = new boolean[10000000];
    
        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);
            System.out.println("真正运行的词数:" + realIndex.get());
            System.out.println("错误次数:" + wrongIndex.get());
        }
    
        @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();
                }
                realIndex.incrementAndGet();
                synchronized (instance) { //保证可见性
                    if (marked[index] && marked[index -1]) {
                        System.out.println("发生错误" + index);
                        wrongIndex.incrementAndGet();
                    }
                    marked[index] = true;
                }
            }
        }
    }
    

    运行结果:

    发生错误12995
    表面上结果是:19999
    真正运行的词数:20000
    错误次数:1

    线程安全问题.jpg
    2.活跃性问题:死锁、活锁、饥饿

    下面的代码演示死锁:

    public class MultiThreadError2 implements Runnable {
        int flag = 1;
        static Object o1 = new Object();
        static Object o2 = new Object();
    
        public static void main(String[] args) {
            MultiThreadError2 multiThreadError1 = new MultiThreadError2();
            MultiThreadError2 multiThreadError2 = new MultiThreadError2();
            multiThreadError1.flag = 1;
            multiThreadError2.flag = 0;
            new Thread(multiThreadError1).start();
            new Thread(multiThreadError2).start();
        }
    
        @Override
        public void run() {
            System.out.println("flag = " + flag);
            if (flag == 1) {
                synchronized (o1) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2){
                        System.out.println("1");
                    }
                }
            }
    
            if(flag == 0){
                synchronized (o2){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o1){
                        System.out.println("0");
                    }
                }
            }
        }
    }
    

    运行结果:

    flag = 1
    flag = 0

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

    什么是发布?
    让一个对象超出这个类的范围去使用。比如一个对象被声明成了public,那么就被发布出去了;或者一个方法的return是个对象的话,那么调用这个方法的类,也获得了该对象。我们把这个类传到其他类的方法中,也是脱离了本类,来到了其他类。这些都叫做发布。发布是我们时时刻刻在做的事情。本来没什么问题,但是一旦发生了逸出,就有问题了。

    那什么又是逸出呢?

    • 方法返回一个private对象(private的本意是不让外部访问)
    public class MultiThreadsError3 {
        private Map<String, String> states;
    
        public MultiThreadsError3() {
            states = new HashMap<>();
            states.put("1", "周一");
            states.put("2", "周二");
            states.put("3", "周三");
            states.put("4", "周四");
            states.put("5", "周五");
            states.put("6", "周六");
            states.put("7", "周日");
        }
    
        //比如提供一个星期服务, 不能提供星期的改写,但是写出了这个方法, 造成了逸出
        public Map<String, String> getStates() {
            return states;
        }
    
        public Map<String, String> getStatesImproved() {
            return new HashMap<>(states);
        }
    
        public static void main(String[] args) {
            MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
            Map<String, String> states = multiThreadsError3.getStates();
           //造成逸出
            System.out.println(states.get("1"));
            states.remove("1");
            System.out.println(states.get("1"));
           //不造成逸出
    //        System.out.println(multiThreadsError3.getStatesImproved().get("1"));
    //        multiThreadsError3.getStatesImproved().remove("1");
    //        System.out.println(multiThreadsError3.getStatesImproved().get("1"));
        }
    }
    

    如果这样发布出去,给过被调用方篡改,会造成服务不可靠。改进方法是,发布一个数据copy对象,供调用方使用。

    • 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
      1.在构造函数中未初始化完毕就this赋值
    public class MultiThreadError4 {
        static Point point;
        public static void main(String[] args) throws InterruptedException {
            new PointMaker().start();
            Thread.sleep(10);
            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;
            MultiThreadError4.point = this;
            Thread.sleep(100);
            this.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();
            }
        }
    }
    
    1. 隐式逸出---注册监听事件
    public class MultiThreadsError5 {
        int count;
        public MultiThreadsError5(MySource source) {
            source.registerListener(new EventListener() {//匿名内部类会持有外部类的引用
                @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);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    mySource.eventCome(new Event() {
                    });
                }
            }).start();
            MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource);
        }
    
        static class MySource {
            private EventListener listener;
    
            void registerListener(EventListener eventListener) {
                this.listener = eventListener;
            }
    
            void eventCome(Event e) {
                if (listener != null) {
                    listener.onEvent(e);
                } else {
                    System.out.println("还未初始化完毕");
                }
            }
        }
    
        interface EventListener {
            void onEvent(Event e);
        }
        interface Event {
        }
    }
    

    运行结果:

    …(省略了一大段打印的意思)
    我得到的数字是:0

    新起线程又过了10ms后,此时还没有运行到count=100,而匿名内部类持有外部类的引用,所以此时count = 0;
    修改代码为:

    public class MultiThreadsError7 {
        int count;
        private EventListener listener;
        //工厂方法能,避免过早发布,后面代码没来的及执行的问题
        private MultiThreadsError7(MySource source) {
            listener = new EventListener() {
                @Override
                public void onEvent(Event e) {
                    System.out.println("\n 我得到的数字是:" + count);
                }
            };
            for (int i = 0; i < 10000; i++) {
                System.out.println(i);
            }
            count = 100;
        }
    
        public static MultiThreadsError7 getInstance(MySource source) {
            //这样会完成所有的初始化工作
            MultiThreadsError7 safeListener = new MultiThreadsError7(source);
            source.registerListener(safeListener.listener);
            return safeListener;
        }
    
        public static void main(String[] args) {
            MySource mySource = new MySource();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    mySource.eventCome(new Event() {
                    });
                }
            }).start();
    
    //        MultiThreadsError7 multiThreadsError5 = new MultiThreadsError7(mySource);
            MultiThreadsError7 multiThreadsError5 = MultiThreadsError7.getInstance(mySource);
        }
    
        static class MySource {
            private EventListener listener;
    
            void registerListener(EventListener eventListener) {
                this.listener = eventListener;
            }
    
            void eventCome(Event e) {
                if (listener != null) {
                    listener.onEvent(e);
                } else {
                    System.out.println("还未初始化完毕");
                }
            }
        }
    
        interface EventListener {
            void onEvent(Event e);
        }
    
        interface Event {
        }
    }
    

    3.构造函数中运行线程

    public class MultiThreadsError6 {
        private Map<String, String> states;
        public MultiThreadsError6() {
            //初始化的工作不能放在新线程中,否则无法判断什么时候初始化完成,从而容易引发空指针等问题。
            new Thread(new Runnable() {
                @Override
                public void run() {
                    states = new HashMap<>();
                    states.put("1", "周一");
                    states.put("2", "周二");
                    states.put("3", "周三");
                    states.put("4", "周四");
                    states.put("5", "周五");
                }
            }).start();
        }
    
        public Map<String, String> getStates() {
            return states;
        }
    
        public static void main(String[] args) throws InterruptedException {
            MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
            //造成时间不同,运行结果不同,从而产生错误
            //Thread.sleep(1000);
            System.out.println(multiThreadsError6.getStates().get("1"));
        }
    }
    

    运行结果:

    Exception in thread "main" java.lang.NullPointerException
    at background.MultiThreadsError6.main(MultiThreadsError6.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

    各种需要考虑线程安全的情况

    • 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
    • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act
    • 不同的数据之间存在捆绑关系的时候
    • 我们使用其他类的时候,如果对方没有声明自己是线程安全的

    三、双刃剑:多线程会导致的问题

    性能问题有哪些体现,什么是性能问题
    单线程不需要调度,不需要用锁,不需要用并发数据结构。

    多线程造成的性能问题主要原因:

    1. 调度:上下文切换。
      什么是上下文。
      context,发生线程调度的调度。最早的CPU只有进程没有线程,后来有了线程。上下文切换,当某一个线程运行到,比如Thread.sleep想要进入阻塞状态。线程调度器会把线程阻塞,然后再让另外一个等待CPU线程进入到Runnable状态,这样的一个动作就是上下文切换。这种上下文切换,其实开销是非常大的。有的时候比线程的执行时间更长。通常而言,一次上下文切换,消耗5000到10000个CPU时钟周期。
      操作系统中,上下文是和寄存器,程序计数器相关的。一次上下文切换,主要包含以下这些活动,挂起线程,把线程目前的状态存到内存中的某处,这个状态就是上下文。这个上下文包含的经典内容,比如我这个线程执行到哪一个指令了,这个指令的位置在哪里,因为后续还要切换回来,跳转到阻塞之前的那个状态。还包括一些寄存器,程序技术器等。

    2. 缓存开销:
      对于CPU要考虑到缓存失效的问题。程序很大概率会访问,访问过的内容,for循环啊,CPU会根据不同的算法,做很多预测,把不同的数据缓存到CPU中,再次使用的时候回很快使用到了。但是一旦进行了上下文切换,那么CPU即将不同线程的不同代码,那么原来的缓存根本就没什么价值了。所以CPU需要重新进行缓存。这导致线程被调度之后,开始的启动速度会比较慢,因为大部分的缓存都失效了。所以CPU为了防止过于频繁的线程切换,会这是一个阈值。两次切换之间不能小于这个时间阈值,否则将导致线程开销的损耗,大于程序的执行。

    那么何时会导致密集的上下文切换?抢锁、IO。
    内存同步进行。编译器和CPU会帮我们把内存进行优化,这些是看不到的,背后的优化。背后的优化包含很多内容,可能会把指令重排序,让我们的缓存利用的更多一些。或者JVM也会把我们的锁进行优化,比如说它发现我们某些锁是没有必要的,把锁给自动删除了。或者啊,关于内存方面,JMM模型是规定我们有主内存以及各个CPU自己的缓存的。这种情况下,如果我们使用缓存。可以大大增加我们执行的速度,不必要每次和主线程进行同步。我们要用多线程,synchronized或者volatile这些关键字,会让不同线程的缓存失效,这样的话,也会由于内存同步的问题,带来一种开销。它就没有办法在自己的CPU内部进行缓存了,只能用主存,这就降低了效率。

    总结: 内存的协作,导致各个线程的缓存失效。切换导致额外的开销。

    相关文章

      网友评论

          本文标题:Java线程系列——线程安全

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