多线程

线程、进程、多线程

普通方法调用和多线程

image

Process与Thread

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

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

本章核心概念

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

线程创建

三种创建方式

image

继承Thread类

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程

具体可以查看帮助文档

/*
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
总结:注意,线程开启不一定立即执行,由CPU调度执行
 */
public class Demo03 extends Thread{
    //重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 2000; i++) {
            System.out.println("副进程");
        }
    }
    public static void main(String[] args) {
        //创建一个线程对象
        Demo03 thread2 = new Demo03();
        //调用start()方法开启线程
        thread2.start();

        for (int i = 0; i < 2000; i++) {
            System.out.println("主进程");
        }
    }
}
/*
...
副进程
副进程
主进程
副进程
副进程
副进程
副进程
副进程
...
 */
案例:多线程同步下载图片
//继承Thread,实现多线程同步下载图片
public class Demo04 extends Thread{
    private String url; //网络图片地址
    private String name; //保存的文件名

    public Demo04(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);
    }

    public static void main(String[] args) {
        Demo04 t1 = new Demo04("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "1.png");
        Demo04 t2 = new Demo04("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "2.png");
        Demo04 t3 = new Demo04("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "3.png");
        t1.start();
        t2.start();
        t3.start();
    }
}

//下载器
class  WebDownloader{
    //下载方法
    public void downloader(String url, String name) {
        try {
            //用到包中方法
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题");
        }
    }
}
/*
下载了文件名为:2.png
下载了文件名为:1.png
下载了文件名为:3.png
 */

实现Runnable

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程

推荐使用Runnable对象,因为Java单继承的局限性,实际上Tread类中实现的就是Runnable接口,本质上是一样的,区别在于接口和类

public class Demo05 implements Runnable{
    //重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 2000; i++) {
            System.out.println("副进程");
        }
    }
    public static void main(String[] args) {
        //创建runnable接口的实现类对象
        Demo05 thread2 = new Demo05();
        //创建线程对象,调用start()方法开启线程
        new Thread(thread2).start();

        for (int i = 0; i < 2000; i++) {
            System.out.println("主进程");
        }
    }
}

小结

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

买票案例

//多个线程同时操作同一给对象
//买火车票的例子
//发现问题:多个线程操作同一个资源的况下,线程不安全,数据紊乱
public class Demo06 implements Runnable{
    //票数
    private int ticketNums = 10;

    @Override
    public void run() {
        while (true) {
            if (ticketNums <= 0) {
                break;
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- +"张票");
        }
    }

    public static void main(String[] args) {
        Demo06 t = new Demo06();

        new Thread(t,"张三").start();
        new Thread(t,"李四").start();
        new Thread(t,"王五").start();
    }
}
/*
李四拿到了第10张票
张三拿到了第8张票
王五拿到了第9张票
李四拿到了第6张票
王五拿到了第5张票
张三拿到了第7张票
李四拿到了第2张票
王五拿到了第4张票
张三拿到了第3张票
王五拿到了第1张票
张三拿到了第0张票
李四拿到了第-1张票
 */

龟兔赛跑

public class Demo07 implements Runnable{
    private static String winner;
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            //睡觉和走一步用时
            int time = 0;
            if (Thread.currentThread().getName().equals("兔子") && i % 4 == 0) {
                time = 4000;
            } else if (Thread.currentThread().getName().equals("乌龟")){
                time = 500;
            }
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (!isWin(i)) {
                System.out.println(Thread.currentThread().getName() + "跑了" + i + "步了");
            } else {
                break;
            }
        }
    }

    public static boolean isWin(int steps){
        if (winner != null) {
            return true;
        } else {
            if (steps >= 10) {
                winner = Thread.currentThread().getName();
                System.out.println("Winner is " + winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Demo07 thread = new Demo07();

        new Thread(thread, "兔子").start();
        new Thread(thread, "乌龟").start();
    }
}
/*
兔子跑了1步了
兔子跑了2步了
兔子跑了3步了
乌龟跑了1步了
乌龟跑了2步了
乌龟跑了3步了
乌龟跑了4步了
乌龟跑了5步了
乌龟跑了6步了
乌龟跑了7步了
兔子跑了4步了
兔子跑了5步了
兔子跑了6步了
兔子跑了7步了
乌龟跑了8步了
乌龟跑了9步了
Winner is 乌龟
 */

实现Callable接口(了解即可)

  • 实现Callable接口,需要返回值类型
  • 重写cal方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务:ExecutorService ser = Executors .newFixedThreadPool(1);
  • 提交执行:Future result1 = ser.submit(t1);
  • 获取结果:boolean r1=result1.get()
  • 关闭服务:ser.shutdownNow();
//线程创建方式三:实现callable接口
/*
callable的好处
1.可以定义返回催
2.可以抛出异常。
 */
public class Demo08 implements Callable<Boolean> {//返回值类型自己选择
    private String url; //网络图片地址
    private String name; //保存的文件名

    public Demo08(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() {
        WebDownloader2 webDownloader = new WebDownloader2();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Demo08 t1 = new Demo08("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "1.png");
        Demo08 t2 = new Demo08("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "2.png");
        Demo08 t3 = new Demo08("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "3.png");

        //创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(3);//线程池
        //提交执行:
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        //获取结果:
        boolean r1 = result1.get();
        boolean r2 = result2.get();
        boolean r3 = result3.get();
        System.out.println(r1);
        System.out.println(r2);
        System.out.println(r3);
        //关闭服务:
        ser.shutdownNow();
    }
}

//下载器
class  WebDownloader2{
    //下载方法
    public void downloader(String url, String name) {
        try {
            //用到包中方法
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题");
        }
    }
}
/*
下载了文件名为:2.png
下载了文件名为:3.png
下载了文件名为:1.png
true
true
true
 */

静态代理模式

//静态代理模式总结:
//真实对象和代理对象都要实现同一个接口
//代理对象要代理真实角色
//好处:
    //代理对象可以做很多真实对象做不了的事
    //真实对象专注做自己的事
public class Demo09 {
    public static void main(String[] args) {

        //可以用匿名类实现,线程,属于实现Runnable接口方式
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("我爱你");
//            }
//        }).start();
        new Thread(() -> System.out.println("我爱你")).start();

        new WeddingCompany(new You()).HappyMarry();
    }
}
interface Marry{
    void HappyMarry();
}
//真实角色,主人公
class You implements Marry{
    @Override
    public void HappyMarry() {
        System.out.println("我要结婚了,好开心");
    }
}
//代理角色,帮助你结婚
class WeddingCompany implements Marry{
    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();//这就是真是对象
        after();
    }

    private void before() {
        System.out.println("布置婚礼现场");
    }
    private void after() {
        System.out.println("婚后交尾款");
    }
}
/*
我爱你
布置婚礼现场
我要结婚了,好开心
婚后交尾款
 */

Lambda表达式

其他相关视频:原理应用

为什么要使用lambda表达式

  • 避免匿名内部类定义过多

  • 可以让你的代码看起来很简洁

  • 去掉了一堆没有意义的代码,只留下核心的逻辑。

  • 其实质属于函数式编程的概念

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

函数式接口的定义:

  • 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
  • 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。

Runnable接口就是一个函数式接口

总结:

  • lambda表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹
  • 前提是接口为函数式接口
  • 多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号,
public class Demo10 {
    //3.静态内部类
    static class lambdaCl2 implements lambdaIf{
        @Override
        public void like(int extent) {
            System.out.println("I like lambda2 with " + extent);
        }
    }
    public static void main(String[] args) {
        //用传统类实现
        new lambdaCl1().like(1);
        //用静态内部类实现
        new lambdaCl2().like(2);
        //4.局部内部类
        class lambdaCl3 implements lambdaIf{
            @Override
            public void like(int extent) {
                System.out.println("I like lambda3 with " + extent);
            }
        }
        new lambdaCl3().like(3);
        //5.用匿名内部类实现
        new lambdaIf() {
            @Override
            public void like(int extent) {
                System.out.println("I like lambda4 with " + extent);
            }
        }.like(4);
        //6.lambda表达式
        lambdaIf like = null;
        like = (e) -> {System.out.println("I like lambda5 with " + e);};
        like.like(5);
        //7.进一步简化
        like = e -> System.out.println("I like lambda6 with " + e);
        like.like(6);
        /*
        总结:
            lambda表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹。
            前提是接口为函数式接口
            多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号,
         */
    }
}

//1.定义一个函数式接口
interface lambdaIf{
    void like(int extent);
}
//2.实现类
class lambdaCl1 implements lambdaIf{
    @Override
    public void like(int extent) {
        System.out.println("I like lambda1 with " + extent);
    }
}
/*
I like lambda1 with 1
I like lambda2 with 2
I like lambda3 with 3
I like lambda4 with 4
I like lambda5 with 5
I like lambda6 with 6
 */

线程状态

image

线程方法

image

停止线程

  • 不推荐使用JDK提供的 stop()、
    destroy()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量
    当flag=false,则终止线程运行。
//测试stop
//1.建议线程正常停止--->利用次数,不建议死循环
//2.建议使用标志位--->设置一个标志位
//3.不要使用stop或者destroy等过时或者JDK不建议使用的方法
public class Demo11 implements Runnable{
    //1.设置一个标识位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("run.....Thread" + i++);
        }
    }

    //2.设置一个公开的方法停止线程,转换标志位
    public void stop() {
        this.flag = false;
    }
    public static void main(String[] args) {
        Demo11 t = new Demo11();
        new Thread(t).start();

        for (int i = 0; i <= 5; i++) {
            System.out.println("main" + i);
            if (i == 4) {
                //调用stop方法切换标识位,让线程停止
                t.stop();
                System.out.println("线程停止了");
            }
        }
    }
}
/*
main0
run.....Thread0
main1
run.....Thread1
main2
run.....Thread2
main3
run.....Thread3
main4
run.....Thread4
线程停止了
main5
 */

线程休眠

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

应用案例:

  • 模拟网络延时:放大问题的发生性
  • 倒计时
  • 打印当前时间
//倒计时模拟,打印时间
public class Demo12 {
    public static void main(String[] args) {
//        timeDown(1000);
        getTime(10);
    }

    //模拟倒计时
    public static void timeDown(int time) {
        while (0 != time--) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(time);
        }
    }
    //打印当前系统时间
    public static void getTime(int num) {
        Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间
        while (0 != num--) {
            try {
                Thread.sleep(1000);
                startTime = new Date(System.currentTimeMillis());
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看CPU心情
public class Demo13 {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield, "a").start();
        new Thread(myYield, "b").start();
    }
}
class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName() + "线程完成执行");
    }
}

线程强制执行_join

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

    public static void main(String[] args) throws InterruptedException {
        Demo14 th = new Demo14();
        Thread thread = new Thread(th);
        thread.start();
        for (int i = 0; i < 15; i++) {
            if (i == 10) {
                thread.join();
            }
            System.out.println("main..." + i);
        }
    }
}
/*
main...0
main...1
main...2
main...3
main...4
join...0
main...5
main...6
main...7
main...8
main...9
join...1
join...2
join...3
join...4
join...5
join...6
join...7
join...8
join...9
main...10
main...11
main...12
main...13
main...14
 */

线程状态观测

  • 线程状态观测

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

    • NEW
      尚未启动的线程处于此状态。
    • RUNNABLE
      在Java虚拟机中执行的线程处于此状态。
    • BLOCKED
      被阻塞等待监视器锁定的线程处于此状态。
    • WAITING
      正在等待另一个线程执行特定动作的线程处于此状态,
    • TIMED WAITING
      正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
    • TERMINATED
      已退出的线程处于此状态。

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

//观察测试线程的状态
public class Demo15 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("over");
        });

        //观察状况
        Thread.State state = thread.getState();
        System.out.println(state);//NEW

        //观察启动后
        thread.start();
        state = thread.getState();
        System.out.println(state);//Run

        while (state != Thread.State.TERMINATED) {//只要线程不终止,就一直输出状态
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            state = thread.getState();
            System.out.println(state);
        }

        //终止后就不能再次启动了,即线程中断或者结束,一旦进入死亡状态,就不能再次启动,否则报错
//        thread.start();
    }
}
/*
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
over
TERMINATED
 */

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

  • 线程的优先级用数字表示,范围从1~10。

    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY =10;
    • Thread.NORM_PRIORITY = 5;
  • 使用以下方式改变或获取优先级

    • getPriority(),setPriority(int xxx)
  • 优先级的设定建议在start()调度前,先设置优先级在启动

  • 优先级低只是意味着获得调度的概率低并不是优先级低就不会被调用了。这都是看CPU的调度

//测试线程的优先级
public class Demo16 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
        MyPriorty myPriorty = new MyPriorty();
        Thread thread5 = new Thread(myPriorty, "thread5");
        Thread thread1 = new Thread(myPriorty, "thread1");
        Thread thread3 = new Thread(myPriorty, "thread3");
        Thread thread7 = new Thread(myPriorty, "thread7");
        Thread thread10 = new Thread(myPriorty, "thread10");

        thread5.start();

        thread1.setPriority(Thread.MIN_PRIORITY);
        thread1.start();

        thread3.setPriority(3);
        thread3.start();

        thread7.setPriority(7);
        thread7.start();

        thread10.setPriority(Thread.MAX_PRIORITY);
        thread10.start();
    }
}

class MyPriorty implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
    }
}
/*
main-->5
thread5-->5
thread10-->10
thread7-->7
thread3-->3
thread1-->1
 */

守护(daemon)线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕,如main()
  • 虚拟机不用等待守护线程执行完毕,如gc()
  • 如,后台记录操作日志,监控内存,垃圾回收等待,
public class Demo17 {
    public static void main(String[] args) {
        Lucy lucy = new Lucy();
        Me me = new Me();

        Thread thread1 = new Thread(lucy);
        thread1.setDaemon(true);//默认是false表示是用户线程,正常的线程都是用户线程..
        Thread thread2 = new Thread(me);

        thread1.start();//守护线程,虚拟机不用等待其执行完毕。
        thread2.start();//虚拟机要确保用户线程执行完毕
    }
}
//好运和自信
class Lucy implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("和好运自信相伴");
        }
    }
}
//我
class Me implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("一生都很开心的活着");
        }
        System.out.println("=====goodbye! world!=====");
    }
}

线程同步

多个线程操作同一个资源

并发:同一个对象被多个线程在同一个时间间隔内操作

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

形成条件:队列+锁

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

三大不安全案例

  • 不安全买票,具体可查看上面线程创建中的小结案例

  • 不安全的取钱

    //不安全的取钱
    //两个人去银行取钱,账户
    public class UnsafeBank {
        public static void main(String[] args) {
            Account account = new Account(15, "888");
    
            Drawing you = new Drawing(account, 10, "我");
            Drawing girlFriend = new Drawing(account, 10, "girlFriend");
    
            you.start();
            girlFriend.start();
        }
    }
    
    class Account{
        int money;//余额
        String name;//卡名
    
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    //模拟取款
    //《这里选择继承Threa的类是因为只用到了一种线程,》当然可以实例化多个线程
    class Drawing extends Thread{
    
        Account account;//账号
        //取了多少钱
        int drawingMoney;
        //手里还有多少钱
        int nowMoney;
        public Drawing(Account account, int drawingMoney, String name) {
            super(name);
            this.account = account;
            this.drawingMoney = drawingMoney;
        }
        //取钱
    
        @Override
        public void run() {
            if (account.money - drawingMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "余额不足,操作失败");
                return;
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //更新卡内余额
            account.money -= drawingMoney;
            //你手里的钱
            nowMoney += drawingMoney;
    
            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(this.getName() + "手里的钱:" + nowMoney);
    
        }
    }
    /*
    888余额为:-5
    888余额为:-5
    girlFriend手里的钱:10
    我手里的钱:10
     */
    
  • 线程不安全的集合

    //线程不安全的集合
    public class UnsafeList {
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            for (int i = 0; i < 10000; i++) {
                new Thread(() -> {
                   list.add(Thread.currentThread().getName());
                }).start();
            }
            System.out.println(list.size());
        }
    }
    /*线程直接可能冲突了,导致没有有效的添加到集合
    9720
     */
    

同步方法

  • 由于我们可以通过 private 关键字来保让数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法synchronized方法和synchronized块

    同步方法:public synchronized void method(int args){}

  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

  • 同步方法弊端

    • 若将一个大的方法申明为synchronized 将会影响效率
    • 方法里面需要修改的内容才需要锁,锁的太多,浪费资源

同步块

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

三大不安全案例的解决办法

  • 买票

    public class Demo06 implements Runnable{
        //票数
        private int ticketNums = 10;
        private boolean flag = true;
        @Override
        public void run() {
            while (true) {
                buy();
                if (!flag) break;
            }
        }
    
        //synchronized同步方法,锁的是this,就是锁的这个Demo6对象
        public synchronized void buy() {
            if (ticketNums <= 0) {
                flag = false;
                return;
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- +"张票");
        }
    
        public static void main(String[] args) {
            Demo06 t = new Demo06();
    
            new Thread(t,"张三").start();
            new Thread(t,"李四").start();
            new Thread(t,"王五").start();
        }
    }
    
  • 取钱

    public class UnsafeBank {
        public static void main(String[] args) {
            Account account = new Account(15, "888");
    
            Drawing you = new Drawing(account, 10, "我");
            Drawing girlFriend = new Drawing(account, 10, "girlFriend");
    
            you.start();
            girlFriend.start();
        }
    }
    
    class Account{
        int money;//余额
        String name;//卡名
    
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    //模拟取款
    //《这里选择继承Threa的类是因为只用到了一种线程,》当然可以实例化多个线程
    class Drawing extends Thread{
    
        Account account;//账号
        //取了多少钱
        int drawingMoney;
        //手里还有多少钱
        int nowMoney;
        public Drawing(Account account, int drawingMoney, String name) {
            super(name);
            this.account = account;
            this.drawingMoney = drawingMoney;
        }
        //取钱
    
        /*
        用synchronized修饰run方法,本身锁的就是Drawing类,
        然而在main方法中临界资源是账号,而不是银行Drawing,new了两个不同的Drawing,
        现实生活中也可以看出,即便在两个银行网点,同时取一个账号的钱,也不会出错
         */
        @Override
        public void run() {
            //这里锁的更具体,限制共享资源,用同步块
            synchronized (account) {
                if (account.money - drawingMoney < 0) {
                    System.out.println(Thread.currentThread().getName() + "余额不足,操作失败");
                    return;
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //更新卡内余额
                account.money -= drawingMoney;
                //你手里的钱
                nowMoney += drawingMoney;
    
                System.out.println(account.name + "余额为:" + account.money);
                System.out.println(this.getName() + "手里的钱:" + nowMoney);
            }
        }
    }
    
  • 集合

    public class UnsafeList {
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            for (int i = 0; i < 10000; i++) {
                new Thread(() -> {
                    synchronized (list) {
                        list.add(Thread.currentThread().getName());
                    }
                }).start();
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(list.size());
        }
    }
    

小结:

  • 锁的范围要具体,要精确否则失效,锁的对象一定是增删改查的资源
  • 在第三个案例中,如果没有休眠时间,即使应用同步后也会出现错误,可能是具体创建需要一些时间,也可以看出休眠的是主线程,这不是同步角度的原因

CopyOnWriteArrayList

//测试JUC安全类型的集合
public class Demo21 {
    public static void main(String[] args) {
        //JUC提供了一个本身针对这种不安全情形的集合
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(list.size());
    }
}

死锁

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

public class Demo22 {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0, "张三");
        Makeup g2 = new Makeup(1, "李四");

        g1.start();
        g2.start();
    }
}

//口红
class Lipstick{
}
//镜子
class Mirror{
}
class Makeup extends Thread{
    //需要的资源只有一份,用static来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//选择
    String girlName; //使用化妆品的人

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

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    //化妆,相互持有对方的锁,就是需要拿到对方的资源
    private void makeup() throws InterruptedException{
        //产生死锁,死锁四个条件:请求保持,不可剥夺,循环等待,互斥访问
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(this.girlName + "获得口红的锁");
                Thread.sleep(1000);
                synchronized (mirror) {
                    System.out.println(this.girlName + "获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) {
                System.out.println(this.girlName + "获得镜子的锁");
                Thread.sleep(1000);
                synchronized (lipstick) {
                    System.out.println(this.girlName + "获得口红的锁");
                }
            }
        }
        //没有死锁
//        if (choice == 0) {
//            synchronized (lipstick) {
//                System.out.println(this.girlName + "获得口红的锁");
//                Thread.sleep(1000);
//            }
//            synchronized (mirror) {
//                System.out.println(this.girlName + "获得镜子的锁");
//            }
//        } else {
//            synchronized (mirror) {
//                System.out.println(this.girlName + "获得镜子的锁");
//                Thread.sleep(1000);
//            }
//            synchronized (lipstick) {
//                System.out.println(this.girlName + "获得口红的锁");
//            }
//        }
    }
}

Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized相同的并发性和内存语在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

image

synchronized与Lock的对比

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

生产者消费者问题

问题分析

作为一个408人,这个问题在熟悉不过了

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

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

线程通信

  • Java提供了几个方法解决线程之间的通信问题

    image

  • 个人认为这里就是PV操作和wait(),singal()的升级版本

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

解决方式

  1. 并发协作模型“生产者/消费者模式”--->管程法
    • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
    • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
    • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
    • 生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
  2. 并发协作模型“生产者/消费者模式”--->信号灯法

管程法

//测试:生产者消费者模型-->利用缓冲区解决:管程法

//生产者, 消费者, 产品, 缓冲区
public class Democracy25 {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container) {
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                container.push((new Chicken(i)));
                System.out.println("生产了" + i + "只鸡");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

//消费者
class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                System.out.println("消费了" + container.pop().id + "只鸡");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

//产品
class Chicken{
    int id;

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

//缓冲区
class SynContainer{
    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];
    //容器计算器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Chicken chicken) throws InterruptedException {
        //如果容器满了,那就等待消费者消费
        if (count >= chickens.length) {
            this.wait();
        }
        //如果没有满, 我们就需要丢入产品
        chickens[count] = chicken;
        count++;

        //通知消费者消费了
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Chicken pop() throws InterruptedException {
        //判断能否消费
        if (count <= 0) {
            //等待生产者生产,消费者等待
            this.wait();
        }
        //可以消费了
        count--;
        Chicken chicken = chickens[count];

        //通知生产者可以生产了
        this.notifyAll();

        return chicken;
    }
}

信号灯

public class Demo26 {
    public static void main(String[] args) {
        TV tv = new TV();

        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者-->演员
class Player extends Thread{
    TV tv;
    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("熊出没");
            } else {
                this.tv.play("六个核桃六六六");
            }
        }
    }
}

//消费者-->观众
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv) {
        this.tv = tv;
    }

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

//产品-->节目
class TV{
    //演员表演,观众等待
    //观众观看,演员等待
    String voice;//节目
    boolean flag = true;//信号灯,true没界面,false有界面
    //表演
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("演员表演了:" + voice);
        //通知观众观看
        this.notifyAll();//通知唤醒
        this.voice = voice;
        this.flag = !this.flag;
    }
    //观看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("观看了:" + voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

线程池

使用线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理(..)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止
  • JDK 5.0起提供了线程池相关API:ExecutorService 和Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class Demo27 {
    public static void main(String[] args) {
        //1.创建服务,创建线程池
        //newFixedThreadPool参数为:线程池大小,j静态方法,不用new
        ExecutorService service = Executors.newFixedThreadPool(10);
        //执行
        service.execute(new Mythread());
        service.execute(new Mythread());
        service.execute(new Mythread());
        service.execute(new Mythread());
        //2.关闭链接
        service.shutdown();
    }
}

class Mythread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
/*
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
 */
posted @ 2025-01-22 23:53  韩熙隐ario  阅读(20)  评论(0)    收藏  举报