JAVA并发编程(五):多线程安全和性能问题

1. 线程安全

1.1 线程安全定义

● 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以或得正确的结果,那么这个对象时线程安全的。
也就是说,当我们使用多线程访问某个对象的属性和方法时,而在编程这个业务逻辑的时候,不需要专门去做额外处理(也就是可以像单线程编程一样),程序就可以正常运行(不会因为多线程而出错),就可以称为线程安全
相反,如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get()调用到期间不能调用set()),或者需要进行额外的同步(比如使用synchronized关键字等),那么就是线程不安全的

1.2 线程不安全

● 由线程安全可知,如果我们在使用多线程访问对象时,对它的一些调用或者操作,需要加锁之类的额外操作,才可以正常运行,我们就可以称之为线程不安全。

1.3 为什么不把所有类都做成线程安全的?

● 在运行速度上有影响:如果我们要把所有的类都做成线程安全的,那么必然我们会对对象的操作做一些加锁,此时多个线程做这些操作的时候,就无法同时进行。也会产生额外的开销。
● 在设计上来说,也会增加设计上的成本,代码量也会增多,需要大量的人力去做线程安全开发的优化等。
● 如果一个类不会应用在多线程中,我们也就没有必要去设计并发处理,无需去过度设计。

2 如何避免线程不安全?

2.1 案例说明

  1. 不安全的index++
public class MultiThreadError implements Runnable {
    private static MultiThreadError multiThreadError = new MultiThreadError();
    private int index = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            index++;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadError);
        Thread thread2 = new Thread(multiThreadError);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadError.index);
    }
}
11852

● 注意上面的结果是不一定的
● 我们看一下index++在两个线程同时执行的时候发生的一种情况,箭头为执行顺序
在这里插入图片描述

● 由于线程调度,线程1和线程2会可能会有如上的执行顺序,也就是说,我们两个线程都在执行index++的时候会让index少加。

2.2 常见问题:死锁、活锁、饥饿

  1. 死锁案例
public class ThreadDeadlock {
    private static Object object1 = new Object();
    private static Object object2 = new Object();
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(" in 1 run");
            synchronized (object1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println("1");
                }
            }
        });
        Thread thread1 = new Thread(() -> {
            System.out.println(" in 2 run");
            synchronized (object2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println("2");
                }
            }
        });
        thread.start();
        thread1.start();
    }
}
in 1 run
 in 2 run

● 这个程序会一直停不下来,就卡在了每个run方法的第二个synchronized块

2.3 对象的发布和初始化的安全

● 发布:使一个对象能够被当前范围之外的代码所使用。
● 逸出:一种错误的发布。
a. 方法返回一个private对象(private本意是不让外部访问)

public class ReleaseEffusion {
    private Map<String, String> states;
    public ReleaseEffusion() {
        this.states = new HashMap<>();
        this.states.put("1", "周一");
        this.states.put("2", "周二");
        this.states.put("3", "周三");
        this.states.put("4", "周四");
        this.states.put("5", "周五");
        this.states.put("6", "周六");
        this.states.put("7", "周日");
    }
    /**
     * 假设提供星期服务。。。
     * @return map
     */
    public Map<String,String> getStates() {
        return this.states;
    }
    public static void main(String[] args) {
        ReleaseEffusion releaseEffusion = new ReleaseEffusion();
        Map<String, String> states = releaseEffusion.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
    }
}

a. 还未完成初始化就把对象提供给外界,如:
■ 构造函数中为初始化完毕就this赋值
■ 隐式逸出—注册监听器事件
■ 构造函数中运行线程

/**
 * 还未初始化完成就发布对象
 */
public class ReleaseEffusionInit {
    private static Point point;
    public static void main(String[] args) throws InterruptedException {
        PointMaker pointMaker = new PointMaker();
        pointMaker.start();
        Thread.sleep(10);
        if (null != point) {
            System.out.println(point);
        }
        TimeUnit.SECONDS.sleep(1);
        if (null != point) {
            System.out.println(point);
        }
    }
    private static class Point{
        private final int x, y;
        public Point(int x, int y) throws InterruptedException {
            this.x = x;
            ReleaseEffusionInit.point = this;
            TimeUnit.SECONDS.sleep(1);
            this.y = y;
        }
        @Override
        public String toString() {
            return "Point{x=" + x + ", y=" + y + '}';
        }
    }
    private static class PointMaker extends Thread {
        @Override
        public void run() {
            try {
                new Point(1, 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Point{x=1, y=0}
Point{x=1, y=1}
Process finished with exit code 0 
/**
 * 监听器模式
 */
public class ReleaseEffusionListener {
    public static void main(String[] args) {
        Source source = new Source();
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            source.eventCome(new Event() {
            });
        }).start();
        new ReleaseEffusionListener(source);
    }
    int count;
    public ReleaseEffusionListener(Source source) {
        source.registerListener(event -> {
            System.out.println("\n我得到数字:" + count);
        });
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }
    private static class Source {
        private EventListener listener;
        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }
        void eventCome(Event e) {
            if (null != listener) {
                listener.onEvent(e);
            } else {
                System.out.println("未初始化完毕");
            }
        }
    }
    private interface EventListener {
        void onEvent(Event e);
    }
    interface Event { }
}
0123456789.......
我得到数字:0
28532854285528...9999
Process finished with exit code 0
/**
 * 构造函数起线程
 * @author yiren
 */
public class ReleaseEffusionConstructorStartThread {
    private Map<String, String> states;
    public ReleaseEffusionConstructorStartThread() {
        new Thread(() -> {
            this.states = new HashMap<>();
            this.states.put("1", "周一");
            this.states.put("2", "周二");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.states.put("3", "周三");
            this.states.put("4", "周四");
            this.states.put("5", "周五");
            this.states.put("6", "周六");
            this.states.put("7", "周日");
        }).start();
    }
    /**
     * 假设提供星期服务。。。
     *
     * @return map
     */
    public Map<String, String> getStates() {
        return this.states;
    }
    public static void main(String[] args) {
        ReleaseEffusionConstructorStartThread releaseEffusion = new ReleaseEffusionConstructorStartThread();
        Map<String, String> states = releaseEffusion.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
        System.out.println(states.get("3"));
    }
}
Exception in thread "main" java.lang.NullPointerException
	at com.imyiren.concurrency.thread.safe.ReleaseEffusionConstructorStartThread.main(ReleaseEffusionConstructorStartThread.java:43)
Process finished with exit code 1

● 如何解决逸出
a. 返回副本

// 上方代码加上这个方法就OK
    public Map<String, String> getStatesCopy() {
        return new HashMap<>(this.states);
    }

a. 工厂模式修复上面监听器

/**
 * 监听器模式 利用工厂模式 来修复一下
 */
public class ReleaseEffusionListenerFix {
    public static void main(String[] args) {
        Source source = new Source();
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            source.eventCome(new Event() {
            });
        }).start();
        ReleaseEffusionListenerFix.getInstance(source);
    }
    private int count;
    private EventListener listener;
    public static ReleaseEffusionListenerFix getInstance(Source source) {
        ReleaseEffusionListenerFix releaseEffusionListenerFix = new ReleaseEffusionListenerFix(source);
        source.registerListener(releaseEffusionListenerFix.listener);
        return releaseEffusionListenerFix;
    }
    private ReleaseEffusionListenerFix(Source source) {
        listener = event -> System.out.println("\n我得到数字:" + count);
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }
    private static class Source {
        private EventListener listener;
        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }
        void eventCome(Event e) {
            if (null != listener) {
                listener.onEvent(e);
            } else {
                System.out.println("未初始化完毕");
            }
        }
    }
    private interface EventListener {
        void onEvent(Event e);
    }
    interface Event { }
}

2.3 需要考虑线程安全问题的一些情况

● 访问共享的变量或者资源,如:属性、静态变量、缓存、数据库等
● 需要顺序操作的,就算每步都是线程安全的,也可能会存在安全问题。如:先读取再修改,先检查再执行
● 不同数据间存在绑定关系,如:ip和端口号
● 使用其他人或者第三方提供的类的时候。核查对方是否声明线程安全。如:HashMap和ConcurrentHashMap。

3. 多线程的性能问题

3.1 性能问题的体现

● 最明显的体验就是慢!比如前端调用一个借口,很久才返回结果或者直接超时。

3.2 造成性能问题的原因

  1. 线程调度:上下文切换
    ○ 何为上下文?
    ■ 就是上下切换需要保存的线程状态或者说数据(比如:线程执行到了那里,各个 参与运算的寄存器是什么内容),以确保恢复线程的执行。
    ○ 缓存开销
    ■ 当一个线程在CPU运算时,有些是需要把数据放到CPU缓存中的,如果上下文切换,那么当前线程CPU的缓存就会失效了。那就CPU就需要重新对新的线程数据进行缓存。所以CPU在启动新线程的时候开始的时候回比较慢,这就是因为CPU之前的缓存大部分都失效了。
    ○ 怎么样会导致频繁的上下文切换?
    ■ 多个线程进行竞争锁,还有就是IO读写
  2. 多个线程协作:内存同步
    ○ 我们的程序运行,编译器和CPU都会对程序进行优化,如指令重排序以更大得利用缓存,但是如果多线程写作的时候,我们就会利用一些手段禁止指令重排序以确保线程安全。还有就是当我们多个线程运行时,JMM中表明,线程会有私有内存区域,如果我们多线程要确保最新数据就会去主存中同步最新数据,这也会带来性能开销。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值