0
点赞
收藏
分享

微信扫一扫

Java并发编程 线程安全

MaxWen 2021-09-24 阅读 77

前言

什么是线程安全?

什么情况下会出现线程安全问题

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

1.演示计数不准确(减少)

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

    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++;
        }
    }
}
表面上结果是18287
探测出错位置
public class MultiThreadsError implements Runnable {

    static MultiThreadsError instance = new MultiThreadsError();
    int index = 0;
    static AtomicInteger realIndex = new AtomicInteger();
    static AtomicInteger wrongCount = new AtomicInteger();
    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("错误次数" + wrongCount.get());

    }

    @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 (instance) {
                if (marked[index] && marked[index - 1]) {
                    System.out.println("发生错误index" + index);
                    wrongCount.incrementAndGet();
                }
                marked[index] = true;
            }
        }
    }
}
发生错误index11889
表面上结果是19999
真正运行的次数20000
错误次数1
当index发生碰撞时,当前marked[index] 由前一个线程设置为true。

2.死锁

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

发生死锁

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

3.1 什么是发布

https://www.cnblogs.com/CreateMyself/p/12459141.html

3.2 什么是逸出
1.方法返回一个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", "周四");
    }

    public Map<String, String> getStates() {
        return 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"));
    }
}

输出

周一
null
发现 private的states 本意是不允许被外部程序修改,却被修改了
2.还未完成初始化,构造函数还没完全执行完毕,就把对象提供给外界 如 :
  • 在构造函数中未初始化完毕就this赋值
public class MultiThreadsError4 {

    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        Thread.sleep(10);
//        Thread.sleep(105);
        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;
        MultiThreadsError4.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();
        }
    }
}

main方法中 Thread.sleep(10);时 输出结果

1,0

main方法中 Thread.sleep(105);时 输出结果

1,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);
            }

        });

        /***
         * 模拟执行业务逻辑 后再赋值count
         */
        try {
            TimeUnit.MILLISECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        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

我们期待是100 结果出现0

  • 构造函数中运行线程
/**
 * 描述:     构造函数中新建线程
 */
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", "周四");
            }
        }).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 com.kpioneer.thread.background.MultiThreadsError6.main(MultiThreadsError6.java:33)

接下来 我们Thread.sleep(1000); 再去执行 System.out.println(multiThreadsError6.getStates().get("1")); 结果输出

周一

因为调用时间不同,结果不同,这样的程序是不安全的。

3.3 如何解决逸出
  • 返回"副本" 解决逸出行为1
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", "周四");
    }

    public Map<String, String> getStatesImproved() {
        return new HashMap<>(states);
    }

    public static void main(String[] args) {
        MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
        System.out.println(multiThreadsError3.getStatesImproved().get("1"));
        multiThreadsError3.getStatesImproved().remove("1");
        System.out.println(multiThreadsError3.getStatesImproved().get("1"));

    }
}
周一
周一
  • 工厂模式解决逸出行为2构造函数未初始化
public class MultiThreadsError7 {

    int count;
    private final EventListener listener;

    private MultiThreadsError7(MySource source) {
        listener = new EventListener() {
            @Override
            public void onEvent(Event e) {
                System.out.println("\n我得到的数字是" + count);
            }

        };
        /***
         * 模拟执行业务逻辑 后再赋值count
         */
        try {
            TimeUnit.MILLISECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count = 100;
    }

    /**
     * 工厂方法
     *
     * @param source
     * @return
     */
    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 instance = MultiThreadsError7.getInstance(mySource);

    }

    interface EventListener {

        void onEvent(Event e);
    }

    interface Event {

    }

    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("还未初始化完毕");
            }
        }

    }
}

还未初始化完毕
注意:在实际开发中我们不会这么明显的犯错,但是也可能会被动犯错,比如调用数据库连接池(框架会自己开启线程初始化),如果我们过早调用就可能出错。

4. 四种需要考虑线程安全的情况

遇到以下四种需要考虑线程安全的情况,需要注意:

  1. 访问共享的变量或资源, 会有并发风险, 比如对象的属性, 静态变量, 共享缓存, 数据库等
    例如此文提到的例子, 用共享变量进行++操作
  2. 所有依赖时序的操作, 即使每一步操作都是线程安全的, 还是存在并发的问题.
    read-modify-write: 先读取, 再修改. check-then-act 先检查, 再执行.
    实际上本质是一样的, 一个线程先获取数据, 再进行下一步的操作. 主要可能的问题是, 数据读取后, 还有可能被其他线程修改. 所以在这种依赖时序的情况下, 可以用synchronized锁等操作.
  3. 不同的数据之间存在绑定关系的时候
    例如IP与端口号. 只要修改了IP就要修改端口号, 否则IP也是无效的. 因此遇到这种操作的时候, 要警醒原子的合并操作. 要么全部修改成功, 要么全部修改失败.
  4. 使用其他类的时候, 如果该类的注释声明了不是线程安全的, 那么就不应该在多线程的场景中使用, 而应该考虑其对应的线程安全的类,或者对其做一定处理保证线程安全,
    例如HashMap就不是线程安全的, 而ConcurrentHashMap则是线程安全的.
    悟空
举报

相关推荐

0 条评论