第十章、核心8:线程安全-多线程会导致的问题

1、线程安全

1.1 什么是线程安全

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

1.2 线程不安全:get同时set

  • 全都线程安全?:运行速度、设计成本、trade off
  • 完全不用于多线程的代码:不过度设计

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

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

/**
 * MultiThreadsError
 *
 * @author venlenter
 * @Description: 普通a++会导致count叠加错误,以下程序已优化处理
 * @since unknown, 2020-05-07
 */
public class MultiThreadsError3 implements Runnable {
    int index = 0;
    final boolean[] marked = new boolean[10000000];
    static AtomicInteger realIndex = new AtomicInteger();
    static AtomicInteger wrongCount = new AtomicInteger();
    static MultiThreadsError3 instance = new MultiThreadsError3();
    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

    @Override
    public void run() {
        marked[0] = true;
        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
            synchronized (instance) {
                if (marked[index] && marked[index - 1]) {
                    System.out.println("发生错误:" + index);
                    wrongCount.incrementAndGet();
                }
                marked[index] = true;
            }

        }
    }

    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("错误次数:" + wrongCount.get());
    }
}
//输出结果
表面上结果是:20000
真正运行的次数:20000
错误次数:0

1.3.2 活跃性问题:死锁、活锁、饥饿

/**
 * MultiThreadError
 *
 * @author venlenter
 * @Description: 第二章线程安全问题,演示死锁
 * @since unknown, 2020-05-11
 */
public class MultiThreadError implements Runnable {
    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        MultiThreadError r1 = new MultiThreadError();
        MultiThreadError r2 = new MultiThreadError();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).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
//程序一直等待不结束

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

什么是发布?

  • public、return都算是获得对象,发布了该对象出去

什么是溢出?

  • 1.方法返回一个private对象(定义了private对象的getXX()方法)(private的本意是不让外部访问)
  • 2.还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如

(1)在构造函数中未初始化完毕就把this赋值出去了

/**
 * MultiThreadsError4
 *
 * @author venlenter
 * @Description: 初始化未完毕,就this赋值
 * @since unknown, 2020-05-12
 */
public class MultiThreadsError4 {
    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        Thread.sleep(105);
        if (point != null) {
            System.out.println(point);
        }
    }
}

class Point {
    private final int x, y;

    Point(int x, int y) throws InterruptedException {
        this.x = x;
        //这里先行给point赋值this,此时外部拿到point对象只有x,没有y的值
        MultiThreadsError4.point = this;
        Thread.sleep(100);
        this.y = y;
    }

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

class PointMaker extends Thread {
    @Override
    public void run() {
        try {
            new Point(1, 1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//输出结果
//可能
Point{x=1, y=1}
//也可能
Point{x=1, y=0}

(2)隐式逸出————注册监听事件

/**
 * MultiThreadsError5
 *
 * @author venlenter
 * @Description: 观察者模式
 * @since unknown, 2020-05-12
 */
public class MultiThreadsError5 {
    int count;

    public MultiThreadsError5(MySource source) {
        source.registerListener(new EventListener() {
            @Override
            //这里EventListener是一个匿名内部类,实际上也用了count这个外部引用变量,当count未初始化完成,拿到的值就还是0
            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 {

    }
}
//输出结果
012345678910...
我得到的数字是:0
...

(3)构造函数中运行线程

/**
 * MultiThreadsError6
 *
 * @author venlenter
 * @Description: 构造函数中新建线程
 * @since unknown, 2020-05-14
 */
public class MultiThreadsError6 {
    private Map 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", "周四");
            }
        }).start();
    }

    public Map getStates() {
        return states;
    }

    public static void main(String[] args) {
        MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
        //在构造函数中states还未初始化完成,就get
        System.out.println(multiThreadsError6.getStates().get("1"));
    }
}
//输出结果
Exception in thread "main" java.lang.NullPointerException
	at ConcurrenceFolder.mooc.threadConcurrencyCore.background.MultiThreadsError6.main(MultiThreadsError6.java:34)

1.4 如何解决逸出

  • 返回“副本”(返回对象的deepCopy)--对应解决(1.方法返回了private对象)
  • 工厂模式--对应解决(2.还没初始化就吧对象提供给外界)

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

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

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

3.1 性能问题有哪些体现、什么是性能问题

  • 服务响应慢、吞吐量低、资源消耗(例如内存)过高等
  • 虽然不是结果错误,但仍然危害巨大
  • 引入多线程不能本末倒置

3.2 为什么多线程会带来性能问题

(1)调度:上下文切换

  • 什么是上下文?:线程A执行到某个地方,然后要切换到另一个线程B的时候,CPU会保存当前的线程A在CPU中的状态(上下文)到内存中的某处,等线程B执行完成后,回到线程A需要还原线程A之前保存的状态(这种切换需要耗时)
  • 缓存开销(考虑缓存失效):多线程切换,从线程A切换到线程B,线程A的缓存就失效了,需要重新加载
  • 何时会导致密集的上下文切换:抢锁、IO

(2)协作:内存同步

  • 为了数据的正确性,同步手段往往会使用禁止编译器优化、使CPU内的缓存失效(java内存模型)

4、常见面试问题

(1)你知道有哪些线程不安全的情况

  • 运行结果错误:a++多线程下出现消失的请求现象
  • 活跃性问题:死锁、活锁、饥饿
  • 对象发布和初始化时的安全问题

(2)平时哪些情况下需要额外注意线程安全问题?

(3)什么是多线程的上下文切换?

 

笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》

你可能感兴趣的:(深入浅出Java并发多线程,多线程,线程安全,a++)