并发和多线程初识

并发和多线程

术语和概念
竞态条件

两条指令的执行具有逻辑关系或者依赖性,多线程执行时会产生故障,这是操作系统层面的定义,一般说的是线程间通信的所产生的故障
现在有少数人可能有偏解,会把业务的非预期执行行为和结果也归纳为这个,其实应该不包含业务上的执行情况

为什么会产生竞态条件的问题

因为无论操作系统层面,还是java虚拟机层面,锁实现的基本原理是通过锁定某个对象或者说内存来实现的,并不是通过操作系统精确定位某个到线程然后暂停它或者恢复它

我们最常见的错误:java.lang.IllegalMonitorStateException: current thread is not owner,这使java为了防止此种竞态条件发生的强制检测

先写几个工具类备用

    private static void wait(Object lock) {
        try {
            synchronized (lock) {
                lock.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void notify(Object lock) {
        synchronized (lock) {
            lock.notify();
        }
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void join(Thread ... ts) {
        for(Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    private static void start(Thread ... ts) {
        for(Thread t : ts) {
            t.start();
        }
    }

设定一个常见面试题:三个线程abc,同时开始,但是a先结束,b再结束,c最后。

预想如下的执行过程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一版代码,尝试人为控制不发生竞态条件,但是java会检测出来

    /**
     * 不加以控制
     */
    public static void testNoControl() {
        Object bLock = new Object();
        Object cLock = new Object();

        Thread a = new Thread(() -> {
            System.out.println("a开始");
            System.out.println("a结束");
            synchronized (bLock) {
                bLock.notify();
            }
        });

        Thread b = new Thread(() -> {
            System.out.println("b开始");
            try {
                bLock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("b结束");
            synchronized (cLock) {
                cLock.notify();
            }
        });

        Thread c = new Thread(() -> {
            System.out.println("c开始");
            try {
                cLock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("c结束");
        });

        start(a, b, c);

        join(a, b, c);
    }

    /**
     * 尝试使线程休眠,等待后续线程先获取到锁对象上的等待机会
     */
    public static void testControlBySleep() {
        Object bLock = new Object();
        Object cLock = new Object();

        Thread a = new Thread(() -> {
            System.out.println("a开始");
            sleep(2000);
            System.out.println("a结束");
            bLock.notify();
        });

        Thread b = new Thread(() -> {
            System.out.println("b开始");
            wait(bLock);
            System.out.println("b结束");
            sleep(1000);
            cLock.notify();
        });

        Thread c = new Thread(() -> {
            System.out.println("c开始");
            wait(cLock);
            System.out.println("c结束");
        });

        start(a, b, c);

        join(a, b, c);
    }

结果肯定不行,java.lang.IllegalMonitorStateException: current thread is not owner

第二版代码,在同步代码块中wait和notify

/**
     * 尝试在同步代码块中让线程等待和唤醒
     */
    public static void testControlBySynchronized() {
        Object bLock = new Object();
        Object cLock = new Object();

        Thread a = new Thread(() -> {
            System.out.println("a开始");
            sleep(2000);
            System.out.println("a结束");
            notify(bLock);
        });

        Thread b = new Thread(() -> {
            System.out.println("b开始");
            wait(bLock);
            System.out.println("b结束");
            sleep(1000);
            notify(cLock);
        });

        Thread c = new Thread(() -> {
            System.out.println("c开始");
            wait(cLock);
            System.out.println("c结束");
        });

        start(a, b, c);

        join(a, b, c);
    }

这一段就可以正确执行

那么即使使用了同步代码块,如果不控制唤醒和等待时机,是否会产生死等

会的,因为notify先调用就会导致wait永远没有机会唤醒,把上述sleep注释掉,很大几率就会死等

那么最常用的正确姿势是什么样的

简单方式:外部标志或者共享变量;

    /**
     * 使用共享变量控制
     */
    public static void testByShareVariable() {
        StringBuffer executed = new StringBuffer();

        Thread a = new Thread(() -> {
            System.out.println("a开始");
            while(!executed.isEmpty()) {
            }
            executed.append("a");
            System.out.println("a结束");
            notify(executed);
        });

        Thread b = new Thread(() -> {
            System.out.println("b开始");
            while(executed.indexOf("a") < 0) {
                wait(executed);
            }
            executed.deleteCharAt(0);
            executed.append("b");
            System.out.println("b结束");
            notify(executed);
        });

        Thread c = new Thread(() -> {
            System.out.println("c开始");
            while(executed.indexOf("b") < 0) {
                wait(executed);
            }
            executed.deleteCharAt(0);
            System.out.println("c结束");
        });

        start(a, b, c);

        join(a, b, c);
    }

因为abc需要顺序结束,其实我们只使用一个共享变量就可以完成这个操作,但是写起来实际上还是费点脑筋的,为了降低出错的几率,java也给我们提供了一些列工具类,只需要理解api的意思就可以完成上述操作,比如countdowanlatch

    /**
     * 利用CountdownLatch 计数器,这中方式和自定义外部共享变量的方式是一样的意思
     */
    public static void testCountdownLatch() {
        CountDownLatch a = new CountDownLatch(1);
        CountDownLatch b = new CountDownLatch(1);
        new Thread(() -> {
            System.out.println("我是a");
            a.countDown();
        }).start();
        new Thread(() -> {
            try {
                a.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是b");
            b.countDown();
        }).start();
        new Thread(() -> {
            try {
                b.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是c");
        }).start();
    }
临界区

应当需要我们去关注的多线程并发问题的那部分代码,范围越小越好,实际表现就是同步代码块的区域

临界区

应当需要我们去关注的多线程并发问题的那部分代码,范围越小越好,实际表现就是同步代码块的区域

阻塞队列

当需要考虑并发问题时,对集合或者共享变量的一切操作任务,都考优先虑使用阻塞队列,极简单任务考虑使用标志或者状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值