多线程

  • 1.线程与进程
  • 2.同步与异步
  • 3.并发与并行
  • 4.继承Thread类
    • 4.1 Thread在程序中的使用
  • 5.Runnable接口
  • 6.Callable接口
  • 7.线程有关操作
    • 7.1 设置和获取线程名称
    • 7.2 线程休眠
    • 7.3 线程中断
    • 7.4 设置守护线程
    • 7.5 停止线程
  • 8.线程安全
    • 8.1显式锁与隐式锁
    • 8.2隐式锁同步代码块
    • 8.3 同步方法
    • 8.4 显式锁
    • 8.5公平锁与非公平锁
  • 9. 线程死锁
  • 10.多线程通信问题
  • 11.线程池Executors

1.线程与进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行.
一个进程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

线程调度

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
  • Java使用的为抢占式调度。
    CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。

2.同步与异步

  • 同步:排队执行 , 效率低但是安全.
  • 异步:同时执行 , 效率高但是数据不安全

3.并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

4.继承Thread类

  • Thread是Java提供的用于实现线程的类

  • 有一个继承的方法run,run方法中的代码就是一条新的执行路径,路径的触发方式不是调用run方法,而是通过Thread对象的start来启动任务

  • 常用方法

  • 字段

  • 常用方法

  • daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了

  • 用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡

面试题:
如何将一个线程停止?
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时
我们可以通过变量做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定

4.1 Thread在程序中的使用

  • 常规用法
    新建一个类继承Thread

代码示例:

  • 编写一个线程

class MyThread extends Thread{   //Thread,JAVA提供的用于实现线程的类/*** 线程线程要执行的任务方法* 每个线程都有自己的栈空间,共用一份堆内存*/@Overridepublic void run() {//这里的代码,就是一条新的执行路径//这个执行路径的触发方式,不是调用run方法,而是通过Thread对象的start来启动任务for(int i = 0;i < 5;i++){System.out.println( i + "这是另一个线程");}}
  • 再在主函数中编写代码,来实现多线程
public static void main(String[] args) {MyThread thread = new MyThread();thread.start();        //时间分配是抢占式分配for(int i = 0;i < 5;i++){System.out.println( i + "这是main中的线程");}}
  • 由于Java的时间分配是抢占式时间分配,谁抢占到谁就先执行,因此得到如下输出结果
0这是另一个线程
0这是main中的线程
1这是另一个线程
1这是main中的线程
2这是另一个线程
2这是main中的线程
3这是另一个线程
4这是另一个线程
3这是main中的线程
4这是main中的线程
  • 图解

使用匿名内部类

  • 代码展示
public static void main(String[] args) {//new Thread(){}.start();new Thread(){        //匿名内部类,仅几行代码即可实现一个线程@Overridepublic void run() {for (int i = 0;i < 5;i++){System.out.println(i + "hahaha");}}}.start();for (int i = 0;i < 5;i++){System.out.println(i + "heiheihei");}}//end main
  • 运行结果
0hahaha
0heiheihei
1heiheihei
1hahaha
2heiheihei
3heiheihei
2hahaha
4heiheihei
3hahaha
4hahaha

5.Runnable接口

  • 用于给线程执行的任务,但是还是要借助Thread
  • 【创建一个任务对象,里面包含了任务→再创建一个线程,为其分配这个任务→start执行】

与前面继承Thread类相比,优势在于

  • 通过 创建任务→给线程分配的方式 来实现多线程,更适合多个线程同时执行相同任务的情况

  • 可以避免单继承所带来的的局限性(java单继承,但是可以多实现)

  • 任务与线程本身分离,提高了程序的健壮性

  • 线程池技术,接收Runnable类型的任务,不接收Thread类型的线程

  • 代码示例
/*** 第二种实现多线程技术* 实现Runnable接口* 用于给线程执行的任务,但是还是要借助Thread*/
class MyRunnable implements Runnable{@Overridepublic void run() {//线程任务for(int i = 0;i < 5;i ++){System.out.println( i + "这是另一个线程");}}
}public static void main(String[] args) {/*** 第二种实现多线程技术* 实现Runnable接口(用的更多)*/MyRunnable r = new MyRunnable();//创建一个任务对象,里面包含了任务Thread t = new Thread(r);//创建一个线程,为其分配一个任务t.start();//执行这个线程for(int i = 0;i < 5;i++){System.out.println( i + "这是main中的线程");}}
输出结果:
0这是另一个线程
1这是另一个线程
2这是另一个线程
3这是另一个线程
0这是main中的线程
4这是另一个线程
1这是main中的线程
2这是main中的线程
3这是main中的线程
4这是main中的线程

6.Callable接口

用的少 我不想写 用到再补

7.线程有关操作

7.1 设置和获取线程名称

  • 代码示例
    static class MyRunnable implements Runnable{@Overridepublic void run() {//currentThread():获取当前正在执行的对象//getName获取线程名称System.out.println(Thread.currentThread().getName());}}//currentThread():获取当前正在执行的对象System.out.println(Thread.currentThread().getName());//main线程Thread t = new Thread(new MyRunnable());t.setName("第0个线程");//使用setName设置线程名称t.start();new Thread(new MyRunnable(),"第1个线程").start();new Thread(new MyRunnable(),"第2个线程").start();new Thread(new MyRunnable(),"第3个线程").start();new Thread(new MyRunnable()).start();//没有给线程setName,则系统会自动命名new Thread(new MyRunnable()).start();new Thread(new MyRunnable()).start();main
第0个线程
第1个线程
第3个线程
Thread-1
第2个线程
Thread-3
Thread-2

7.2 线程休眠

  • 常用方法


sleep为Thread的静态方法,因此可以用Thread直接调用:Thread.sleep()

1秒 = 1000毫秒
1毫秒 = 1000微妙 = 1000000纳秒

代码示例:

        for (int i = 0;i < 5;i++){System.out.println(i);Thread.sleep(1000);            //每次循环暂停1000毫秒后再继续执行}
0
1
2
3
4

7.3 线程中断

  • 一个线程是一个独立的执行路径,是否应该结束,应该由其自身决定
    用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时我们可以通过给对象做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定
    (代码里用interrupt打断 但是打断之后只是提示程序员是否终止 因为可以选择不中止 起到一个提示作用 如果要终止 依靠方法的 异常 方法终止 可以添加return)
    代码示例:

  • 首先新建类实现Runnable接口,并继承run方法 新建线程

    static class MyRunnable implements Runnable{@Overridepublic void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catchfor (int i =0;i < 10;i ++){System.out.println(Thread.currentThread().getName() + ":" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
  • 编写main
    public static void main(String[] args) {Thread t1 = new Thread(new MyRunnable());  //新建线程t1.start();for (int i =0;i < 5;i ++){//main线程System.out.println(Thread.currentThread().getName() + ":" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {//打标记之后程序进去catche.printStackTrace();}}//main线程打印5次,t1线程打印10次,因此main线程先中断,此时对t1线程打标记//给线程t1添加中断标记,但是只是告诉线程它可以死亡,但是未必死亡t1.interrupt();}
  • 打标记处理:
    对线程对象打标记,触发异常,使程序进入catch,后续的处理依旧由程序员决定

① 修改MyRunnable中的try-catch语句

发现中断标记后进入catch,但是程序可以选择不死亡,继续执行

                try {Thread.sleep(1000);} catch (InterruptedException e) {//                    e.printStackTrace();System.out.println("发现了中断标记,但是不死亡");}
  • 输出结果
    每个线程都隔1秒打印一个数,由于线程不死亡,因此发现标记之后继续执行
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,但是不死亡
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
  • ② 修改MyRunnable中的try-catch语句
    发现中断标记后进入catch,程序死亡,中断程序、释放资源
                try {Thread.sleep(1000);} catch (InterruptedException e) {//                    e.printStackTrace();System.out.println("发现了中断标记,线程自杀");return;//表示线程结束,资源释放}
  • 输出结果:
    每个线程都隔1秒打印一个数,由于发现中断标记后线程自杀死亡,因此发现标记之后结束程序
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,线程自杀

7.4 设置守护线程

线程分为守护线程和用户线程

用户线程:当一个进程不包含任何存活的用户线程时,进行结束(我们直接创建的线程都是用户线程)

守护线程:用于守护用户线程,当最后一个用户线程结束时,守护线程自动死亡

  • 代码示例:
    新建类实现Runnable接口,并继承run方法 新建线程
    static class MyRunnable implements Runnable{@Overridepublic void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catchfor (int i =0;i < 10;i ++){System.out.println(Thread.currentThread().getName() + ":" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}
  • 编写main
    在线程启动前,用setDaemon()来标记守护线程
    public static void main(String[] args) {Thread t1 = new Thread(new MyRunnable());//t1为子线程t1.setDaemon(true);//设置t1为守护线程,在t1启动前设置t1.start();//启动守护线程//main主线程,当主线程结束时,守护线程也会结束for (int i =0;i < 5;i ++){System.out.println(Thread.currentThread().getName() + ":" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
main:3
Thread-0:3
main:4
Thread-0:4
Thread-0:5

7.5 停止线程

以下文多线程通信问题中的生产者与消费者问题中的程序为例,我们让线程运行,但是我们只是打开了线程,并没有关闭线程,到最后程序运行完只能手动停止线程

        Thread.currentThread().interrupt();String threadName = Thread.currentThread().getName();System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());

8.线程安全

代码示例:

    /*** 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题*/static class Ticket implements Runnable{//总票数private int count = 10;@Overridepublic void run() {//每次被触发就进卖买票操作while(count > 0){//卖票System.out.println("正在准备卖票");//try-catch使得卖票的时间更长try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count --;System.out.println("出票成功!余票:" + count);}}//end}public static void main(String[] args) {//线程不安全Runnable runnable = new Ticket();//启动三个线程new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();}正在准备卖票,请稍等...
正在准备卖票,请稍等...
正在准备卖票,请稍等...
出票成功!余票:8
正在准备卖票,请稍等...
出票成功!余票:9
正在准备卖票,请稍等...
出票成功!余票:7
正在准备卖票,请稍等...
出票成功!余票:5
正在准备卖票,请稍等...
出票成功!余票:6
正在准备卖票,请稍等...
出票成功!余票:4
正在准备卖票,请稍等...
出票成功!余票:3
正在准备卖票,请稍等...
出票成功!余票:2
正在准备卖票,请稍等...
出票成功!余票:1
正在准备卖票,请稍等...
出票成功!余票:0
出票成功!余票:-2
出票成功!余票:-1

通过输出结果观察可知,余票出现了负数,但是代码逻辑上余票count=0时便不再执行了

出现问题原因:假设三段线程为ABC,ABC可能同时进行到while,假设A先进入,此时count = 1,当A进入休眠未进行到count–时,B检测到count = 1,进入while,当B进入休眠未进行到count–时,C检测到count = 1,进入while,此时A运行count–,count = 0,B接着运行count- -,count =-1,C接着运行count- -,count = -2,同时由于线程阻塞以及线程调度,输出的顺序可能不同

这就是多线程完成统一任务时出现的线程不安全问题

8.1显式锁与隐式锁

  • 所谓的显式和隐式,就是在使用的时候使用者是否需要手动写代码去获取锁和释放锁
  • 隐式锁:隐式锁使用synchronized修饰符。在使用sync关键字的时候,当sync代码块执行完成之后程序能够自动获取锁和释放锁
  • 显式锁:显式锁使用Lock关键字。在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
  • 添加synchronized关键字的同步代码块和同步方法属于隐式锁

8.2隐式锁同步代码块

线程同步,使线程排队执行

实现思路:每个线程在执行时看同一把锁,谁抢到了锁,谁就执行

线程同步实现:synchronized

格式:
synchronized(锁对象){
// 同步代码块
}

锁对象: java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象

  • 代码示例:
    对原有的线程不安全的卖票示例进行修改
    在while循环中加锁
    同步代码块为:当余票大于0时,进行卖票操作
    因此当一个线程正在执行同步代码块时,另外的线程不会执行该代码块,在后面排队等待执行
 /*** 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题**  解决线程不安全问题:排队执行*/static class Ticket implements Runnable{//总票数private int count = 10;private Object o = new Object();//创建对象@Overridepublic void run() {//每次被触发就进卖买票操作while(true){synchronized (o){//加锁if(count > 0){//卖票System.out.println("正在准备卖票,请稍等...");//try-catch使得卖票的时间更长try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count --;System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);}else{break;}}}//end while}//end run}
  • 由于只创建了一个任务,因此Object对象只创建了一个,即创建了一把锁
    而后面启动的三个线程由于只有一个任务,因此三个线程在执行的时候看同一把锁,谁抢到锁谁就执行,排队执行
        Runnable runnable = new Ticket();//只有一个任务,因此下面的object对象只创建了一个//启动三个线程,o是同一个,只有一个任务,因此在执行的时候只看一个onew Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();//如果上述写法写成如下,则依旧为不安全线程//此时创建了三个任务(new Ticket()),分别创建三个object对象(即锁),此时相当于3个人卖票,每个人卖10张 //错误写法!!!!注意//new Thread(new Ticket()).start();//new Thread(new Ticket()).start();//new Thread(new Ticket()).start();
  • 加了锁之后的输出结果:
正在准备卖票,请稍等...
Thread-0出票成功!余票:9
正在准备卖票,请稍等...
Thread-0出票成功!余票:8
正在准备卖票,请稍等...
Thread-0出票成功!余票:7
正在准备卖票,请稍等...
Thread-0出票成功!余票:6
正在准备卖票,请稍等...
Thread-0出票成功!余票:5
正在准备卖票,请稍等...
Thread-0出票成功!余票:4
正在准备卖票,请稍等...
Thread-0出票成功!余票:3
正在准备卖票,请稍等...
Thread-0出票成功!余票:2
正在准备卖票,请稍等...
Thread-0出票成功!余票:1
正在准备卖票,请稍等...
Thread-0出票成功!余票:0

如果将创建锁的对象写在任务的代码块中,如下所示

此时,每个线程启动时都会创建o对象,因此每个线程都有自己锁o,每个线程在执行时都看自己的锁,这时不能排队,要格外注意!!!!

错误写法:

public void run() {//每次被触发就进卖买票操作Object o = new Object();//!!!!!!!三个线程启动时都会创建o对象,即每个线程都有自己的锁o,每个人都看自己的不同的锁,此时不能排队while(true){synchronized (o){//加锁if(count > 0){//卖票System.out.println("正在准备卖票,请稍等...");//try-catch使得卖票的时间更长try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count --;System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);}else{break;}}}//end while}//end run

8.3 同步方法

  • 与同步代码块相似,不同的是,同步方法以方法为单位进行加锁,给方法添加synchronized修饰符

  • 同步方法的锁为this
    同步方法有可能被静态修饰,如果被静态修饰,则同步方法的锁为类.class

代码示例:

 /*** 创建一个任务,但是交给三个线程去执行,则会出现线程不安全问题* 解决线程不安全问题:排队执行*/static class Ticket implements Runnable{//总票数private int count = 10;@Overridepublic void run() {//每次被触发就进卖买票操作while(true){boolean flag = sale();//sale()为加了锁的方法if(!flag){break;}}//end while}//end run//添加synchronized修饰符,给方法加锁public synchronized boolean sale(){//this,同步的方法的锁//Ticket.class,如果方法为静态方法,则同步方法的锁为类.classif(count > 0){//卖票System.out.println("正在准备卖票,请稍等...");//try-catch使得卖票的时间更长try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count --;System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);return true;}return false;}}
  • 如果同步代码块锁了一段代码,同步方法锁了另一端代码,锁的对象都是this,那么这当一段代码正在执行时,另一段加锁的代码不能执行
    如下面的代码所示,在循环前加了一把锁,则当一个线程执行这段代码块时,同步方法sale不能执行
  public void run() {synchronized (this){//再加一把锁}while(true){boolean flag = sale();if(!flag){break;}}//end while}//end run
  • 如果有多个同步的方法,且多个方法都是this这把锁,则其中一个方法执行、其他方法无法执行

8.4 显式锁

显式锁使用Lock关键字

在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象

显式锁比隐式锁更好,更能体现锁的概念,体现了面向对象的机制

显式锁Lock的子类:ReentrantLock

代码示例:

  • 创建隐式锁
 Lock l = new ReentrantLock();
  • 在进行代码块前锁住
 l.lock();
  • 在代码块结束后开锁
 l.unlock();//代码执行完毕,开锁
  • 完整代码
    public static void main(String[] args) {//线程不安全//解决方案3:显式锁Lock//java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象Runnable runnable = new Ticket();//只有一个任务//启动三个线程,但使用的都是runnable对象,因此用的都是同一把锁lnew Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();}static class Ticket implements Runnable{//总票数private int count = 10;//创建显式锁lprivate Lock l = new ReentrantLock();@Overridepublic void run() {//每次被触发就进卖买票操作while(true){l.lock();//进入if之前,锁住if(count > 0){//卖票System.out.println("正在准备卖票,请稍等...");//try-catch使得卖票的时间更长try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count --;System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);}else{break;}l.unlock();//代码执行完毕,开锁}//end while}//end run}

不论是显式锁还是隐式锁,都可以有效地控制多线程获取资源、解决所出现的线程不安全问题

8.5公平锁与非公平锁

  • 公平锁:排队,先来先到,在Lock构造方法传入Boolean值True,则为公平锁

  • 非公平锁:抢,隐式锁Sync属于非公平锁,Lock默认为非公平锁
    实现公平锁:

  • 显式锁Lock的构造方法中,参数为True则表示公平锁

9. 线程死锁

  • 死锁:多个线程线程互相持有对方所需要的资源,多个线程因竞争资源而造成的一种僵局(互相等待)

死锁举例
拿生活中的场景并结合代码,举一个简单的栗子:

  • 挟持着人质的罪犯与警察两人僵持不下(警察抓着罪犯,而罪犯手上有人质)
    罪犯对警察说:“你放了我,我放人质!”
    然而警察听到后内心想:“我救人质,但是罪犯跑了”
    警察对罪犯说:“你放了人质,我放过你!”
    然而罪犯听到后内心想:“警察放过我,但是人质跑了”

根据这个场景,来进行代码的实现

  • 罪犯Culprit
  /*** 罪犯*/static class Culprit{//罪犯对警察说public synchronized void say(Police p){System.out.println("罪犯:你放了我,我放人质!");p.fun();}//听了警察的话,内心回应public synchronized void fun(){System.out.println("罪犯内心:警察放过我,但是人质跑了");}}
  • 警察Police
  /*** 警察*/static class Police{//警察对罪犯说public synchronized void say(Culprit c){System.out.println("警察:你放了人质,我放过你!");c.fun();}//听了罪犯的话,警察回应public synchronized void fun(){System.out.println("警察内心:我救人质,但是罪犯跑了");}
  • 新建线程MyThread,警察对罪犯说
static class MyThread extends Thread{private Culprit c;private Police p;//构造方法public  MyThread(Culprit c,Police p){this.c = c;this.p = p;}@Overridepublic void run() {/*** 警察say方法执行完之后,调用罪犯的fun方法,等待罪犯回应*/p.say(c);//警察说话,让罪犯回应}}
  • 新建主线程,罪犯对警察说
    public static void main(String[] args) throws InterruptedException {Culprit c = new Culprit();//新建一个罪犯对象Police p = new Police();//新建一个警察对象new MyThread(c,p).start();//新建线程:警察说话,让罪犯回应/*** 罪犯的say方法调用执行完后,调用警察的fun方法,等待警察回应*/c.say(p);//主线程:罪犯说话,让警察回应}

此时,有两个线程,而这两个线程中,警察和罪犯都说完了自己的话(执行say),等待对方回应(执行fun),然而等待对方回应前先必须等待对方把话说完(执行say),但是不知道对方有没有先说完(有没有执行完say),因此卡住了,造成了死锁

死锁的结果输出:

罪犯和警察说完之后都在等待对方回应,从而造成了死锁,程序卡在那无法继续进行,只能手动结束程序

罪犯:你放了我,我放人质!
警察:你放了人质,我放过你!罪犯:你放了我,我放人质!
警察内心:我救人质,但是罪犯跑了
警察:你放了人质,我放过你!
罪犯内心:警察释放我,但是人质跑了

死锁避免

  • 线程按照一定的顺序加锁)
    加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
    根源上解决: 在任何有可能产生锁的方法中,不调用另一个有可能产生锁的方法

10.多线程通信问题

  • 多线程通信问题,也就是生产者与消费者问题
  • 生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

以下为百度百科对于该问题的解释:

  • 生产者与消费者问题:
    生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

解决办法:

  • 要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
  • 厨师为生产者,服务员为消费者,假设只有一个盘子盛放食品。
    厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

  • 在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保了数据的安全

  • 定义厨师线程

 /*** 厨师,是一个线程*/static class Cook extends Thread{private Food f;public Cook(Food f){this.f = f;}//运行的线程,生成100道菜@Overridepublic void run() {for (int i = 0 ; i < 100; i ++){if(i % 2 == 0){f.setNameAneTaste("小米粥","没味道,不好吃");}else{f.setNameAneTaste("老北京鸡肉卷","甜辣味");}}}}
  • 定义服务员线程
/*** 服务员,是一个线程*/static class Waiter extends Thread{private Food f;public Waiter(Food f){this.f = f;}@Overridepublic void run() {for(int i =0 ; i < 100;i ++){//等待try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}f.get();}}//end run}//end waiter
  • 新建食物类
 /*** 食物,对象*/static class Food{private String name;private String taste;public void setNameAneTaste(String name,String taste){this.name = name;//加了这段之后,有可能这个地方的时间片更有可能被抢走,从而执行不了this.taste = tastetry {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}this.taste = taste;}//end setpublic void get(){System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);}}//end food
  • main方法中去调用两个线程
    public static void main(String[] args) {Food f = new Food();Cook c = new Cook(f);Waiter w = new Waiter(f);c.start();//厨师线程w.start();//服务生线程     }

运行结果:

只截取了一部分,我们可以看到,“小米粥”并没有每次都对应“没味道,不好吃”,“老北京鸡肉卷”也没有每次都对应“甜辣味”,而是一种错乱的对应关系

...
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

name和taste对应错乱的原因:

当厨师调用set方法时,刚设置完name,程序进行了休眠,此时服务员可能已经将食品端走了,而此时的taste是上一次运行时保留的taste。
两个线程一起运行时,由于使用抢占式调度模式,没有协调,因此出现了该现象

以上运行结果解释如图:

加入线程安全
针对上面的线程不安全问题,对厨师set和服务员get这两个线程都使用synchronized关键字,实现线程安全,即:当一个线程正在执行时,另外的线程不会执行,在后面排队等待当前的程序执行完后再执行

代码如下所示,分别给两个方法添加synchronized修饰符,以方法为单位进行加锁,实现线程安全

 /*** 食物,对象*/static class Food{private String name;private String taste;public synchronized void setNameAneTaste(String name,String taste){this.name = name;try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}this.taste = taste;}//end setpublic synchronized void get(){System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);}}//end food

输出结果:

由输出可见,又出现了新的问题:
虽然加入了线程安全,set和get方法不再像前面一样同时执行并且菜名和味道一一对应,但是set和get方法并没有交替执行(通俗地讲,不是厨师一做完服务员就端走),而是无序地执行(厨师有可能做完之后继续做,做好几道,服务员端好几次…无规律地做和端)

...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

实现生产者与消费者问题
由上面可知,加入线程安全依旧无法实现该问题。因此,要解决该问题,回到前面的引入部分,严格按照生产者与消费者问题中所说地去编写程序

生产者与消费者问题:
生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

  • 厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

  • 在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保数据的安全

  • 首先在Food类中加一个标记flag:
    True表示厨师生产,服务员休眠
    False表示服务员端菜,厨师休眠
 private boolean flag = true;
  • 对set方法进行修改
    当且仅当flag为True(True表示厨师生产,服务员休眠)时,才能进行做菜操作
    做菜结束时,将flag置为False(False表示服务员端菜,厨师休眠),这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
    然后唤醒在当前this下休眠的所有进程,而厨师线程进行休眠
     public synchronized void setNameAneTaste(String name,String taste){if(flag){//当标记为true时,表示厨师可以生产,该方法才执行this.name = name;try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}this.taste = taste;flag = false;//生产完之后,标记置为false,这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况this.notifyAll();//唤醒在当前this下休眠的所有进程try {this.wait();//此时厨师线程进行休眠} catch (InterruptedException e) {e.printStackTrace();}}}//end set
  • 对get方法进行修改
    当且仅当flag为False(False表示服务员端菜,厨师休眠)时,才能进行端菜操作
    端菜结束时,将flag置为True(True表示厨师生产,服务员休眠),这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师生产一份的情况
    然后唤醒在当前this下休眠的所有进程,而服务员线程进行休眠
        public synchronized void get(){if(!flag){//厨师休眠的时候,服务员开始端菜System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);flag = true;//端完之后,标记置为true,这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师只生产一份的情况this.notifyAll();//唤醒在当前this下休眠的所有进程try {this.wait();//此时服务员线程进行休眠} catch (InterruptedException e) {e.printStackTrace();}}// end if}//end get

作了以上调整之后的程序输出:

我们可以看到,没有出现数据错乱,并且菜的顺序是交替依次进行的

...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

11.线程池Executors

  • 池:容器的意思

使用一个线程通常要经过创建线程、创建任务、执行任务、关闭线程,在这个过程中,创建任务和执行任务的时间很少

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,浪费的时间多,因此o频繁创建线程o会大大降低系统的效率(频繁创建线程和销毁线程需要时间)

线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源

  • 作用
    降低资源消耗
    提高响应速度
    提高线程的可管理性

分类
不论是哪一类,获取线程池的对象都是ExecutorService

1 缓存线程池
长度没有限制

  • 创建缓存线程池:
    .newCachedThreadPool()
//创建缓存线程池ExecutorService service = Executors.newCachedThreadPool();
  • 向线程池中加入新的任务,指挥线程池执行新的任务(run):
//向线程池中加入新的任务,指挥线程池执行新的任务(run)service.execute(new Runnable() {//execute中传入任务对象即可@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});//向线程池中加入新的任务,指挥线程池执行新的任务(run)service.execute(new Runnable() {//execute中传入任务对象即可@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});//向线程池中加入新的任务,执行新的任务(run)service.execute(new Runnable() {//execute中传入任务对象即可@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});
  • 输出结果:
    由输出可知,三个线程名称为1,2,3
pool-1-thread-3任务执行
pool-1-thread-1任务执行
pool-1-thread-2任务执行

添加休眠时间,使程序休眠一段时间

 Thread.sleep(1000);//停一秒之后,再去执行线程,此时缓存线程池中已有内容,执行缓存池中的内容
  • 指挥线程池执行任务
    此时缓存池中已有内容,再去执行任务时,执行缓存池中空闲的任务
        //向线程池中加入任务,指挥线程池执行新的任务(run)service.execute(new Runnable() {//execute中传入任务对象即可@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});
  • 输出结果:

由输出结果可知,线程实现了重复使用,休眠后执行的任务是缓存池中已有的空闲任务3

pool-1-thread-1任务执行
pool-1-thread-3任务执行
pool-1-thread-2任务执行
pool-1-thread-3任务执行

2 定长线程池
相对于缓存线程池,长度有限制,线程池中的当前线程数目不会超过给定的长度

当该值为0的时候,意味着没有任何线程,线程池会终止

代码示例:

创建定长线程池,这里指定线程池大小为2
.newFixedThreadPool(参数),参数为线程池的长度

//创建定长线程池,指定了线程池的大小为2ExecutorService service = Executors.newFixedThreadPool(2);

向线程池中加入新的任务,指挥线程池执行任务
如下面代码所示,添加3个任务

     //向线程池中加入任务,指挥线程池执行新的任务(run)service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});

输出结果:

由于线程池长度为2,因此最多两个任务,线程池中的当前线程数目不会超过2

pool-1-thread-2任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

3 单线程线程池
与定长线程池中传入参数为1的作用相同,即线程池中只有一个线程

  • 创建单线程线程池
    .newSingleThreadExecutor()
        ExecutorService service = Executors.newSingleThreadExecutor();
  • 向线程池中加入新的任务,指挥线程池执行任务
 service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}});
  • 输出结果:
    由输出可知,线程池中只有一个线程
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

4 周期性任务定长线程池
为定长线程池

把一个任务定时在某个时期执行,或者是周期性执行

  • 任务在某个时期执行
    创建单线程线程池
    .newScheduledThreadPool(参数),参数为线程池的长度
        //创建 周期性任务定长线程池//任务创建出来的结果不一样ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
  • 向线程池中加入新的任务,指挥线程池执行任务
    .schedule(参数1,参数2,参数3)
    参数1:定时执行的任务
    参数2:表示时长的数字x(每隔x运行一次任务)
    参数3:时长数字的时间单位,由TimeUnit的常量制定
        /*** 定时执行一次* 参数1:定时执行的任务* 参数2:表示时长的数字x(每隔x运行一次任务)* 参数3:时长数字的时间单位,由TimeUnit的常量制定*/service.schedule(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}},5, TimeUnit.SECONDS);//任务在5秒钟后执行
  • 输出结果:
    5秒钟后输出
pool-1-thread-1任务执行

周期性执行

  • 创建单线程线程池
    .newScheduledThreadPool(参数),参数为线程池的长度
        //创建 周期性任务定长线程池//任务创建出来的结果不一样ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
  • 向线程池中加入新的任务,指挥线程池执行任务
    .schedule(参数1,参数2,参数3,参数4)
    参数1:定时执行的任务
    参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
    参数3:表示时长的数字x(每隔x运行一次任务)
    参数4:时长数字的时间单位,由TimeUnit的常量制定
       /*** 周期性执行* 参数1:任务* 参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)* 参数3:表示时长的数字x(每隔x运行一次任务)* 参数4:时长数字的时间单位,由TimeUnit的常量制定*/service.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "任务执行");}},5,1,TimeUnit.SECONDS);//5秒后执行,每隔1秒执行一次
  • 输出结果:
    5秒钟后开始输出,之后每隔1秒输出一次,直到停止程序
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
  • 无论是哪种线程池,使用完毕后必须手动关闭线程池,否则会一直在内存中存在

3.4 常用类库-多线程相关推荐

  1. java 常用类库_JAVA(三)JAVA常用类库/JAVA IO

    成鹏致远 |lcw.cnblog.com|2014-02-01 JAVA常用类库 1.StringBuffer StringBuffer是使用缓冲区的,本身也是操作字符串的,但是与String类不同, ...

  2. JAVA计时函数的库_JAVA开发常用类库UUID、Optional、ThreadLocal、TimerTask、Base64使用方法与实例详解...

    1.UUID类库 UUID 根据时间戳实现自动无重复字符串定义 // 获取UUID public static UUID randomUUID() // 根据字符串获取UUID public stat ...

  3. Java常用类库之String

    Java常用类库之String学习与积累 概述 在学习Java时,我们知道Java的基本数据类型有整型的int,byte,short,long,字符型的char,布尔型的Boolean和浮点型的flo ...

  4. Java常用类库API

    Java常用类库API 字符串操作 String类 String两种赋值方式 String类中的构造函数 String()方法 String(byte[] bytes)方法 String(byte[] ...

  5. Java常用类库以及简介,具体使用细节进行百度(爬虫爬取的数据)

    来至于互联网 Office文档的Java处理包 POI [推荐] Apache POI是一个开源的Java读写Excel.WORD等微软OLE2组件文档的项目.目前POI已经有了Ruby版本. 结构: ...

  6. 还在重复造轮子?Java开发人员必知必会的20种常用类库和API

    介绍 一个有经验的Java开发人员特征之一就是善于使用已有的轮子来造车.<Effective Java>的作者Joshua Bloch曾经说过:"建议使用现有的API来开发,而不 ...

  7. Java 必知必会的 20 种常用类库和 API

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:为什么程序员都不喜欢使用switch,而是大量的 if--else if ?个人原创+1博客:点击前往,查看更多 ...

  8. JavaSE——常用类库(String类)

    第1节 常用类库--String 因为String相对之前的类来说更加常用一些,所以对字符串类进行专门的整理. 1. 概述 String类表示字符串,Java中的所有字符串文字都实现为此类的实例. 字 ...

  9. JavaSE——常用类库(下)(Date、DateFormat、Calendar、System类)

    第1节 常用类库(下) 六.java.util.Date Date类表示特定的时刻,精度为毫秒. 在JDK 1.1之前, Date类还有两个附加功能. 它允许将日期解释为年,月,日,小时,分钟和秒值. ...

最新文章

  1. 【多线程高并发】深入理解JMM产生的三大问题【原子性、可见性、有序性】
  2. VTK:颜色边缘用法实战
  3. SQL Server:Like 通配符特殊用法:Escape
  4. 苹果邮箱收发件服务器
  5. websocket实现java服务端与js端通信
  6. python中求根公式_用python做个带GUI的求根公式吧
  7. Python进阶(一)Python中的内置函数、内置方法、私有属性和方法详解
  8. [SOA] Mule ESB 3.x 入门(二)—— 配置(spring, properties, log4j)
  9. 学习一种新编程语言要做的14个练习
  10. python爬取网页题库_用Python爬取本站离线题库
  11. KingabseES 锁机制
  12. C语言-输入任意多个数字,存到整型数组,支持任意间隔符,同时支持输入字母存到字符数组中
  13. sql to_char 日期转换字符串
  14. 这11个免费学习的网站,个个堪称神器,不收后悔!
  15. 脑机接口科普0003——Hans Berger
  16. python中计时工具timeit模块的基本用法
  17. SCI科技论文英语翻译的一点个人心得
  18. 微信后台架构浅析--读写扩散技术
  19. .flo光流文件转换为png图片
  20. 电影怎么转成gif动画?一分钟教你在线转gif动图

热门文章

  1. 少儿python编程课程大纲_1.Python编程-课程教学大纲.doc
  2. Win10磁盘占用100%解决方法
  3. 微信小程序地图组件和相机组件实现基于location的AR效果的尝试(失败)
  4. 深度学习mask掩码机制
  5. python怎么选取第几行第几列_python DataFrame获取行数、列数、索引及第几行第几列的值方法...
  6. 锁机制:读者写者问题 Linux C
  7. 乱码问题之文件,文本文件以及编码
  8. 有乳腺结节严不严重 乳房结节1cm要手术吗
  9. Py之py2neo:py2neo的简介、安装、使用方法之详细攻略
  10. android 短信转发设置权限,用Tasker实现Android手机短信转发到钉钉