13.多线程详解

01、多线程概述

02、线程、进程、多线程

普通方法调用和多线程调用

image-20210309141158636

进程与线程(Process与Thread)

  • 说起进程,就不得不说一下程序,程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
  • 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。
  • 通常在一个进程中可以包含若干了线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。

注意:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

核心概念

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程。如主线程,gc线程
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如CPU调度,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

创建线程的三种方式

  • Thread class – 继承Thread类
  • Runnable接口 – 实现Runnable接口
  • Callable接口 – 实现Callable接口

03、继承Thread类

  • 自定义线程类继承Thread
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()开启线程
public class MyThreadDemo extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("我正在看代码"+i);
        }
    }

    public static void main(String[] args) {
        //main线程 主线程
        MyThreadDemo t = new MyThreadDemo();
        //还是单线程
//        t.run();
        //这才是多线程
        t.start();
        for (int i = 0; i < 300; i++) {
            System.out.println("我正在学习"+i);

        }
    }
}

04、网图下载

实现多线程同步下载图片

public class ThreadDemo2 extends Thread {
    private String url;
    private String pathName;

    public ThreadDemo2(String url, String pathName) {
        this.url = url;
        this.pathName = pathName;
    }

    @Override
    public void run() {
        WebPicDownLoad downLoad = new WebPicDownLoad();
        downLoad.down(this.url, this.pathName);
        System.out.println(this.pathName + "下载成功");
    }

    public static void main(String[] args) {
        //主线程
        ThreadDemo2 t1 = new ThreadDemo2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F611a945f6832f7d0410f5a737768e433a5606082.jpg&refer=http%3A%2F%2Fi0.hdslb.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617883623&t=38f531199f07123b9e645ed4e90c8d41", "D:\\嘿嘿1.jpeg");
        ThreadDemo2 t2 = new ThreadDemo2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbpic.588ku.com%2Foriginal_origin_min_pic%2F18%2F06%2F25%2Fbd7e8eca3b8daad6d23ce0d928c25641.jpg%21r650&refer=http%3A%2F%2Fbpic.588ku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617883779&t=654aa8fe369a454d580cba38f1d014b0", "D:\\嘿嘿2.jpeg");
        ThreadDemo2 t3 = new ThreadDemo2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdnimg103.lizhi.fm%2Fradio_cover%2F2016%2F08%2F21%2F2552377814997298180_320x320.jpg&refer=http%3A%2F%2Fcdnimg103.lizhi.fm&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617883814&t=5a17f62f29b51555beba29a4077901b8", "D:\\嘿嘿3.jpeg");
        t1.start();
        t2.start();
        t3.start();
    }

}

class WebPicDownLoad {
    public void down(String url, String pathName) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(pathName));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

05、实现Runnable接口

  • 定义MyRunnable实现Runnable接口
  • 实现run方法,编写方法体
  • 创建线程对象,调用start()方法启动线程
public class ThreadDemo3 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("我正在看代码"+i);
        }
    }

    public static void main(String[] args) {
        ThreadDemo3 demo3 = new ThreadDemo3();
        new Thread(demo3).start();
        //简写
        /*new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                System.out.println("我正在看代码"+i);
            }
        }).start();*/
        for (int i = 0; i < 300; i++) {
            System.out.println("我正在学习"+i);
        }
    }
}

小结

  • 继承Thread
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

06、初始并发问题

多个线程操作同一个资源的情况下,线程不安全

public class ThreadDemo4 implements Runnable {
    //10张票
    private int ticketNums = 10;
    @Override
    public void run() {
        while (true) {
            if (ticketNums <= 0) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第"+(ticketNums--)+"张票");
            //休眠200毫秒
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo4 demo4 = new ThreadDemo4();
        new Thread(demo4,"小明").start();
        new Thread(demo4,"小红").start();
        new Thread(demo4,"黄牛").start();
    }
}

输出(可以看到并发的问题)

小明拿到了第10张票
小红拿到了第9张票
黄牛拿到了第8张票
黄牛拿到了第7张票
小明拿到了第7张票
小红拿到了第6张票
小红拿到了第5张票
黄牛拿到了第4张票
小明拿到了第3张票
黄牛拿到了第2张票
小明拿到了第1张票

07、龟兔赛跑

public class Race implements Runnable {

    /**
     * 胜利者
     */
    private static String winner;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            boolean flag = gameOver(i) ;
            if (flag) {
                break;
            }
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
            //兔子每10步休息200毫秒
            if (Thread.currentThread().getName().equals("兔子") && (i % 10 == 0)) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 判断比赛是否结束 steps >= 100
     * @param steps
     * @return
     */
    private boolean gameOver(int steps){
        if (winner != null) {
            return true;
        }
        if (steps >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("胜利者是:"+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race, "兔子").start();
        new Thread(race, "乌龟").start();

    }
}

08、实现Callable接口

- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1)
- 提交执行Future<Boolean> result1 = ser.submit(t1);
- 获取结果:boolean r1 = resut1.get()
- 关闭服务:ser.shutdownNow()
public class CallableDemo implements Callable<Boolean> {
    private String url;
    private String pathName;

    public CallableDemo(String url, String pathName) {
        this.url = url;
        this.pathName = pathName;
    }

    @Override
    public Boolean call() {
        WebPicDownLoad downLoad = new WebPicDownLoad();
        downLoad.down(this.url, this.pathName);
        System.out.println(this.pathName + "下载成功");
        return true;
    }

    public static void main(String[] args) {
        //创建目标对象
        CallableDemo t1 = new CallableDemo("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F611a945f6832f7d0410f5a737768e433a5606082.jpg&refer=http%3A%2F%2Fi0.hdslb.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617883623&t=38f531199f07123b9e645ed4e90c8d41", "D:\\嘿嘿1.jpeg");
        CallableDemo t2 = new CallableDemo("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbpic.588ku.com%2Foriginal_origin_min_pic%2F18%2F06%2F25%2Fbd7e8eca3b8daad6d23ce0d928c25641.jpg%21r650&refer=http%3A%2F%2Fbpic.588ku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617883779&t=654aa8fe369a454d580cba38f1d014b0", "D:\\嘿嘿2.jpeg");
        CallableDemo t3 = new CallableDemo("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdnimg103.lizhi.fm%2Fradio_cover%2F2016%2F08%2F21%2F2552377814997298180_320x320.jpg&refer=http%3A%2F%2Fcdnimg103.lizhi.fm&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617883814&t=5a17f62f29b51555beba29a4077901b8", "D:\\嘿嘿3.jpeg");

        FutureTask<Boolean> f1 = new FutureTask<>(t1);
        new Thread(f1).start();
        FutureTask<Boolean> f2 = new FutureTask<>(t2);
        new Thread(f2).start();
        FutureTask<Boolean> f3 = new FutureTask<>(t3);
        new Thread(f3).start();
        try {
            boolean r1 = f1.get();
            boolean r2 = f2.get();
            boolean r3 = f3.get();
            System.out.println(r1);
            System.out.println(r2);
            System.out.println(r3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        /*其他方式
        //- 创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(3);
        //- 提交执行
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        //- 获取结果:
        try {
            boolean r1 = result1.get();
            boolean r2 = result2.get();
            boolean r3 = result3.get();
            System.out.println(r1);
            System.out.println(r2);
            System.out.println(r3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //- 关闭服务:
        ser.shutdownNow();*/
    }
}

class WebPicDownLoad {
    public void down(String url, String pathName) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(pathName));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

09、静态代理模式

/**
 * @ClassName: StaticProxyDemo
 * @Description: 静态代理
 * 真实对象和代理对象都要实现同一个接口
 * 代理对象要代理真实对象
 *   好处:
 *     代理对象可以做很多真实对象做不了的事情
 *     真实对象专注做自己的事情
 * @Author: zyy
 * @Date: 2021/03/10 10:50
 * @Version: 1.0
 */
public class StaticProxyDemo {
    public static void main(String[] args) {
//        WeddingCompany weddingCompany = new WeddingCompany(new You());
//        weddingCompany.happyMarry();
        //简化一下
        new WeddingCompany(new You()).happyMarry();
        //对比一下
        new Thread(() -> System.out.println("开心")).start();
    }
}

class You implements Marry {
    @Override
    public void happyMarry() {
        System.out.println("结婚啦!");
    }
}

class WeddingCompany implements Marry {
    private Marry person;

    WeddingCompany(Marry person) {
        this.person = person;
    }

    @Override
    public void happyMarry() {
        before();
        this.person.happyMarry();
        after();
    }

    private void after() {
        System.out.println("结婚后付尾款");
    }

    private void before() {
        System.out.println("结婚前场地布置");
    }
}

interface Marry {
    void happyMarry();
}

10、Lambda表达式

  • 希腊字母表排序第十一位的字母,英语名称为Lambda
  • 避免匿名内部类定义过多
  • 其实质属于函数式编程的概念

为什么要使用lambda表达式

  • 避免匿名内部类定义过多
  • 可以让你的代码看起来很简洁
  • 去掉了一堆没有意义的代码,只留下了核心的逻辑

也许你会说,我看了lambda表达式,不但不觉得简洁,反而觉得更乱,看不懂了。那是因为我们还没有习惯,用的多了,看习惯了,就好了。

理解Functional Interface(函数式接口)是学习java8 lambda表达式的关键所在。

函数式接口的定义

  • 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
  • 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象
public class LambdaDemo {
    /**
     * 静态内部类
     */
    static class Like2 implements ILike {
        @Override
        public void what() {
            System.out.println("学习lambda[2]");
        }
    }
    public static void main(String[] args) {
        ILike iLike = new Like();
        iLike.what();

        ILike iLike2 = new Like2();
        iLike2.what();

        /**
         * 局部内部类
         */
        class Like3 implements ILike {
            @Override
            public void what() {
                System.out.println("学习lambda[3]");
            }
        }
        ILike iLike3 = new Like3();
        iLike3.what();

        /**
         * 匿名内部类
         */
        ILike iLike4 = new ILike() {
            @Override
            public void what() {
                System.out.println("学习lambda[4]");
            }
        };
        iLike4.what();
        //使用lambda简化
        ILike iLike5 = () -> {
            System.out.println("学习lambda[5]");
        };
        iLike5.what();

    }

}

/**
 * 定义一个函数式接口,只包含一个抽象方法
 */
interface ILike {
    void what();
}

class Like implements ILike {

    @Override
    public void what() {
        System.out.println("学习lambda");
    }
}
public class LambdaDemo2 {
    public static void main(String[] args) {
        //匿名内部类
        /*ILove iLove = new ILove() {
            @Override
            public void love(String name) {
                System.out.println("I Love "+name);
            }
        };*/
        //lambda简化
        /*ILove iLove = (String name) -> {
            System.out.println("I Love "+name);
        };*/
        //再简化:去掉参数类型
        /*ILove iLove = (name) -> {
            System.out.println("I Love "+name);
        };*/
        //再简化:去掉花括号{}
        ILove iLove = name -> System.out.println("I Love " + name);
        iLove.love("wife");
    }
}

interface ILove {
    void love(String name);
}

总结

  • lambda表达式只有一行代码的情况下才可以简化成一行,如果有多行,那么就用代码块包裹
  • 前提是接口为函数式接口
  • 多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号(单个参数可以省略括号)

11、线程停止

线程状态

image-20210310164100500

image-20210310164216563

线程方法

方法说明
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠
void join()等待该线程终止
static void yield()暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程,别用这个方式
boolean isAlive()测试线程是否处于活动状态

停止线程

  • 不推荐使用JDK提供的stop()、destory()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行
public class ThreadStopDemo implements Runnable {
    private boolean flag = true;
    @Override
    public void run() {
        int i = 1;
        while (flag) {
            System.out.println("run..."+(i++));
        }
    }

    public void stop(){
        flag = false;
    }

    public static void main(String[] args) {
        ThreadStopDemo demo = new ThreadStopDemo();
        new Thread(demo).start();
        for (int i = 1; i <= 500; i++) {
            System.out.println("main..."+i);
            if (i == 300) {
                demo.stop();
                System.out.println("线程该停止了");
            }
        }
    }
}

12、线程休眠_sleep

  • sleep(long millis) 指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁

模拟倒计时+打印当前系统时间

public static void main(String[] args) {
    //模拟倒计时
    System.out.println("开始倒计时");
    int num = 10;
    while (true) {
        System.out.println(num--);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (num <= 0) {
            break;
        }
    }
    System.out.println("开始报时");
    //打印当前系统时间
    int count = 10;
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    while (true) {
        System.out.println(LocalDateTime.now().format(dateTimeFormatter));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count--;
        if (count <= 0) {
            break;
        }
    }
}

13、线程礼让_yield

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cup重新调度,礼让不一定成功,看cup调度
public class ThreadYieldDemo implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始!");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"结束!");
    }

    public static void main(String[] args) {
        ThreadYieldDemo demo = new ThreadYieldDemo();
        new Thread(demo, "a").start();
        new Thread(demo, "b").start();
    }
}

输出(不是固定的)

a开始!
b开始!
a结束!
b结束!

14、线程强制执行_join

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成插队
public class ThreadJoinDemo implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println("线程VIP-"+i);
        }
    }

    public static void main(String[] args) {
        ThreadJoinDemo demo = new ThreadJoinDemo();
        Thread thread = new Thread(demo);
        thread.start();
        //主线程
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程-"+i);
            if (i==50){
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

15、观测线程状态

Thread.State

线程状态。线程可以处于以下状态之一

  • new

尚未启动的线程处于状态

  • runnable

在java虚拟机中执行的线程处于此状态

  • blocked

被阻塞 等待监视器锁定的线程处于此状态

  • waitiing

正在等待另一个线程执行特定动作的线程处于此状态

  • timed_waiting

正在等待另一个线程执行动作达到指定等待时间的线程处于此状态

  • terminated

已退出的线程处于此状态

一个线程可以在给定时间点处于一个状态。这些状态是不返回任何操作系统线程状态的虚拟机状态。

public class ThreadStateDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束");
        });

        System.out.println("线程start之前的状态:"+thread.getState());
        thread.start();
        System.out.println("线程start之后的状态:"+thread.getState());
        while (!Thread.State.TERMINATED.equals(thread.getState())) {
            System.out.println("线程terminated之前的状态:"+thread.getState());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("线程的状态:"+thread.getState());
    }
}

输出

线程start之前的状态:NEW
线程start之后的状态:RUNNABLE
线程terminated之前的状态:RUNNABLE
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:RUNNABLE
线程结束
线程的状态:TERMINATED

16、线程的优先级

  • java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程
  • 线程的优先级用数字表示,范围从1~10
    • Thread.MIN_PRIORITY =1
    • Thread.MAX_PRIORITY =10
    • Thread.NORM_PRIORITY =5
  • 使用以下方式改变或获取优先级
    • getPriority() setPriority(int x)

优先级的设置建议在start()调度前

优先级低只是意味着获取调度的概率低,并不是优先级低就不会被调用了,这都是看cup的调度

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority, "t1");
        Thread t2 = new Thread(myPriority, "t2");
        Thread t3 = new Thread(myPriority, "t3");
        Thread t4 = new Thread(myPriority, "t4");
        Thread t5 = new Thread(myPriority, "t5");
        Thread t6 = new Thread(myPriority, "t6");

        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(7);
        t3.setPriority(Thread.MAX_PRIORITY);
        t4.setPriority(4);
        t5.setPriority(9);
        t6.setPriority(2);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();

    }
}

class MyPriority implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
    }
}

17、守护线程

  • 线程分为用户线程和守护线程(daemon)
  • 虚拟机需要确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志,监控内存,垃圾回收等
public class ThreadDaemonDemo {
    public static void main(String[] args) {
        God god = new God();
        You2 you2 = new You2();
        Thread thread = new Thread(god);
        thread.setDaemon(true);
        thread.start();

        Thread thread1 = new Thread(you2);
        thread1.start();

    }
}


class God implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println("上帝守护着你");
        }
    }
}

class You2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            System.out.println("你开心过的一个月");
        }
        System.out.println("======一个月完美的结束了======");
    }
}

18、线程同步机制

多个线程操作同一个资源

并发:同一个对象被多个线程同时操作

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程时候完毕,下一个线程再使用。

线程同步

  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

19、三大不安全案例

1. 不安全的买票

public class UnSafeDemo1 implements Runnable {
    private int ticketNum = 10;
    private boolean flag = true;

    public static void main(String[] args) {
        UnSafeDemo1 demo1 = new UnSafeDemo1();
        new Thread(demo1,"小红").start();
        new Thread(demo1,"小明").start();
        new Thread(demo1,"黄牛").start();
    }

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    private void buy(){
        if (ticketNum <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName()+"拿到"+(ticketNum--)+"张票");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

2. 不安全的取钱

public class UnSafeDemo2 {
    public static void main(String[] args) {
        Account account = new Account(100, "张三");
        DrawMoney drawMoney = new DrawMoney(account, 70, "张三");
        DrawMoney drawMoney1 = new DrawMoney(account, 70, "张三的老婆");

        drawMoney.start();
        drawMoney1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("卡内余额:" + account.balance);
    }

}

class Account {
    int balance;
    String accName;

    public Account(int balance, String accName) {
        this.accName = accName;
        this.balance = balance;
    }
}

class DrawMoney extends Thread {
    private Account account;
    private int drawingMoney;

    DrawMoney(Account account, int drawingMoney, String who) {
        super(who);
        this.account = account;
        this.drawingMoney = drawingMoney;

    }

    @Override
    public void run() {
        drawing();
    }

    private void drawing() {
        if (this.account.balance - drawingMoney < 0) {
            System.out.println("余额不足," + Thread.currentThread().getName() + "取钱失败");
            return;
        }
        //sleep可以提高问题的发生的概率
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.account.balance = this.account.balance - drawingMoney;
        System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney);


    }
}

正常情况返回

张三取了70
余额不足,张三的老婆取钱失败
卡内余额:30

并发情况返回(异常)

张三的老婆取了70
张三取了70
卡内余额:-40

3. 线程不安全的集合

public class UnSafeDemo3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            final int finalI = i;
            new Thread(() -> {
                list.add(finalI);
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

输出(理想输出应该是10000)

9997

20、同步方法及同步块

1. 同步方法

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块(同步方法: public synchronized void method(int args){} )
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。(缺陷:若将一个大的方法申明为synchronized将会影响效率)

2. 同步块

  • 同步块:synchronized(obj) {}
  • 一般锁变化的对象,需要增删改的对象
  • ojb称之为同步监视器
    • obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射的时候讲解]
  • 同步监视器的执行过程
    • 第一个线程访问,锁定同步监视器,执行其中的代码
    • 第二个线程访问,发现同步监视器被锁定,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

3.改造不安全的买票

public class UnSafeDemo1 implements Runnable {
    private int ticketNum = 10;
    private boolean flag = true;

    public static void main(String[] args) {
        UnSafeDemo1 demo1 = new UnSafeDemo1();
        new Thread(demo1,"小红").start();
        new Thread(demo1,"小明").start();
        new Thread(demo1,"黄牛").start();
    }

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    private synchronized void buy(){
        if (ticketNum <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName()+"拿到"+(ticketNum--)+"张票");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

4.改造不安全的取钱

public class UnSafeDemo2 {
    public static void main(String[] args) {
        Account account = new Account(100, "张三");
        DrawMoney drawMoney = new DrawMoney(account, 70, "张三");
        DrawMoney drawMoney1 = new DrawMoney(account, 70, "张三的老婆");

        drawMoney.start();
        drawMoney1.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("卡内余额:" + account.balance);
    }

}

class Account {
    int balance;
    String accName;

    public Account(int balance, String accName) {
        this.accName = accName;
        this.balance = balance;
    }
}

class DrawMoney extends Thread {
    private Account account;
    private int drawingMoney;

    DrawMoney(Account account, int drawingMoney, String who) {
        super(who);
        this.account = account;
        this.drawingMoney = drawingMoney;

    }

    @Override
    public void run() {
        drawing();
    }

    private void drawing() {
        //synchronized (this) 这样是没用的,需要锁的是变化的量,锁账户
        synchronized (account) {
            if (this.account.balance - drawingMoney < 0) {
                System.out.println("余额不足," + Thread.currentThread().getName() + "取钱失败");
                return;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.account.balance = this.account.balance - drawingMoney;
            System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney);
        }
    }
}

5.改造不安全的集合

public class UnSafeDemo3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            final int finalI = i;
            new Thread(() -> {
                synchronized (list) {
                    list.add(finalI);
                }
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

21、CopyOnWriteArrayList

public static void main(String[] args) {
    List<Integer> list = new CopyOnWriteArrayList<>();
    for (int i = 0; i < 10000; i++) {
        final Integer fInt = Integer.valueOf(i);
        new Thread(() -> list.add(fInt)).start();
    }
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //输出 10000
    System.out.println(list.size());
}

22、死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。(两个或者多个线程互相抱着对方需要的资源,然后形成僵持)

死锁

public class DeadLockDemo {
    public static void main(String[] args) {
        MakeUp makeUp1 = new MakeUp(0, "小红");
        MakeUp makeUp2 = new MakeUp(1, "小青");
        makeUp1.start();
        makeUp2.start();
    }
}

/**
 * 口红
 */
class Lipstick {

}

/**
 * 镜子
 */
class Mirror {

}

class MakeUp extends Thread {
    /**
     * 只有一个镜子和一只口红
     */
    private static final Lipstick lipstick = new Lipstick();
    private static final Mirror mirror = new Mirror();

    private int choice;
    private String girlName;

    public MakeUp(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }


    @Override
    public void run () {
        makeUp();
    }

    private void makeUp() {
        if (choice == 0) {
            //第一个女生先获取口红的锁,一秒钟后再获取镜子的锁
            synchronized (lipstick) {
                System.out.println(this.girlName+"获取到口红的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror) {
                    System.out.println(this.girlName+"获取到镜子的锁");
                }
            }
        } else {
            //其他女生先获取镜子的锁,两秒钟后再获取口红的锁
            synchronized (mirror) {
                System.out.println(this.girlName+"获取到镜子的锁");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick) {
                    System.out.println(this.girlName+"获取到口红的锁");
                }
            }
        }
    }
}

解开死锁

public class DeadLockDemo {
    public static void main(String[] args) {
        MakeUp makeUp1 = new MakeUp(0, "小红");
        MakeUp makeUp2 = new MakeUp(1, "小青");
        makeUp1.start();
        makeUp2.start();
    }
}

/**
 * 口红
 */
class Lipstick {

}

/**
 * 镜子
 */
class Mirror {

}

class MakeUp extends Thread {
    /**
     * 只有一个镜子和一只口红
     */
    private static final Lipstick lipstick = new Lipstick();
    private static final Mirror mirror = new Mirror();

    private int choice;
    private String girlName;

    public MakeUp(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }


    @Override
    public void run () {
        makeUp();
    }

    private void makeUp() {
        if (choice == 0) {
            //第一个女生先获取口红的锁,一秒钟后释放口红的锁,再获取镜子的锁
            synchronized (lipstick) {
                System.out.println(this.girlName+"获取到口红的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (mirror) {
                System.out.println(this.girlName+"获取到镜子的锁");
            }
        } else {
            //其他女生先获取镜子的锁,两秒钟后释放镜子的锁,再获取口红的锁
            synchronized (mirror) {
                System.out.println(this.girlName+"获取到镜子的锁");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lipstick) {
                System.out.println(this.girlName+"获取到口红的锁");
            }
        }
    }
}

死锁避免的方法

  • 产生死锁的四个必要条件

    • 互斥条件:一个资源每次只能被一个进程使用(你想要的我的资源,我想要你的资源)
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(你请求我的资源,我请求你的资源,我抱着我的资源不放,你抱着你的资源不放)
    • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。(资源没用用完之前,不能强行抢)
    • 循环等待条件:若干进程之前形成一种头尾相接的循环等待资源关系。(你等着我释放资源,我等着你释放资源)

    上面列出了死锁的四个必要条件,我们只要想办法破坏其中的任意一个或者多个条件就可以避免死锁。

23、Lock锁

  • 从jdk5.0开始,java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象
  • ReetrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReetrantLock,可以显示加锁、释放锁
public class LockDemo {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        Thread t1 = new Thread(buyTicket, "小明");
        Thread t2 = new Thread(buyTicket, "小红");
        Thread t3 = new Thread(buyTicket, "黄牛");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicket implements Runnable {
    /**
     * 余票
     */
    private int ticketsNum = 10;
    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        buy();
    }

    private void buy() {
        while (true) {
            reentrantLock.lock();
            try {
                if (ticketsNum <= 0) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "拿到了第" + (ticketsNum--) + "张票");
            } finally {
                reentrantLock.unlock();
            }

        }

    }
}

synchronized与Lock的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,除了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了响应资源)> 同步方法(在方法体之外)

24、生产者消费者问题

1.线程通信

  • 应用场景:生产者和消费者问题
    • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费
    • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
    • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

image-20210312152358384

2.线程通讯-分析

  • 这个一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

    • 对于生产者,没有生产产品之前,要通知消费着等待,而生产了产品之后,有需要马上通知消费者消费
    • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
    • 在生产者消费者问题中,仅有synchronized是不够的
      • synchronized可以阻止并发更新同一个共享资源,实现了同步
      • synchronized不能用来实现不同线程之前的消息传递(通信)
  • java提供几个方法解决线程之前的通信问题

方法名作用
wait()表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

3.解决方式1

并发协作模型”生产者/消费者模式“–>管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

4.解决方式2

并发协作模型”生产者/消费者模式“–>信号灯法

25、管程法

public class PCDemo {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Cousumer(container).start();
    }
}

/**
 * 生产者
 */
class Productor extends Thread {
    /**
     * 缓冲区
     */
    private SynContainer container;

    public Productor(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了" + i + "只鸡");
        }
    }
}

/**
 * 消费者
 */
class Cousumer extends Thread {
    /**
     * 缓冲区
     */
    private SynContainer container;

    public Cousumer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            Chicken pop = container.pop();
            System.out.println("消费了" + pop.getId() + "只鸡");
        }
    }

}

/**
 * 鸡(食物)
 */
class Chicken {
    /**
     * 鸡的编号
     */
    private int id;

    public Chicken(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

/**
 * 缓冲区
 */
class SynContainer {
    /**
     * 缓冲区的容器大小(十只鸡)
     */
    private Chicken[] chickens = new Chicken[10];
    /**
     * 计数器
     */
    private int count = 0;
    /**
     * 生产者往容器中放入产品
     */
    public synchronized void push(Chicken chicken) {
        //如果容器满了,生产者就需要等待消费者消费
        if (count == 10) {
            //生产者开始等待消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没有满,生产者往容器中继续放入产品
        chickens[count] = chicken;
        count++;
        //生产者通知消费者消费
        this.notifyAll();
    }

    /**
     * 消费者消费容器中的产品
     */
    public synchronized Chicken pop() {
        //消费者判断容器中是否有产品
        if (count == 0) {
            //消费者等待生产者生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消费者开始消费容器中的产品
        count--;
        Chicken chicken = chickens[count];
        //消费者通知生产者继续生产
        this.notifyAll();
        return chicken;
    }
}

26、信号灯法

public class PCDemo2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Actor(tv).start();
        new Audience(tv).start();
    }
}

/**
 * 生产者--演员
 */
class Actor extends Thread {
    private TV tv;

    public Actor(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            if (i % 2 == 0) {
                tv.perform("快乐大本营");
            } else {
                tv.perform("抖音");
            }
        }
    }
}

/**
 * 消费者--观众
 */
class Audience extends Thread {
    private TV tv;

    public Audience(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            tv.watch();
        }
    }
}


/**
 * TV
 * 演员表演节目,观众等待
 * 观众观看节目,演员等待
 */
class TV {
    /**
     * 节目名称
     */
    private String showName;
    /**
     * 标志位
     */
    private boolean flag = true;

    /**
     * 生产者生产产品 -- 演员表演节目
     *
     * @param showName
     */
    public synchronized void perform(String showName) {
        if (!flag) {
            //演员等待,观众在观看
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //演员开始表演节目
        this.showName = showName;
        System.out.println("演员表演了节目:" + this.showName);
        flag = !flag;
        //演员通知观众去观看
        this.notifyAll();
    }

    /**
     * 消费者消费产品--观众观看节目
     */
    public synchronized void watch() {
        if (flag) {
            //观众等待,演员在表演节目
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //开始观看节目
        System.out.println("观众观看了节目:" + this.showName);
        flag = !flag;
        //观众通知演员表演
        this.notifyAll();
    }
}

27、线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

  • 好处

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间会终止

    使用线程池

    • jdk5.0起提供了线程池相关api:ExecutorService和Executors
    • ExecutorService:真正的线程池接口。常见子类ThreadPoolExcutor
      • void execute(Runnabe command):执行任务/命令,没有返回值,一般用来执行Runnable
      • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
      • void shutdown():关闭连接池
    • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class PoolDemo {
    public static void main(String[] args) {
        //创建服务,创建线程池,线程池大小5(不推荐用下面这种方式创建线程池,避免资源耗尽的风险)
        ExecutorService service = Executors.newFixedThreadPool(5);
        //执行任务
        service.submit(new MyThread());
        service.submit(new MyThread());
        service.submit(new MyThread());
        service.submit(new MyThread());
        service.submit(new MyThread());
        service.submit(new MyThread());
        service.submit(new MyThread());
        //关闭线程池
        service.shutdown();
    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

28、总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值