线程安全的情况分析

描述什么是线程安全

《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。
这句话的意思是:不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。相反,如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get()调用到期间不能调用set()),或者需要进行额外的同步(比如使用synchronized关键字等),那么就是线程不安全的。

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

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

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);
                    wrongCount.incrementAndGet();
                }
                marked[index] = true;
            }
        }
    }
}

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

public class MultiThreadDeadLockError implements Runnable {
    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        MultiThreadDeadLockError r1 = new MultiThreadDeadLockError();
        MultiThreadDeadLockError r2 = new MultiThreadDeadLockError();
        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");
                }
            }
        }
    }
}

数据争用:同时写数据,会造成一方的数据被丢弃或者写入错误,最终造成错误数据
竞争条件:执行顺序(读取文件内容:在文件写完之后读取,如果文件没写完就读取,造成顺序上的错误)

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

什么是发布:让这个对象可以让超出这个类范围的其他地方调用

  • public修饰的方法
/**
 * 描述:发布逸出
 */
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;
    }
    /**
     * 改进方法:返回副本
     * @return
     */
    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"));
    }
}
运行结果
周一
null
返回null就是将发布对象给修改了,这个不是我们希望看到的
  • return返回值是一个对象
  • 把对象作为参数传入其他方法

什么是逸出:被发布到了不该发布的地方

  • 1.方法返回一个private对象(private的本意是不让外部访问)

  • 2.还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
    2.1 在构造函数中未初始化完毕就this赋值

    /**
     * 描述:初始化未完毕,就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();
            }
        }
    }
    sleep(10)的结果是1 , 0因为初始化未完毕
    sleep(105)的结果是1 , 1 因为初始化完毕
    

    2.2 隐式逸出——注册监听事件

    /**
     * 观察者模式
     */
    public class MultiThreadsError5 {
        int count = 0;
        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.println(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 {
        }
    }
    

注册监听器比较隐晦的,这里监听器是一个匿名内部类,它可以持有外部类MultiThreadsError5的引用,所以它可以直接操作(读写)外部类的成员变量,注册监听器本身是线程安全的,但是由于count还没完成赋值100(for循环至少花了10ms以上的时间)。此时监听器拿到的count就是0

2.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", "周四");
            }
        }).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.concurrency_core.background.MultiThreadsError6.main(MultiThreadsError6.java:33)

初始化的工作在另一个线程中,还没有执行完毕,会报错(时间不同,结果不同),当上面代码sleep(1000)就会正确显示
举个例子:比如说我们在构造函数中,需要去拿到一个线程池的引用或者数据库的连接池,而我们在创建这个连接池或者连接的时候,它往往都是在后台新开线程的,只不过我们根本察觉不到,因为我们是调用它的构造函数,比如调用一个数据库连接的构造函数,它在构造函数后面也做了很多的动作,它需要用多线程去加快自己的处理速度,所以我们其实也隐含了在构造函数中执行了一个新开线程的工作,这样一来,我们发布的过早,就容易造成线程安全的问题。

用工厂模式修改注册监听的方法

/**
 * 描述:用工厂模式修复刚才的初始化问题
 */
public class MultiThreadsError7 {
    int count;
    private EventListener listener;

    private MultiThreadsError7(MySource source) {
        listener = new EventListener() {
            @Override
            public void onEvent(MultiThreadsError5.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();
        MultiThreadsError7 safeListener =getInstance(mySource);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mySource.eventCome(new MultiThreadsError5.Event() {});
            }
        }).start();
    }

    static class MySource {

        private EventListener listener;

        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }

        void eventCome(MultiThreadsError5.Event e) {
            if (listener != null) {
                listener.onEvent(e);
            } else {
                System.out.println("还未初始化完毕");
            }
        }

    }

    interface EventListener {

        void onEvent(MultiThreadsError5.Event e);
    }

    interface Event {

    }
}

工厂模式解决的问题:注册过程不是在构造方法中执行,而是在工厂方法中执行,工厂方法调用两部分内容:1.初始化值,2.注册

需要考虑线程安全的情况有哪些

  • 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act

1)read-modify-write: 一个线程读了一个共享数据,并且在此基础上更新数据。该例子在上面的index++ 已经展示过了
2)check-then-act: 一个线程读取了一个共享数据,并在此基础上决定其下一个的操作

  • 不同的数据之间存在捆绑关系的时候:IP和端口号
  • 我们使用其他类的时候,如果对方没有声明自己是线程安全的,那么大概率会存在并发问题

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

性能问题有哪些体现、什么是性能问题
为什么多线程会带来性能问题

调度:上下文切换

什么是上下文:保存现场(主要指在发生线程调度时,即可以运行的线程数超过CPU的数量)
上下文切换:可以认为是内核(操作系统的核心)在CPU上对于进程(包括线程)进行的活动:
(1)挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处,
(2)在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复,
(3)跳转到线程计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程
当某一个线程运行到比如Thread.sleep()的时候,调度器就会将线程阻塞,然后让另一个等待CPU的线程进入runnable状
态,上下文切换的开销时非常大,有时候甚至比线程执行的开销都大
缓存开销:缓存失效,一旦进行了上下文切换,cpu将执行不同线程的不同代码,导致原来的缓存失效。CPU将进行重新缓存

何时会导致密集的上下文切换

抢锁,IO

协作:内存同步

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值