JavaEE 初阶 -- 多线程基础

线程

  • 线程是什么?

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码

在这里插入图片描述

  • 为什么要有线程?

操作系统支持多任务,程序猿就需要并发(并行+并发)编程,通过多进程,可以实现并发编程,但是如果需要频繁创建和销毁进程,就会占用很大的资源,对资源的申请和释放是一个低效的操作

解决这个问题的方法:

  1. 进程池

就是一个类似于字符串常量池,数据库连接池一类的东西。当我们释放一个进程时,并不是真的把他的资源释放,而是把它放到池里面,下次再要用的时候,就直接从里面拿出来就行了,这样就跳过了申请和释放的过程。但是进程不用的时候它也消耗资源,进程很多的时候,吃的资源就多,那么其它正在使用进程调度的进程所能调用的资源就减少了。

  1. 使用线程来实现并发编程

线程比进程更轻量,创建线程比创建进程更快,销毁线程比销毁进程更快,调度线程比调度进程更快。

  • 为什么线程比进程轻量?

进程的重量在资源申请释放,线程是包含在进程中的,每个进程至少有一个线程存在,即主线程,一个进程可以有很多个线程,每个线程共用一份系统资源(1.内存空间 2.文件描述符表),只有在进程启动创建第一个线程的时候,需要花成本去申请系统资源,一旦进程(第一个线程)创建完毕,此后,后续再创建的线程,就不需要再申请资源了,创建/销毁的效率就提高了不少。进程是系统分配资源的最小单位,线程是系统调度的最小单位。

注意: 线程不是越多越好,CPU的核心数是固定的,线程多了也用不上,只能在那等着,此时程序的效率没有进一步的提升,反而可能会下降,因为调度本身也会有开销。此时就是进行调度,调度上了一个线程,势必会挤下一个线程,总并发程度仍然是固定不变的。真正有效果的,是再搞一个CPU(再搞一个主机),即分布式系统。

  • 进程和线程的区别

  1. 进程包含线程。
  2. 进程有自己独立的内存空间和文件描述符表,同一个进程中的多个线程之间,共享同一份地址空间和文件描述符表。
  3. 进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位。
  4. 进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一进程里多个线程之间,一个线程挂了,可能会把整个进程带走,影响到其它线程的。

进程调度的四大属性:

  1. 进程的优先级
  2. 进程的状态 -> 1. 就绪状态 2. 阻塞状态
  3. 进程的记账信息
  4. 进程的上下文

Java中执行多线程编程

在 Java 标准库中,就提供了一个 Thread 类,来 表示 / 操作 线程。
Thread 类也可以视为是 Java 标准库提供的 API(API:Thread 类提供的方法 和 类)。
Tread能够表示一个线程。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread t");
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread  t = new MyThread();// 先创建MyThread实例,t的引用实际上是指向子类的实例
        t.start();// 启动线程,在进程中搞了另外一个流水线,新的流水线开始并发的执行另一个逻辑了,也就是run方法里面的代码
    }
}

接下来看一个更加直接表示线程之间是并发执行的例子

class MyThread extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello t");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread  t = new MyThread();// 先创建MyThread实例,t的引用实际上是指向子类的实例
        t.start();// 启动线程,在进程中搞了另外一个流水线,新的流水线开始并发的执行另一个逻辑了,也就是run方法里面的代码

        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

sleep方法会抛异常:
在这里插入图片描述
运行效果:
在这里插入图片描述
t就是我们创建的线程,在一个进程中,至少会有一个线程,在一个java进程中,就会有一个调用main方法的线程,这个线程是系统自己创建的,也可以称为是主线程,所以 t 线程和main线程就是属于并发执行的关系,从宏观上讲是并行+并发的(取决于内部的调度),我们是区分不了到底是哪一个的,多个线程在CPU上调度执行顺序是随机的,而内部的调度虽然有优先级,但是这个优先级对于系统来说,只是建议,至于要怎么调度,还是得看系统的(阳奉阴违)。

在这里插入图片描述
在这里插入图片描述

Thread 类创建线程的写法

  1. 使用继承Thread,重写run方法的方式来创建线程

这种上面写的就是。

  1. 使用实现Runnable,重写run
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello t");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.start();

        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 继承Thread,使用匿名内部类

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("hello t");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        t.start();

        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 实现Runnable,使用匿名内部类

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("hello t");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t.start();

        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其实,创建线程最推荐的写法,最简单最直观的写法就是:

  1. 使用lambda表达式

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread( () -> {
            while(true) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 实现Callable

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo31 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 这只是创建个任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        // 还需要找个人来完成这个任务(线程)
        // Thread 不能直接传 callable,需要再包装一层
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        System.out.println(futureTask.get());
    }
}

Thread类里面一些常见的方法

在这里插入图片描述

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "我的线程");

        t.start();
    }
}

在这里插入图片描述
在这里插入图片描述

Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明

在这里插入图片描述

public class ThreadDemo7 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello t");
        });

        t.start();

        try {
            // 上述 t 线程没有进行任何循环和 sleep, 意味着里面的代码会迅速执行完毕.
            // main 线程如果 sleep 结束, 此时 t 基本上就是已经执行完了的状态. 此时 t 对象还在
            // 但是在 系统中 对应的线程已经结束了.
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(t.isAlive());
    }
}

在这里插入图片描述

public class ThreadDemo8 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {

            }
        });

        // 默认是前台线程, 也就是设为 false
        // 此时这个线程会阻止进程结束

        // 改成 true 变成后台线程. 不影响进程的结束.
        t.setDaemon(true);
        t.start();

    }
}

设置了之后,就会结束:
在这里插入图片描述

启动一个线程 - start()

  • 调用start方法和run方法的不同点:

作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程
运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

中断一个线程

中断这里就是字面意思,就是让一个线程停下来,线程的终止,本质上来说,让一个线程终止,办法就一种,让线程的入口方法执行完毕,(return ,抛出异常 …)

1. 可以手动的设置一个标志位

public class ThreadDemo10 {
    public static boolean isQuit = false;

    public static void main(String[] args) {
        Thread t = new Thread( () -> {
            while(!isQuit) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t 线程终止");
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        isQuit = true;
    }
}

此处因为多个线程共用一个虚拟地址空间!因此,main 线程 修改的 isQuit 和 t 线程判定 的 isQuit 是同一个值。但是,如果是在进程的那种情况下,在不同的虚拟地址的情况下,这种写法就会失效

  • 如果把isQuit从成员变量改为局部变量 main里面,该代码还能否正常工作?

不能,因为lambda表达式的变量捕获规则,捕获的变量必须是final或者“实际final”的,这里后面isQuit会被修改,所以只能作为外面的成员变量。

2.使用 Thread 中内置的一个标志位来进行判断来进行判定

public class ThreadDemo11 {
    public static void main(String[] args) {
        Thread t = new Thread( () -> {
        	// currentThread 是获取到当前线程实例.
            // 此处 currentThread 得到的对象就是 t
            // isInterrupted 就是 t 对象里自带的一个标志位.
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //break;
                }
            }
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 把 t 内部的标志位给设置成 true
        t.interrupt();
    }
}

运行结果:
在这里插入图片描述

Thread currentThread().isInterrupted() 【这是一个实例方法,其中 currentThread 能够获取当前线程的实例】

运行后此时发现:3s后,调用t.interrupt()方法的时候,线程没有正在的就结束了,而是打印了异常信息,又继续执行了,也就是说, t.interrupt() 不仅仅是针对 while循环的条件(标记位) 进行操作,它还触发一个异常
在这里插入图片描述
所以如果需要结束循环,就得在catch中搞个break
特殊情况:
在这里插入图片描述

  • 为啥sleep要清空标志位呢?

目的就是为了让线程自身能够对于线程何时结束,有一个更明确的控制。
当前,interrupt方法效果,不是让线程立即结束,而是告诉她你该结束了,至于他是否要结束,立即结束还是等会结束,都是代码来灵活控制的,interrupt只是通知而不是命令。

在这里插入图片描述
在这里插入图片描述

等待一个线程 - join()

在这里插入图片描述
但是我们如果这样写:

public class ThreadDemo12 {
    public static void main(String[] args) {
        Thread t = new Thread( () -> {
            System.out.println("hello t");
        });
        t.start();

        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("hello main");
    }
}

运行结果就一定是先打印hello t,再打印hello main

在main中调用t.join(),就是执行到join时让main线程等待 t 线程结束后再执行下去,在t.join执行的时候,如果 t 线程还没结束,mian线程就会阻塞等待(Blocking),但是其它线程不受影响,代码走到这一行就停下来,当前这个线程不参与cpu的调度执行了。但是如果mian线程调用t.join时,t 已经结束了,此时join不会阻塞,会继续执行下去

  • join有一个有参数版本的,就是设定了等待时间,如果时间一过,就不等待了。虽然不等待,但是这个线程还是依旧在执行的。

线程的状态

在这里插入图片描述
在这里插入图片描述

NEW

public class ThreadDemo13 {
    public static void main(String[] args) {
        Thread t = new Thread( () -> {
            while(true) {

            }
        });
        System.out.println(t.getState());
        t.start();
    }
}

把 Thread 对象创建好了,但是没有调用start方法。

TERMINATED

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread( () -> {
            
        });
        t.start();

        Thread.sleep(2000);
        System.out.println(t.getState());
    }

操作系统中的线程已经执行完毕,销毁了。但是 Thread 对象还在,此时获取的状态就是 TERMINATED

RUNNABLE

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread( () -> {
            while(true) {

            }
        });
        t.start();
        System.out.println(t.getState());
    }

正在执行或者在就绪状态的,随时可以别调度到CPU上的

TIMED_WAITING

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread( () -> {
            while(true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }

代码中调用了 sleep、join(超时时间),就会进入到 TIMED_WAITING。它的意思就是当前的线程在一定的时间内,是阻塞状态。

BLOCKED

当前线程在等待 锁,导致到了阻塞(阻塞状态之一),一般是在我们使用 synchronized 来去加锁的时候,可能会触发这种状态。

WAITING

当前线程在等待 唤醒,导致到了阻塞(阻塞状态之一),一般是在我们使用 wait 来等待唤醒的时候,可能会触发这种状态。

线程安全

某个代码,在多线程环境下执行,会出bug,就是线程不安全,本质上是因为线程之间的调度顺序是不确定的

class Counter {
    private int count = 0;

    public void add() {
        count++;
    }

    public int get() {
        return count;
    }
}

public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

每次运行的结果都是随机的,因为

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

线程不安全的原因

在这里插入图片描述
既然count++不是原子的,那么有没有办法让他变为原子的,有的:

加锁

"锁"就能保证"原子性"的效果,一旦某个线程加上锁之后,其它线程也想加锁,就不能直接加上,需要阻塞等待,一直等到拿到锁的线程释放了锁为止。线程调度,是抢占式执行,导致了进程的调度是随机的。

  • synchronized
class Counter2 {
    private int count = 0;
    public void add() {
        synchronized(this) {
            count++;
        }
    }
    public int getCount() {
        return count;
    }
}

public class ThreadDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = new Counter2();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter2.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter2.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter2.getCount());
    }
}

加锁还能怎么写:

synchronized public void add() {
        count++;
}
private Object locker = new Object();
public void add() {
    synchronized (locker ) {
        count++;
    }
}

在这里插入图片描述
在这里插入图片描述

synchronized旁边括号的this是锁对象:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

由于内存可见性,引起的线程不安全

public class ThreadDemo16 {
    public static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 0) {
                
            }
            System.out.println("线程 t1 结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });

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

在这里插入图片描述
这里线程并没有结束,这就是内存可见性的锅:
在这里插入图片描述
编译器优化导致了这个问题,如果是在单线程中,编译器的优化是不会有问题的,但是在多线程中,就不一定了,出现了误判导致了bug。

所谓内存可见性,就是在多线程环境下,编译器对代码的优化,产生了误判,从而引起了bug。

所以我们的处理方法就是让编译器对这个场景暂停优化

volatile

被volatile修饰的变量,此时编译器就会禁止上述优化,能够保证每次都是从内存从新读取数据

在这里插入图片描述
在这里插入图片描述

  1. volatile不保证原子性
  2. volatile适用的场景,是一个线程读,一个线程写的情况
  3. synchronized则是多个线程写
    volatile这个效果,称为"保证内存可见性"

禁止指令重排序

指令重排序,也是编译器优化的策略,调整了代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变。还是对于单线程容易保证,多线程不好说。

在这里插入图片描述
这里加锁就是其中一个在执行的时候,另外一个不执行,就不会出现先执行1和3,在执行t2的情况,只能有一块执行,而使用volatile就是让new操作严格按照1,2,3的顺序执行,也不会出错。

在这里插入图片描述

volatile 不能保证原子性

wait 和 notify

wait和notify都是属于Object类的方法,只要你是个类对象,不是内置类型(基本数据类型),就能使用这两个方法,调用wait方法就会阻塞等待,等到notify的通知才继续

举个例子:一群人去ATM取钱,第一个老哥进去后,发现ATM中的钱被取空了,于是这个老哥只能在那里干等,其它人也进不去,这叫线程饿死。如果使用wait和notify,这个老哥就会出来,等到有人把钱存进去之后,notify通知他后,他在进去取钱。
在这里插入图片描述
notify也是需要放到锁中使用的,必须要先执行wait然后notify,此时才有效果,如果还没有wait就notify,就是空打一炮,此时wait无法唤醒,代码不会出现其它异常,但是notify就没有了作用,代码功能就不能正常执行了。

public class ThreadDemo18 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            try {
                System.out.println("wait 开始");
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("wait 结束");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("notify 开始");
            synchronized (object) {
                object.notify();
            }
            System.out.println("notify 结束");
        });
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

在这里插入图片描述

wait可以带参数,就不会是死等的状态了
唤醒操作,还有一个叫notifyAll,会把所有的线程唤醒同时去竞争锁
可以有多个线程等待同一个对象,如果调用notify,就只会随机唤醒一个

wait和sleep对比

他们最大的区别就是初心不同,就是设计者东西是来解决什么问题的,wait解决的是线程之间的顺序控制,而sleep单纯就是让当前线程休眠一会

共同点:
都是使线程暂停一段时间的方法。
不同点:
wait是Object类中的一个方法,sleep是Thread类中的一个方法;
wait必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用;
wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作;

刷新内存

synchronized 的工作过程
1、 获得互斥锁
2、 从主内存拷贝变量的最新副本到工作的内存
3、 执行代码
4、 将更改后的共享变量的值刷新到主内存
5、 释放互斥锁
这也是为什么前面 synchronized 可以解决 内存可见性的线程问题。

可重入

直观来说:同一个线程针对同一个锁,连续加锁两次。
如果出现了死锁,就是不可重入,如果不会死锁,就是可重入。

  • 设计模式目前学两个:单例模式和工厂模式

单例模式

一个程序中,某个类,只创建出一个实例(一个对象),不能创建多个对象。
比如:JDBC编程中的DataSource,就是单例的。大部分跟数据有关的东西,服务器里面只存一份,那么就都可以使用单例模式来进行表示。

  • 单例模式有两种典型的实现:
    1. 饿汉模式(急迫)
    1. 懒汉模式(从容)

比如:你吃完饭用了4个碗,对于饿汉模式:就会把这4个碗都给洗了;而懒汉模式,就会等到下次要吃饭的时候,要用到2个碗就只洗2个。对于打开一个有10G的文件来说,饿汉模式会把一整个文件都给加载了,而懒汉模式,就会在你看到哪里再加载到哪里,就会很快,所以这种模式下,懒汉模式的效率更快。

饿汉模式

class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() { }
}

public class ThreadDemo19 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
    }
}

在这里插入图片描述
饿汉模式是比较着急的,在类加载的时候,就会把instance对象创建出来

懒汉模式

懒汉模式来实现单例:非必要,不创建

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy() {}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}

在这里插入图片描述

实现一个线程安全的单例模式

在这里插入图片描述
如何解决这个问题呢?

  • 加锁

在这里插入图片描述
但是加锁,又是一个比较低效的操作,加锁可能设计到阻塞等待,所以非必要,不加锁。

在这里插入图片描述

  • 注意:这代码还有一个重要的问题:指令重排序

如果两个线程同时调用getInstance:
在这里插入图片描述
在这里插入图片描述

小结

单例模式线程安全问题
饿汉模式,天然就是安全的,只是读操作
懒汉模式,不安全的,有读还有写

  1. 加锁,把if和new变成原子操作
  2. 双重if,减少不必要的加锁操作
  3. 使用volatile禁止指令重排序,保证后续线程肯定拿到的是完整对象
class SingletonLazy {
    volatile private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}

阻塞队列

阻塞队列和普通的队列有着相同的特性:先进先出

但是,对比于普通队列,阻塞队列有着其它的功能:
阻塞队列,带有阻塞特性,是线程安全的

  1. 如果队列为空,尝试出队列,就会阻塞等待,等待队列不为空为止
  2. 如果队列满, 尝试入队列,也会阻塞等待,等待到队列不满为止

这个阻塞队列,尤其是写多线程代码时,多个线程之间进行数据交互,可以使用阻塞队列简化代码编写。Java标准库,就提供了阻塞队列的使用。其中入队方法为put,出队方法为take,和队列的入队出队不大一样。

在这里插入图片描述
阻塞就会需要去唤醒,唤醒可能就会被打断,打断就会抛异常。

下面我们写一个"生产者消费者模型"多线程使用的阻塞队列

什么是生产者消费者模型:包饺子,如果4个人同时需要和面,擀面皮,包饺子的话,一般擀面杖只有一根,然后他们会竞争这根擀面杖,效率就会很低,即使有4根擀面杖,来回切换也会使得效率低下,更高效的做法是1个人负责擀面皮,另外3个负责包饺子,而中间那个存放饺子皮的盖帘,这个交易场所,就是阻塞队列。

  • 生产者消费者模型的初心是为了干什么?

能解决的问题有很多,最主要的两个方面就是:

  1. 可以让上下游模块之间,更好的“解耦合”
  2. 削峰填谷

耦合:简单来说就是牵一发而动全身,写代码要追求低耦合。
内聚:相关联的代码,分门别类的规划起来。

1. 可以让上下游模块之间,更好的“解耦合”

假设有两个服务器:A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据。
此时如果A和B直接通信,此时就是耦合比较高的情况,如果B挂了,就会导致A也挂了,另外如果再加个C,此时对于A有一个较大的调整,这样就是高耦合。
在这里插入图片描述
此时如果引入生产者消费者模型,耦合就降低了
在这里插入图片描述

2. 削峰填谷

在这里插入图片描述
A作为入口服务器来说,起到的效果就是调用一下和其它服务器把结果汇总
B是有具体业务的,工作压力承担大,单个请求吃的资源就多,容易挂

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo22 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费元素: " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                System.out.println("生成元素: " + value);
                try {
                    queue.put(value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
    }
}

在这里插入图片描述
这个结果就说明阻塞等待了,消费等待生产

实现阻塞队列

  1. 先实现一个普通队列
  2. 加上线程安全
  3. 加上阻塞功能
class MyBlockingQueue {
    private int[] elems = new int[1000];

    volatile private int head = 0;
    volatile private int tail = 0;
    volatile private int size = 0;

    synchronized public void put(int elem) throws InterruptedException {
        if(size == elems.length) {
            this.wait();
        }
        elems[tail] = elem;
        tail++;
        if(tail == elems.length) {
            tail = 0;
        }
        size++;
        this.notify();
    }

    synchronized public Integer take() throws InterruptedException {
        if(size == 0) {
            this.wait();
        }
        int elem = elems[head];
        head++;
        if(head == elems.length) {
            head = 0;
        }
        size--;
        this.notify();
        return elem;
    }
}

public class ThreadDemo23 {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
    }
}

在这里插入图片描述
在这里插入图片描述

class MyBlockingQueue {
    private int[] elems = new int[1000];

    volatile private int head = 0;
    volatile private int tail = 0;
    volatile private int size = 0;

    synchronized public void put(int elem) throws InterruptedException {
        while (size == elems.length) {
            this.wait();
        }
        elems[tail] = elem;
        tail++;
        if(tail == elems.length) {
            tail = 0;
        }
        size++;
        this.notify();
    }

    synchronized public Integer take() throws InterruptedException {
        while (size == 0) {
            this.wait();
        }
        int elem = elems[head];
        head++;
        if(head == elems.length) {
            head = 0;
        }
        size--;
        this.notify();
        return elem;
    }
}

public class ThreadDemo23 {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();

        Thread t1 = new Thread(() -> {
            while(true) {
                try {
                    int value = myBlockingQueue.take();
                    System.out.println("消费 : " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            int value = 0;
            while(true) {
                try {
                    myBlockingQueue.put(value);
                    System.out.println("生产 :" + value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

在这里插入图片描述

定时器

定时器就是闹钟,设定一个时间,当时间到,就可以执行一个指定的代码
java标准库中,提供了定时器:Timer,是java.util包下的
Timer它的核心方法就只有一个:schedule

public class ThreadDemo26 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        }, 2000);
        System.out.println("hello0");
    }
}

在这里插入图片描述
上面代码的运行结果:
在这里插入图片描述

先打印了hello0,过了2s,再打印出的hello1,但是程序并没有结束

这是因为Timer里头内置了线程(还是前台线程,会阻止线程结束),来负责执行注册任务的,当我们代码走到任务结束了,Timer的内部这个线程还在等,等待其它任务加进来,就继续执行。

自己实现一个定时器

定时器,内部管理的不仅仅是一个任务,可以管理很多任务的
我们从MyTimer类的内部需要什么入手

  1. 管理很多的任务
  2. 执行时间到了的任务

任务又可以分为:

  1. 描述任务(创建一个类来表示一个定时器的任务)
  2. 组织任务(使用一定的数据结构进行数据组织)
  3. 执行时间到了的任务

组织的任务需要的数据结构:核心的数据结构就堆(PriorityQueue),因为需要线程安全,所以有个更加适合的就是PriorityBlockingQueue,带有阻塞功能的优先级队列

import java.util.concurrent.PriorityBlockingQueue;

class MyTimer {
    private PriorityBlockingQueue<MyTimerTask> priorityBlockingQueue = new PriorityBlockingQueue<>();

    private Object block = new Object();

    public void schedule(Runnable runnable, long delay) {
        MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
        priorityBlockingQueue.put(myTimerTask);
    }

    public MyTimer() {
        Thread t = new Thread(() -> {
            while(true) {
                try {
                    MyTimerTask myTimerTask = priorityBlockingQueue.take();
                    long curTime = System.currentTimeMillis();
                    if(myTimerTask.delay <= curTime) {
                        myTimerTask.run();
                    }else {
                        priorityBlockingQueue.put(myTimerTask);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

class MyTimerTask {
    public Runnable runnable;
    public long delay;
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.delay = System.currentTimeMillis() + delay;
    }
    public void run() {
        runnable.run();
    }
}

在这里插入图片描述
写到这里,我们遗漏了两个重要的问题:

  1. 既然线程每次需要拿出最先执行的任务,则优先级队列在插入任务的时候应该要有个比较的规则
  2. 线程里面会循环拿出任务进行比较时间,如果时间没到,会频繁的拿出来比较,就会出现 “忙等”,CPU的资源就无法得到释放,一直占用着,所以要利用到线程等待。

在这里插入图片描述
在这里插入图片描述

import java.util.concurrent.PriorityBlockingQueue;

class MyTimer {
    private PriorityBlockingQueue<MyTimerTask> priorityBlockingQueue = new PriorityBlockingQueue<>();

    private Object block = new Object();

    public void schedule(Runnable runnable, long delay) {
        MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
        priorityBlockingQueue.put(myTimerTask);
        synchronized (block) {
            block.notify();
        }
    }

    public MyTimer() {
        Thread t = new Thread(() -> {
            while(true) {
                try {
                    synchronized (block) {
                        MyTimerTask myTimerTask = priorityBlockingQueue.take();
                        long curTime = System.currentTimeMillis();
                        if(myTimerTask.delay <= curTime) {
                            myTimerTask.run();
                        }else {
                            priorityBlockingQueue.put(myTimerTask);
                            block.wait(myTimerTask.delay - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

class MyTimerTask implements Comparable<MyTimerTask> {
    public Runnable runnable;
    public long delay;
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.delay = System.currentTimeMillis() + delay;
    }
    public void run() {
        runnable.run();
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.delay - o.delay);
    }
}

public class ThreadDemo27 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello5");
            }
        }, 5000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello4");
            }
        }, 4000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello3");
            }
        }, 3000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        }, 2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        }, 1000);
        System.out.println("hello0");
    }
}

线程池

  • 线程池的执行流程

线程池的执行流程:
1.当新加入一个任务时,先判断当前线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务;
2.如果结果为 true,则判断任务队列是否已满,如果结果为 false,则把任务添加到任务队列中等待线程执行
3.如果结果为 true,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务
4.如果结果为 true,执行拒绝策略

提前把线程准备好,创建线程不是从系统申请,而是从池子里拿,线程不用了,也是还给池子

池的目的:就是为了提高效率,线程的创建,虽然比进程轻量,但是在频繁创建的情况下,开销也是不可忽略的。所以希望还能够进一步提高效率:1. 协程(轻量级线程),不过java标准库还不支持
2. 线程池

  • 为啥从池子里拿线程比从系统创建线程要更加高效?
    从池子里拿线程,纯粹的用户态操作
    从系统创建线程,涉及到用户态和内核态之间的切换
    真正的创建是要在内核态完成的

  • 线程池的优点:

降低资源消耗:减少线程的创建和销毁带来的性能开销。
提高响应速度:当任务来时可以直接使用,不用等待线程创建
可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。

在这里插入图片描述

举个例子:你去银行办理银行卡,大厅就是用户态,柜台里就相当于内核态,有个老哥去办银行卡,工作人员要他的身份证复印件,给他两个选择,一个是在大厅的复印机自己去复印,另一个是工作人员帮他复印,很显然,如果他自己去复印,他肯定直接就去复印完然后回来,可是工作人员帮的话,可能中途还去个厕所啥的。

所以,纯用户态的操作,时间是可控的,涉及到内核操作,时间就不太可控了

在这里插入图片描述
在这里插入图片描述

ThreadPoolExecutor

在java标准库中,线程池对应的类,叫ThreadPoolExecutor,是原装的线程池对象,上述工厂方法是对这个对象的进一步封装,我们进入java的官方文档来看一下这个类
在这里插入图片描述

标准库提供的4种拒绝策略(经典的面试题)

在这里插入图片描述
拒绝策略:
在这里插入图片描述

模拟实现一个线程池

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while(true){
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

public class ThreadDemo29 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int number = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello" + number);
                }
            });
        }
    }
}

在这里插入图片描述
当前的代码中,搞了10个线程,那么在实际开发中,一个线程池线程的数量,设置为多少合适?

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值