Day278&279&280.线程双刃剑 -Juc

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day278&279&280.线程双刃剑 -Juc相关的知识,希望对你有一定的参考价值。

线程双刃剑

一、线程安全问题

1、学习前的思考

  • 一共有哪几类线程安全问题?
  • 哪些场景需要额外注意线程安全问题?
  • 什么是多线程带来的上下文切换?

2、什么是线程安全

线程本身是安全的,但是他的使用要求较高,我们的使用不当会造成线程安全问题

定义

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


人话版

不管业务中遇到怎么的多个线程访问某个对象或某个方法的情况而编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是key像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全


什么是线程不安全

多个线程同时做一个操作时,如set一个对象的值,会出现错误,我们需要额外的进行操作保证结果正确,如Synchronized关键词修饰做同步


那为什么不全部设计为线程安全

运行速度、设计成本、trade off


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

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

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadsError);
        Thread thread2 = new Thread(multiThreadsError);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadsError.index);
    }

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

image-20210525221826831

image-20210525221957096

上面的情况,我们知道,线程之间有抢到相同i的情况去++,所以导致结果不符合预期;

那么a++具体在哪里消失?又小时了几个??


public class MultiThreadsError implements Runnable {
    static MultiThreadsError multiThreadsError = new MultiThreadsError();
    final boolean[] marked = new boolean[100000];
    int index = 0;
    static AtomicInteger realInt = new AtomicInteger();//正确次数
    static AtomicInteger errorInt = new AtomicInteger();//错误次数

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadsError);
        Thread thread2 = new Thread(multiThreadsError);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadsError.index);
        System.out.println("正确次数为:"+realInt);
        System.out.println("错误次数为:"+errorInt);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            index++;
            realInt.incrementAndGet();
            synchronized (multiThreadsError){
            if (marked[index]){
                errorInt.incrementAndGet();
                System.out.println("该下标【"+index+"】越界被标记过了");
            }
            marked[index] = true;
            }
        }
    }
}

通过一个boolean数组来标记已经++了的下标为true值,若如果已经为true,那么打印下标为越界线程错误的话

AtomicInteger为细化步骤,就会让这次操作不会出现线程不安全操作,这里用他记录错误/正确次数

image-20210525224533665

为什么这里错误的次数会这么多,且结果没到20000?

因为当线程1执行到if (marked[index])时,他拿到锁进去后给给index=0的位置赋值为true,然后他释放锁;但是他速度特别快直接又执行到index++,也就是还没有等到线程2拿到锁进行下面的流程,就++,让index的值变成了1,那么也就是说线程1,++了两次index,那么线程2拿到index的时候 就已经是1了,那么就不为true,直接走下面,的赋值为true,导致少了几次,就会出现表面结果为19999的情况出现!!!

那么怎么保证这么情况不会发送???


就引入工具类,CyclicBarrier;类似栅栏

public class MultiThreadsError implements Runnable {
    static MultiThreadsError multiThreadsError = new MultiThreadsError();
    final boolean[] marked = new boolean[100000];
    int index = 0;
    static AtomicInteger realInt = new AtomicInteger();//正确次数
    static AtomicInteger errorInt = new AtomicInteger();//错误次数
    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
    //有参构造参数2,代表等待两个线程经过,再放行
    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadsError);
        Thread thread2 = new Thread(multiThreadsError);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadsError.index);
        System.out.println("正确次数为:"+realInt);
        System.out.println("错误次数为:"+errorInt);
    }

    @Override
    public void run() {
        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();
            }
            realInt.incrementAndGet();
            synchronized (multiThreadsError){
            if (marked[index]){
                errorInt.incrementAndGet();
                System.out.println("该下标【"+index+"】越界被标记过了");
            }
            marked[index] = true;
            }
        }
    }
}

尽然还有出现安全问题!!!

image-20210525225838350

观察上面!发现被标记的下标都为偶数???!怎么可能错误这么巧合?

因为synchronized会让线程操作具有可见性,那么当线程1进来1下标时,线程2可见性见到1下标时标记为true,那么就认为1下标已经被标记过,那么就有可能跳过


通过marked[index-1],来感知前面一位是true还是false,来判断是否发送问题标记错误;并将marked[0] = true;让如果在第一个位置发生线程问题,保证标记错误

public class MultiThreadsError implements Runnable {
    static MultiThreadsError multiThreadsError = new MultiThreadsError();
    final boolean[] marked = new boolean[100000];
    int index = 0;
    static AtomicInteger realInt = new AtomicInteger();//正确次数
    static AtomicInteger errorInt = new AtomicInteger();//错误次数
    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
    //有参构造参数2,代表等待两个线程经过,再放行
    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadsError);
        Thread thread2 = new Thread(multiThreadsError);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadsError.index);
        System.out.println("正确次数为:"+realInt);
        System.out.println("错误次数为:"+errorInt);
    }

    @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();
            }
            realInt.incrementAndGet();
            synchronized (multiThreadsError){
            if (marked[index] && marked[index-1]){
                errorInt.incrementAndGet();
                System.out.println("该下标【"+index+"】越界被标记过了");
            }
            marked[index] = true;
            }
        }
    }
}

image-20210525231140258


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

这里以死锁为例子

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;

        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);

        thread1.start();
        thread2.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("1");
                }
            }
        }

    }
}

image-20210526212131490


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

什么是发布

对象范围域,如public、private…等类似概念

什么是逸出

他被发布到不该发布的地方

image-20210526224304209

  1. 方法返回一个private对象(private的本意是不让外部访问)
public class MultiThreadError3 {
    private Map<String,String> states;

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

    //这里逸出了
    public Map<String,String> getStates(){
        return states;
    }

    //导致下面可以获取修改states对象的内容
    public static void main(String[] args) {
        MultiThreadError3 multiThreadError3 = new MultiThreadError3();
        Map<String, String> states = multiThreadError3.getStates();
        System.out.println(states.get("1"));

        states.remove("1");
        System.out.println(states.get("1"));
    }
}

  1. 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界
/******
 @author 阿昌
 @create 2021-05-26 21:57
  *******
  *  初始化未完毕,就this赋值
 */
public class MultiThreadError4 {
    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        Thread.sleep(10);
        if (point!=null){
            System.out.println(point);
        }
    }
}

class Point {
    private final int x, y;

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

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';以上是关于Day278&279&280.线程双刃剑 -Juc的主要内容,如果未能解决你的问题,请参考以下文章

让vim更加智能化

Day280.线程8大基础知识---面试题总结 -Juc

day19-线程之间的通信&线程池&设计模式

day19-线程之间的通信&线程池&设计模式

day19-线程之间的通信&线程池&设计模式

day18-多线程&线程同步&死锁