Java并发基础学习(六)——多线程会导致的问题

前言

多线程虽然会给我们程序带来很多的提高,但是其实也有些问题需要解决,如果不解决多线程带来的问题,其实多线程并不能较好的发挥其作用,一般来说主要是线程安全问题(对相关变量的访问控制问题)和性能问题(线程上下文切换带来的性能损耗等)。本篇博客就总结一下这两个内容

线程安全问题

关于线程安全问题,还是要先梳理一下这个概念,在《Java Concurrenty in Practice》一书中,对线程安全给出了一个定义——当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

换成一个稍微明白点的表述:不管业务中遇到怎样的多线程的方式访问某个对象或者某个方法的情况,在调用这个业务逻辑的时候,都不需要额外做任何处理,程序也能始终给出一致的结果,就称为线程安全。

变量访问

实例:a++多线程下出现消失的现象

/**
 * autor:liman
 * createtime:2021/9/22
 * comment: 运行结果出错的a++
 */
public class MultiThreadsErrorSimple implements Runnable {
    
    

    static MultiThreadsErrorSimple instance = new MultiThreadsErrorSimple();

    int index = 0;

    @Override
    public void run() {
    
    
        for (int i = 0; i < 10000; i++) {
    
    
            index++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread threadOne = new Thread(instance);
        Thread threadTwo = new Thread(instance);
        threadOne.start();
        threadTwo.start();
        threadOne.join();
        threadTwo.join();
        System.out.println(instance.index);
    }
}

活跃性问题

其实活跃性问题具体包含死锁和活锁等问题,这里先简单演示死锁的程序

/**
 * autor:liman
 * createtime:2021/9/23
 * comment:死锁示例
 */
public class DeadLockDemo implements Runnable{
    
    

    int flag = 1;
    static Object object01 = new Object();
    static Object object02 = new Object();

    public static void main(String[] args) {
    
    
        DeadLockDemo deadLockDemo01 = new DeadLockDemo();
        DeadLockDemo deadLockDemo02 = new DeadLockDemo();
        deadLockDemo01.flag=1;
        deadLockDemo02.flag=0;
        Thread thread01 = new Thread(deadLockDemo01);
        Thread thread02 = new Thread(deadLockDemo02);
        thread01.start();
        thread02.start();

    }

    @Override
    public void run() {
    
    
        System.out.println("当前的flag="+flag);
        if(1==flag){
    
    
            synchronized (object01){
    
    
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (object02){
    
    
                    System.out.println("escape dead lock,flag:1");
                }
            }
        }else{
    
    
            synchronized (object02){
    
    
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (object01){
    
    
                    System.out.println("escape dead lock,flag:1");
                }
            }
        }
    }
}

对象发布和逸出的问题

所谓的对象发布,其实就是将对象声明成public,逸出指的是对象发布到了不该发布的地方,关于逸出情况有些复杂,会对逸出进行详细说明

1、方法返回private对象

/**
 * autor:liman
 * createtime:2021/9/26
 * comment:对象发布和逸出时的线程安全问题
 * 发布逸出,方法返回一个private对象
 */
@Slf4j
public class ObjectPublish {
    
    

    private Map<String,String> states;

    public ObjectPublish() {
    
    
        this.states = new HashMap<>();
        states.put("1","周一");
        states.put("2","周二");
        states.put("3","周三");
        states.put("4","周四");
        states.put("5","周五");
        states.put("6","周六");
    }

    //states逸出了,因为这里将private的对象返回给外部接口,外部接口可以对其中的数据进行操作
    public Map<String,String> getStates(){
    
    
        return states;
    }

    public static void main(String[] args) {
    
    
        ObjectPublish objectPublish = new ObjectPublish();
        //外部接口拿到private对象,可以对其进行修改
        Map<String, String> states = objectPublish.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
    }
}

上述方法中的private类型的Map对象,被返回给外部,可以被多个线程频繁修改,最后的结果会出现错乱。

2、对象还没有完成初始化的时候,就把对象提供给外界

扫描二维码关注公众号,回复: 13238403 查看本文章

比如没有初始化完毕,就this赋值;构造函数中运行线程等等。

/**
 * autor:liman
 * createtime:2021/9/27
 * comment:构造函数没有构造完成,就返回了对象
 */
public class ObjectPublishWithOutConstructComplete {
    
    
    static Point point;

    public static void main(String[] args) throws InterruptedException {
    
    
        new PointMaker().start();
        Thread.sleep(10);//如果睡眠时间过短,Y这个属性是来不及初始化的
        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;
        //在没有初始化完成的时候,将本对象赋值给了外部对象的Point属性
        ObjectPublishWithOutConstructComplete.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();
        }
    }
}

上述并不能正常得到x =1,y=1的情况,因为在构造函数本身中,没有完成初始化的相关逻辑,就将本身赋值给了外部。

3、监听器模式下也会出现这类问题,这个比较隐晦。

/**
 * autor:liman
 * createtime:2021/9/27
 * comment: 比较隐晦的,监听器的时候,对象未初始化完成
 */
public class ObjectPublishListener {
    
    

    int count;

    public ObjectPublishListener(MySource mySource) {
    
    
        //这里需要访问外部的count,但是这里过早的暴露了EventListener对象,过早的操作没有初始化完成的count,会产生相关问题。
        mySource.registerListener(new EventListener() {
    
    
            @Override
            public void onEvent(Event e) {
    
    
                System.out.println("我得到的数字是" + count);
            }
        });

        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println(i);
        }
        count = 100;
    }

    public static void main(String[] args) {
    
    
		MySource source = new MySource();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                source.eventTriger(new Event(){
    
    });
            }
        }).start();

        ObjectPublishListener objectPublishListener = new ObjectPublishListener(source);
    }

    static class MySource {
    
    
        private EventListener eventListener;

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

        void eventTriger(Event e) {
    
    
            if (null != eventListener) {
    
    
                eventListener.onEvent(e);
            } else {
    
    
                System.out.println("还未初始化完成");
            }
        }
    }

    interface EventListener {
    
    
        void onEvent(Event e);
    }

    interface Event {
    
    

    }
}

上述代码中,在构造函数中进行事件的绑定,但是,在内部类构造事件的时候,引用了外部类的对象,导致外部类的属性提前逸出。

除了上述三种情况之外,还有一种在构造函数中启动线程的时候,也会出现相关线程安全的问题,这个就比较简单,这里就不做实例展示了。

相关的解决方式

1、针对方法返回private对象造成的数据不一致问题,我们可以采用返回数据副本的方式解决

//如果要返回,返回一个数据副本,并不真正返回原始数据
public Map<String,String> getStates(){
    
    
    return new HashMap<>(states);
}

2、针对对象还没有完成初始化的时候,就把对象提供给外界的问题,我们采用工厂模式进行解决

//使用工厂模式修复这个问题
public static ObjectPublishListenerThreadFix getInstance(MySource mySource){
    
    
    ObjectPublishListenerThreadFix listenerThreadFix = new ObjectPublishListenerThreadFix(mySource);
    //在初始化完成之后,再注册监听器
    mySource.registerListener(listenerThreadFix.eventListener);
    return listenerThreadFix;
}

private ObjectPublishListenerThreadFix(MySource mySource) {
    
    
    eventListener = new EventListener() {
    
    
        @Override
        public void onEvent(Event e) {
    
    
            System.out.print("我得到的数字是" + count);
        }
    };

    for (int i = 0; i < 1000; i++) {
    
    
        System.out.print(i);
    }
    System.out.println();
    count = 100;
}

上面只是简单列举了几个多线程数据不安全的情况,但是如果单独记忆这种代码级别的解决方案,是很痛苦的。下面针对几种需要考虑线程安全的问题,做一个小结

1、访问共享的变量或资源,会有并发风险,这里的共享变量或资源指的是:对象的属性,静态变量,共享缓存,数据库等等。

2、所有依赖时序的操作,即使每一步操作都是线程安全的,但是如果存在操作时序不对,比如操作的数据变量未初始化完成,依旧会产生并发问题。

3、不同的数据之前存在捆绑关系的时候,这种某种程度上和2比较像

性能问题

从某种程度上来讲,多线程可以提高复杂的运算效率,但是一定程度上多线程可能会带来性能提交

上下文切换

线程运行个数超过CPU核心数的时候,CPU就需要对线程进行调度,线程调度中就涉及线程切换,线程的切换的开销是很大的,CPU需要保存当前线程的运行场景,将当前线程的当前运行状态保存好,为载入新的运行线程做准备。这样来来回回其实是很耗费性能的。而引起密集的上下文切换的操作就包括抢锁和IO操作。

内存同步

多个线程之间,针对数据的同步其实大部分是基于JMM模型的,这种需要我们后续详细学习并总结,这里只是需要知道,多个线程之间,同步数据也是多线程消耗性能的一个原因。

总结

简单总结了一些多线程引起的线程安全问题,可以忽略,这个不需要做更多的学习,只需要知道Java多线程如果使用不当会存在线程安全问题。

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/120893154