JAVA并发编程-线程安全、性能问题

360 阅读7分钟

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

2、哪些场景需要额外注意线程安全问题

3、什么是多线程的上下文切换

1、线程安全

我是红色

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作(调度、交替、执行顺序),调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。 --《Java Concurrency In Practice》

1、线程不安全

比如读操作的同时完成写操作->不安全,需额外同步

但是否需要全都线程安全?--摇考虑运行速度、设计成本、如何平衡两者?

对于完全不用于多线程的程序,不需要考虑太多多线程相关的内容

2、什么情况下会出现线程安全问题,如何避免?

  • 运行结果错误:i++多线程下出现消失的请求现象
public class MultiThreadError implements Runnable {
    int index = 0;
    static MultiThreadError instance = new MultiThreadError();

    public static void main(String[] args) throws Exception {
        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++;
        }
    }
}

这是因为i++不是原子性操作,它本身是i=1,i+1,i=2这三个操作的集合,当线程1还在前2个操作的时候,线程2读到的i依然是i=1,那么线程1最终得到i=2,线程2最终也得到i=2,两次i++相当于只自加了1次。

  • 活跃性问题:死锁、活锁、饥饿
/**
 *  测试死锁,两个线程同时需要lock1和lock2,却先后各拿了一把锁
 */
public class DeadLock implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        String o1 = "lock1";
        String o2 = "lock2";
        Thread t1 = new Thread(new DeadLock(o1, o2));
        Thread t2 = new Thread(new DeadLock(o2, o1));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

    private String lock1;
    private Object lock2;

    public DeadLock(String lock1, String lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + " get " + lock1);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " get " + lock2);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

可以看出来两个线程各拿了一个锁就等待了。

debug可以看下几个线程的状态,monitor本质是BLOCKED状态。

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

发布:脱离了本类来到其他类就称为发布,比如一个对象被称为public,或者return一个对象,或者把一个对象作为一个参数传到了其他方法中。

逸出:被发布到了不该发布的地方。

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

/**
 * 方法返回一个private对象,这个对象可能被外界修改,产生坏影响
 */
public class returnPrivate {
    private Map<Integer, String> theMap;

    public returnPrivate() {
        theMap = new HashMap<>();
        theMap.put(1, "song");
        theMap.put(2, "xin");
        theMap.put(3, "ran");
    }

    public Map<Integer, String> getMap() {
        return this.theMap;
    }

    public static void main(String[] args) {
        returnPrivate rP = new returnPrivate();
        Map<Integer, String> map = rP.getMap();
        System.out.println(map.get(1));
        map.remove(1);
        Map<Integer, String> map2 = rP.getMap();
        System.out.println(map2.get(1));
    }
}

结果很尴尬,第一个人调用后对得到的map作了操作,第二个无法得到原来的正确map。

解决方法

用"副本"代替成员变量

    public Map<Integer, String> getMapImproved() {
        return new HashMap<>(this.theMap);
    }

2、还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如: 在构造函数中未初始化完毕this赋值;隐式逸出-注册监听事件;构造函数中运行线程。

/**
 * 构造函数中初始化还没完成就使用this进行赋值
 */
public class ThisInConstruct {
    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new MakePoint()).start();
        Thread.sleep(10);
//        Thread.sleep(110);
        System.out.println("Point is :" + point.toString());
    }
}

class Point {
    private int x, y;

    public Point(int x, int y) throws InterruptedException {
        this.x = x;
        ThisInConstruct.point = this;
        Thread.sleep(100);
        this.y = y;
    }

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

class MakePoint implements Runnable {

    @Override
    public void run() {
        try {
            new Point(1, 1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当在main中,只延时10ms时,结果为

延时为110ms时,结果为

也就是说,程序的结果与执行时间有关,这是很可怕的一件事情。

用观察者模式来实现监听,看上去没有使用this赋值,但实际上已经将内部变量隐式逸出了。

/**
 * 观察者模式来实现注册监听事件
 */
public class ObserverListen {
    int count;

    //构造方法中就订阅一个subject
    public ObserverListen(Subject subject) {
        Listner listner = new Listner() {
            @Override
            public void response() {
                System.out.println("\n 得到的count = " + count);
            }
        };
        subject.register(listner);
        //一个类中,监听其实是非主要部分,应该有一段主逻辑,用for来模拟
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 10;
    }

    public static void main(String[] args) {
        Subject sub = new Subject();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sub.eventCome(new Event(){});
            }
        }).start();
        ObserverListen observerListen = new ObserverListen(sub);
    }
}


class Subject {
    private Listner listner;

    void register(Listner listner) {
        this.listner = listner;
    }

    void eventCome(Event e) {
        if (null != this.listner) {
            listner.response();
        } else {
            System.out.println("Listener初始化未完成!");
        }
    }
}

interface Listner {
    void response();
}

interface Event {
}

表面上看上去,诶,我没有在构造方法中去做this赋值,但是监听的部分其实相当于有做了,所以看到count并不是10。

解决方法

分析代码其实容易看出来问题,就是注册监听器的时间不对,如果在初始化真正完成后,再去注册监听器就ok了,使用工厂模式进行修正。

public class ObserverListenUpdate {
    private int count;
    //把监听器定义为成员变量
    private Listner listner;

    //构造方法中就订阅一个subject
    public ObserverListenUpdate(Subject subject) {
        Listner listner = new Listner() {
            @Override
            public void response() {
                System.out.println("\n 得到的count = " + count);
            }
        };
        this.listner = listner;
        //一个类中,监听其实是非主要部分,应该有一段主逻辑,用for来模拟
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 10;
    }

    //用工厂方法去得到实例
    public static ObserverListenUpdate getInstance(Subject subject) {
        ObserverListenUpdate instanceSafe = new ObserverListenUpdate(subject);
        subject.register(instanceSafe.listner);
        return instanceSafe;
    }

    public static void main(String[] args) {
        Subject sub = new Subject();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sub.eventCome(new Event(){});
            }
        }).start();
        ObserverListenUpdate observerListen = getInstance(sub);
    }
}

第三类:

/**
 * 在构造函数中运行新线程,在获取新线程中赋值的变量出现NPE
 * 如果延时,就能获取到
 * 这种因为时间不同造成结果不同的程序,非常不安全,也不稳定
 */
public class ConstructNewThread {

    private Map<String, String> states;

    public ConstructNewThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                states = new HashMap<>();
                states.put("1", "song");
                states.put("2", "xin");
                states.put("3", "ran");
            }
        }).start();
    }

    public Map<String, String> getStates() {
        return states;
    }

    public static void main(String[] args) throws InterruptedException {
        ConstructNewThread multiThreadsError6 = new ConstructNewThread();
//        Thread.sleep(1000);
        System.out.println(multiThreadsError6.getStates().get("1"));
    }
}

当不在main中延时,获取不到变量

当使用了sleep进行延时

2、考虑线程安全的情况

1、访问共享的变量或资源,比如对象的属性、静态变量、共享缓存、数据库等

2、所有依赖时序的操作,即使每一步的操作都是线程安全的,还是存在并发问题。可以用synchronized锁,做原子操作

3、不同的数据之间存在捆绑关系的时候,比如ip和端口号,这两个结合在一起,才是正确的,所以要同时修改成功,或者同时修改失败

4、使用其他类的时候,如果对方没有生命自己是线程安全的。比如HashMap,如果要线程安全,使用ConcurrentHashMap

3、多线程会导致的问题

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

  • 服务响应慢、吞吐量低、资源消耗(比如内存)过高等

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

  • 调度:上下文切换,缓存开销,频繁地竞争锁,或者由于IO读写等原因导致频繁阻塞
  • 协作:内存同步->JAVA内存模型->为了数据的正确性,同步手段往往会使用禁止编译器优化,使CPU内的缓存失效。