多线程安全和性能问题

1,925 阅读5分钟

1. 线程安全

1.1 线程安全定义

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

  • 也就是说,当我们使用多线程访问某个对象的属性和方法时,不需要专门去做额外处理,程序就可以正常运行,就可以称为线程安全。

1.2 线程不安全

  • 由线程安全可知,如果我们在使用多线程访问对象时,对它的一些调用或者操作,需要加锁之类的额外操作,才可以正常运行,我们就可以称之为线程不安全。

1.3 为什么不把所有类都做成线程安全的?

  • 在运行速度上有影响:如果我们要把所有的类都做成线程安全的,那么必然我们会对对象的操作做一些加锁,此时多个线程做这些操作的时候,就无法同时进行。也会产生额外的开销。
  • 在设计上来说,也会增加设计上的成本,代码量也会增多,需要大量的人力去做线程安全开发的优化等。
  • 如果一个类不会应用在多线程中,我们也就没有必要去设计并发处理,无需去过度设计。

2 如何避免线程不安全?

2.1 案例说明

  1. 不安全的index++
/**
 * 第一种:运行结果出错。
 * 计数不准确(减少),找出具体位置
 *
 * @author yiren
 */
public class MultiThreadError implements Runnable {
    private static MultiThreadError multiThreadError = new MultiThreadError();

    private int index = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            index++;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadError);
        Thread thread2 = new Thread(multiThreadError);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadError.index);
    }
}
11852

Process finished with exit code 0
  • 注意上面的结果是不一定的
  • 我们看一下index++在两个线程同时执行的时候发生的一种情况,箭头为执行顺序

  • 由于线程调度,线程1和线程2会可能会有如上的执行顺序,也就是说,我们两个线程都在执行index++的时候会让index少加。

2.2 常见问题:死锁、活锁、饥饿

  1. 死锁案例
/**
 * 死锁问题
 *
 * @author yiren
 */
public class ThreadDeadlock {
    private static Object object1 = new Object();
    private static Object object2 = new Object();

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(" in 1 run");
            synchronized (object1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println("1");
                }
            }

        });
        Thread thread1 = new Thread(() -> {
            System.out.println(" in 2 run");
            synchronized (object2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println("2");
                }
            }
        });
        thread.start();
        thread1.start();
    }
}
 in 1 run
 in 2 run
 
  • 这个程序会一直停不下来,就卡在了每个run方法的第二个synchronized块

2.3 对象的发布和初始化的安全

  • 发布:使一个对象能够被当前范围之外的代码所使用。

  • 逸出:一种错误的发布。

    1. 方法返回一个private对象(private本意是不让外部访问)
    /**
     * 发布逸出
     * @author yiren
     */
    public class ReleaseEffusion {
        private Map<String, String> states;
    
        public ReleaseEffusion() {
            this.states = new HashMap<>();
            this.states.put("1", "周一");
            this.states.put("2", "周二");
            this.states.put("3", "周三");
            this.states.put("4", "周四");
            this.states.put("5", "周五");
            this.states.put("6", "周六");
            this.states.put("7", "周日");
        }
    
        /**
         * 假设提供星期服务。。。
         * @return map
         */
        public Map<String,String> getStates() {
            return this.states;
        }
    
        public static void main(String[] args) {
            ReleaseEffusion releaseEffusion = new ReleaseEffusion();
            Map<String, String> states = releaseEffusion.getStates();
            System.out.println(states.get("1"));
            states.remove("1");
            System.out.println(states.get("1"));
        }
    }
    
    1. 还未完成初始化就把对象提供给外界,如:
      • 构造函数中为初始化完毕就this赋值
      • 隐式逸出---注册监听器事件
      • 构造函数中运行线程
    /**
     * 还未初始化完成就发布对象
     * @author yiren
     */
    public class ReleaseEffusionInit {
        private static Point point;
    
        public static void main(String[] args) throws InterruptedException {
            PointMaker pointMaker = new PointMaker();
            pointMaker.start();
            Thread.sleep(10);
            if (null != point) {
                System.out.println(point);
            }
            TimeUnit.SECONDS.sleep(1);
            if (null != point) {
                System.out.println(point);
            }
        }
        private static class Point{
            private final int x, y;
    
            public Point(int x, int y) throws InterruptedException {
                this.x = x;
                ReleaseEffusionInit.point = this;
                TimeUnit.SECONDS.sleep(1);
    
                this.y = y;
            }
    
            @Override
            public String toString() {
                return "Point{x=" + x + ", y=" + y + '}';
            }
        }
    
        private static class PointMaker extends Thread {
            @Override
            public void run() {
                try {
                    new Point(1, 1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    Point{x=1, y=0}
    Point{x=1, y=1}
    
    Process finished with exit code 0 
    
    /**
     * 监听器模式
     * @author yiren
     */
    public class ReleaseEffusionListener {
    
        public static void main(String[] args) {
            Source source = new Source();
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                source.eventCome(new Event() {
                });
    
            }).start();
    
            new ReleaseEffusionListener(source);
        }
    
        int count;
    
        public ReleaseEffusionListener(Source source) {
            source.registerListener(event -> {
                System.out.println("\n我得到数字:" + count);
            });
            for (int i = 0; i < 10000; i++) {
                System.out.print(i);
            }
    
            count = 100;
        }
    
        private static class Source {
            private EventListener listener;
    
            void registerListener(EventListener eventListener) {
                this.listener = eventListener;
            }
    
            void eventCome(Event e) {
                if (null != listener) {
                    listener.onEvent(e);
                } else {
                    System.out.println("未初始化完毕");
                }
            }
        }
    
        private interface EventListener {
            void onEvent(Event e);
        }
    
        interface Event { }
    }
    
    0123456789.......
    我得到数字:0
    28532854285528...9999
    Process finished with exit code 0
    
    /**
     * 构造函数起线程
     * @author yiren
     */
    public class ReleaseEffusionConstructorStartThread {
        private Map<String, String> states;
    
        public ReleaseEffusionConstructorStartThread() {
            new Thread(() -> {
                this.states = new HashMap<>();
                this.states.put("1", "周一");
                this.states.put("2", "周二");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.states.put("3", "周三");
                this.states.put("4", "周四");
                this.states.put("5", "周五");
                this.states.put("6", "周六");
                this.states.put("7", "周日");
            }).start();
    
        }
    
        /**
         * 假设提供星期服务。。。
         *
         * @return map
         */
        public Map<String, String> getStates() {
            return this.states;
        }
    
        public static void main(String[] args) {
            ReleaseEffusionConstructorStartThread releaseEffusion = new ReleaseEffusionConstructorStartThread();
            Map<String, String> states = releaseEffusion.getStates();
            System.out.println(states.get("1"));
            states.remove("1");
            System.out.println(states.get("1"));
            System.out.println(states.get("3"));
        }
    }
    
    Exception in thread "main" java.lang.NullPointerException
    	at com.imyiren.concurrency.thread.safe.ReleaseEffusionConstructorStartThread.main(ReleaseEffusionConstructorStartThread.java:43)
    
    Process finished with exit code 1
    
  • 如何解决逸出

    1. 返回副本
    		// 上方代码加上这个方法就OK
        public Map<String, String> getStatesCopy() {
            return new HashMap<>(this.states);
        }
        
    
    1. 工厂模式修复上面监听器
    /**
     * 监听器模式 利用工厂模式 来修复一下
     * @author yiren
     */
    public class ReleaseEffusionListenerFix {
    
        public static void main(String[] args) {
            Source source = new Source();
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                source.eventCome(new Event() {
                });
    
            }).start();
    
            ReleaseEffusionListenerFix.getInstance(source);
        }
    
        private int count;
        private EventListener listener;
    
        public static ReleaseEffusionListenerFix getInstance(Source source) {
            ReleaseEffusionListenerFix releaseEffusionListenerFix = new ReleaseEffusionListenerFix(source);
            source.registerListener(releaseEffusionListenerFix.listener);
            return releaseEffusionListenerFix;
        }
    
        private ReleaseEffusionListenerFix(Source source) {
            listener = event -> System.out.println("\n我得到数字:" + count);
            for (int i = 0; i < 10000; i++) {
                System.out.print(i);
            }
    
            count = 100;
        }
    
        private static class Source {
            private EventListener listener;
    
            void registerListener(EventListener eventListener) {
                this.listener = eventListener;
            }
    
            void eventCome(Event e) {
                if (null != listener) {
                    listener.onEvent(e);
                } else {
                    System.out.println("未初始化完毕");
                }
            }
        }
    
        private interface EventListener {
            void onEvent(Event e);
        }
    
        interface Event { }
    }
    

2.3 需要考虑线程安全问题的一些情况

  • 访问共享的变量或者资源,如:属性、静态变量、缓存、数据库等
  • 需要顺序操作的,就算每步都是线程安全的,也可能会存在安全问题。如:先读取再修改,先检查再执行
  • 不同数据间存在绑定关系,如:ip和端口号
  • 使用其他人或者第三方提供的类的时候。核查对方是否声明线程安全。如:HashMapConcurrentHashMap

3. 多线程的性能问题

3.1 性能问题的体现

  • 最明显的体验就是慢!比如前端调用一个借口,很久才返回结果或者直接超时。

3.2 造成性能问题的原因

  1. 线程调度:上下文切换
    • 何为上下文?
      • 就是上下切换需要保存的线程状态或者说数据(比如:线程执行到了那里,各个 参与运算的寄存器是什么内容),以确保恢复线程的执行。
    • 缓存开销
      • 当一个线程在CPU运算时,有些是需要把数据放到CPU缓存中的,如果上下文切换,那么当前线程CPU的缓存就会失效了。那就CPU就需要重新对新的线程数据进行缓存。所以CPU在启动新线程的时候开始的时候回比较慢,这就是因为CPU之前的缓存大部分都失效了。
    • 怎么样会导致频繁的上下文切换?
      • 多个线程进行竞争锁,还有就是IO读写
  2. 多个线程协作:内存同步
    • 我们的程序运行,编译器和CPU都会对程序进行优化,如指令重排序以更大得利用缓存,但是如果多线程写作的时候,我们就会利用一些手段禁止指令重排序以确保线程安全。还有就是当我们多个线程运行时,JMM中表明,线程会有私有内存区域,如果我们多线程要确保最新数据就会去主存中同步最新数据,这也会带来性能开销。

4. 面试问题

  1. 一共有哪几类线程安全问题?

  2. 那些场景需要额外注意线程安全问题?

  3. 什么是多线程带来的上下文切换?