记录《疯狂Java讲义精粹》划线内容及理解(第十一章 多线程)

第11章 多线程

11.1 线程概述

一个程序包含一个进程,一个进程包含多个线程

11.1.1 线程和进程

当一个程序进入内存运行时,即变为一个进程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

进程的特征

  • 独立性,进程是系统中独立存在的实体,可以拥有自己独立的资源,拥有自己私有的地址空间

  • 动态性,程序只是一个静态的指令集,进程是一个正在系统中活动的指令集,进程具有生命周期和状态,程序无

  • 并发性,多个进程可在单个处理器上并发执行,不会互相影响


并发(concurrency)和并行(parallel)

并发:在同一时刻只有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多进程同时执行的效果

并行:在同一时刻,有多条指令在多个处理器上同时执行

现在大部分系统基本都支持多进程并发运行,常用的策略有

  • 共用式的多任务操作策略

  • 抢占式多任务操作策略(效率高一点)


线程(Thread)也称为轻量级进程(Lightweight Process),线程是进程的组成部分,线程是进程的执行单元


线程的特点

  1. 每个线程独立的

  2. 可并发执行

  3. 进程初始化时,主线程被创建

  4. 线程拥有自己的堆栈,程序计数器和局部变量(不拥有系统资源)

  5. 多个线程共享父进程资源

  6. 线程之间相互隔离(不知道对方是否存在)

  7. 线程之间抢占运行

  8. 线程之间可互相创建和撤销


对多线程的调度和管理,以及资源分配,由进程本身负责

操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。


11.1.2 多线程的优势

与分隔的进程相比,进程中线程之间的隔离程度要小。它们共享内存、文件句柄和其他每个进程应有的状态。

线程比进程有更高的性能,因为多个线程共享一块内存区域,通信成本很小,

线程创建的成本低

因此,使用多线程来实现并发比使用多进程并发的性能要高


多线程编程的优点

  1. 共享内存

  2. 创建线程代价小

  3. java对多线程的支持

线程调度

  1. 分时调度

    • 所有线程轮流 使用cpu,每个线程平分cpu的时间片
  2. 抢占式调度(Java)

    • 让优先级高的线程首先使用cpu,如线程优先级相同,随机选择一个线程

11.2 线程的创建和启动

java使用Thread类来代表线程,所有线程对象都必须是Thread类或其子类的实例


11.2.1 继承Thread类创建线程类

通过继承Thread类来启动多线程

  1. 创建thread类的子类,并重写run()方法,该方法体代表了线程需要完成的任务,因此把run()也称为线程执行体
  2. 创建该子类实例(创建线程对象)
  3. 调用该实例的start()方法,启动线程

注意,如果不适用start()方法,而是直接调用run(),那么其他线程,在run()方法返回之前无法并发执行,系统会将run()方法当作一个普通方法来执行


Thread类的方法:

  • Thread.currentThread():返回当前正在执行的线程对象

  • getName():返回调用该方法的线程名字

可以通过setName(字符串)来设置线程的名字

通过getName()来取得指定线程的名字

如果继承Thread,直接调用run()方法,那么this.getName()方法返回的该对象的名字

Thread.currentThread().getName()总是返回当前线程的名字

默认情况下

  • 主线程名为main

  • 用户的多个线程名字依次为Thread-0,Thread-1……

通过继承Thread来实现多线程时,多个线程之间无法共享线程类的实例变量


11.2.2 实现Runnable接口创建线程类

实现Runnable接口来实现多线程,步骤如下:

  1. 定义Runnable的实现类,重写run()方法

  2. 创建该实现类的实例对象,用该对象作为构造器形参,来创建Thread对象,

    //创建实现类对象
    Runnableimpl  op=new  Runnableimpl();
    //创建Thread对象
    //将Runnable实现类对象作为形参传入
    //并且可以为新线程指定名字
    Thread  o1=new Thread(op,"线程1");
    
  3. 调用对象的start()方法来启动线程

    说明:实际执行时,还是需要执行Thread对象的run方法,只是该方法调用了Runnable实现类的run方法,但使用这种方法,能使多线程共享一个线程类的实例属性


11.2.3 使用Callable和Future创建线程

java在JDK1.5时,受倒c#启发(c#可以将任意方法包装为线程执行体),提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体

  • call()方法可以有返回值(该返回值和接口泛型,类型相同)

  • call()方法可以声明抛出异常

    ​ java在JDK1.5时,提供了Future接口,该接口代表了Callable接口中 call 方法的返回值,该接口有实现类FutureTask,该实现类实现了Tutrre接口和Runnable接口,因此,这个实现类FutureTask可以作为Thread构造器的形参


    Future接口中定义了公共方法来控制它关联的Callable任务

  • boolean cancel(boolean may):试图取消该Future里关联的Callable任务

  • V get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞

  • V get(long timeout,TimeUnit unit):返回Callable任务里call()的返回值,该方法最多让程序阻塞timeout和unit指定的时间,指定时间过后Callable还是没有返回值,将抛出TimeoutException异常

  • boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true

  • trueboolean isDone():如果Callable任务已完成,则返回true


    使用Callable的步骤

    1. 创建Callable的实现类,重写call()方法

    2. 创建Callable实现类实例,用FutureTask包装该实例(该类封装了Callable对象call()方法的返回值)

    3. 使用FutureTask对象作为Thread构造器的形参,启动线程

    4. 调用FutureTask实例的get()方法来获得子线程执行结束后的返回值

    public class ThirdThread implements Callable<Integer>{  
     // 实现call()方法,作为线程执行体  
     public Integer call(){  
     int i = 0;  
     for ( ; i < 100 ; i++ ){ System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);  
     }  
     // call()方法可以有返回值  
     return i;  
     }  
     public static void main(String[] args){  
     // 创建Callable对象  
     ThirdThread rt = new ThirdThread();  
     // 使用FutureTask来包装Callable对象  
     FutureTask<Integer> task = new FutureTask<Integer>(rt);  
     for (int i = 0 ; i < 100 ; i++){  
     System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);  
     if (i == 20){  
     // 实质还是以Callable对象来创建并启动线程  
     new Thread(task , "有返回值的线程").start();  
     } }try{  
     // 获取线程返回值  
     System.out.println("子线程的返回值:" + task.get());  
     }catch (Exception ex){  
     ex.printStackTrace();  
     }  
     }  
    }
    

11.2.4 创建线程的三种方式对比

Runnable接口和Callable接口(可以有返回值)的优劣:

好处:

  1. 线程类不止实现了上述两个接口,还可以继承其他类
  2. 可以共享一个Thread对象,所以可以使多个线程来处理同一份资源

坏处:

  1. 使编程复杂,要访问当前线程,必须使用Thread.currentThread()方法

继承Thread实现多线程的优劣

好处:编写简单,访问当前线程可直接使用this关键字

坏处:继承了Thread类,不能继承别的类


11.3 线程的生命周期

线程生命周期有:

  1. 新建(New)
  2. 就绪(Runnable)
  3. 运行(Running)
  4. 阻塞(Blocked)
  5. 死亡(Dead)

在一个线程的生命周期内,会多次在运行和阻塞之间切换


11.3.1 新建和就绪状态

新建(New):当使用new关键字创建一个线程后,JVM会为其分配内存,初始化成员变量,该线程处于新建状态

就绪(Runnable):当线程对象调用start()方法后,JVM会为其创建方法调用栈,创建程序计数器,这时线程还没有运行,具体运行时间要等JVM,里的线程调度器调度,这时线程处于就绪阶段

如果直接调用run()方法,则线程不再处于新建状态,会被当作普通方法执行

只能对处于新建状态的线程,调用start()方法,否则将引发lllegalThreadStatrException(指示线程没有处于请求操作所要求的适当状态)

如果想让子线程在,它的start()方法被调用后,马上执行,可以执行Thread.sleep(1),让当前线程(主线程)睡眠一毫秒,这时子线程会马上抢占CPU,子线程开始执行


11.3.2 运行和阻塞状态

对于CPU的调度

  • PC使用抢占式调度策略
    • 系统会将cpu的时间分片,一个线程执行完一个时间片后,系统会剥夺该线程所占用的资源,其他线程开始抢占下一个时间片,在这时,系统也会考虑线程的优先级
  • PE使用协作模式
    • 线程主动调用Sleep()或yield(),放弃它所占的资源

线程进入阻塞状态

  • 线程调用sleep()方法,主动放弃所占资源
  • 线程调用了一个阻塞IO方法,在该方法返回前,线程被阻塞
  • 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有
  • 线程在等待某个通知(notify)
  • 程序挂起了线程(调用了suspend()方法,该方法会导致死锁,谨慎使用)

解除阻塞

  • 调用sleep()方法的线程,已经经过了指定时间
  • 阻塞IO方法已返回
  • 线程成功获得了同步监视器
  • 等待通知的线程,得到了通知
  • 被挂起的线程,调用了resume()恢复方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bWcNvkSI-1661949179367)(D:\学习相关\images\线程生命周期.jpg)]

线程在就绪和运行状态之间的转换不受程序控制,但有一个方法例外,就是调用yield()方法,可以让运行状态的线程转入就绪状态


11.3.3 线程死亡

线程结束方式

  1. run()或call()方法执行完毕,正常结束

  2. 线程抛出一个未捕获的异常Exception或Error

  3. 调用线程的stop()方法来结束线程(会导致死锁,谨慎使用)

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响


线程状态测试方法

isAlive()

    返回True——线程处于就绪,运行,阻塞状态时返回

    返回False——线程处于新建,死亡状态时返回

对死亡的线程调用start()方法,将引发lllegalThreadState Exception(非法的线程状态异常)异常


11.4 控制线程

11.4.1 join线程

join()方法,让一个线程等待另一个线程执行完毕

当一个线程调用了其他线程的join()方法后,主调线程将被阻塞,直到被调线程执行完毕

join方法

  • join() 等待被join的线程执行完毕

  • join(long millis) 等待被join的线程,最长millis毫秒,如果在这个时间内线程还没有执行结束,则不再等待

  • join(long millis,int nanos) 等待被join的线程最多millis毫秒加nanos毫微秒(毫微秒一般不常用,因为太小了,计算机硬件,操作系统都无法精确到毫微秒)

该方法需要理清楚调用join()后,谁等待,谁运行


11.4.2 后台线程

后台线程(守护线程/精灵线程,我喜欢这个名字): 在后台运行,它的任务是为其他的线程提供服务

JVM的垃圾回收线程就是典型的守护线程

特征:如果所有的前台的线程都死亡,后台线程会自动死亡

设置后台线程:调用Thread对象的setDaemon(true)

//格式
线程名.setDaemon(true

判断线程是否为后台线程:isDaemon()


一些需要知道的点

  1. 主线程默认是前台线程

         前台线程创建的线程,默认为前台线程
    
         后台线程创建的线程,默认是后台线程
    
  2. 在JVM通知后台线程死亡时,后台线程需要一定的时间响应

  3. 设置某个线程为后台线程时,需要在该线程启动之前,否则,将引发IllegalThreadStateException(非法的线程状态)异常


11.4.3 线程睡眠:sleep

sleep()方法,将当前正在执行的线程暂停一段时间,并进入阻塞状态,该方法是Thread类的静态方法

重载sleep

  • static void sleep(long millis)让当前线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器线程调度器的精度和准确度的影响

  • static void sleep(long millis,int nanos)让当前线程暂停millis毫秒加nanos毫微秒,并进入阻塞

在线程睡眠的时间段内,该方法不会获得执行机会,即使CPU空闲(举例子说明,不会出现这种情况)


11.4.4 线程让步(暂停):yield

Thread类的静态方法yield(),让当前正在执行的线程暂停,它不会阻塞线程,只是让该线程转入就绪状态

实际上,该方法只会让线程暂停,但是暂停后,只有优先级与该线程相同,或更高,才能获得执行的机会


sleep和yield方法的区别

  1. sleep方法给其他线程运行机会时,不理会其他线程的优先级,yield方法,只给优先级相同,或更高的线程运行机会

  2. sleep会将线程转入阻塞,并且限定了阻塞时间,yield将线程强制转为就绪状态(所以完全有可能在yield(暂停)之后,马上又被运行)

  3. sleep会抛出InterruptedException(中断异常),yield没有声明抛出任何异常

  4. sleep具有更好的移植性,


11.4.5 改变线程优先级

每个线程都具有一定的优先级,默认情况下,main线程具有普通优先级,每个被创建的线程,拥有与它父线程相同的优先级

高优先级的线程,有更多被执行的机会

修改线程优先级

  • setPriority(int newPriority)

    • 设置线程优先级,范围1-10之间,该方法可以使用Thread类的三个静态常量

      • MAX_PRIORITY:值为10

      • MIN_PRIORITY:值为1

      • NORM_PRIORITY:值为5

  • getPriority()

    • 获取线程的优先级
//改变主线程的优先级
Thread.currentThread().setPriority(整数)
//改变子线程的优先级,该操作可在线程启动后执行
线程名.setPriority(整数)

不同操作系统对线程优先级的支持不一样,所以尽量使用Thread的常量来为线程赋优先级,这样具有更好的移植性


11.5 线程同步

当使用多线程来访问同一个数据,很容易“偶然”出现线程的安全问题

1.5.1 线程安全问题

这里涉及一个非常经典的线程同步案例,银行取钱案例

两个人对同一账户执行取钱操作,假设在A取钱后,到账户扣钱之间的这段时间内,B又取钱(可以取到,因为扣钱操作还未发生),则会发生错误


11.5.2 同步代码块(加锁)

在java的多线程中,引入了同步监视器(线程锁)来解决多线程带来的问题,

同步监视器

//锁对象
private static Object obj=new Object();

//obj就是同步监视器(或者说锁对象,该锁对象是任意的,只需保证多个线程拿到的是同一把就可)
//线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
synchronized(obj){


//同步代码块
……    

}

同步监视器的目的:阻止两个线程对同一个共享资源的并发访问


同一时刻,只能有一个线程,可以获得对同步监视器的锁定,当同步代码块执行完毕后,该线程会释放同步监视器的锁定

obj意味着被加锁的资源,在同步代码块内,被加锁的资源不能被别的线程访问,直到同步代码块结束


11.5.3 同步方法

使用synchronized关键字修饰的方法,称为同步方法

在书中只说了同步方法的锁对象为this,而没有分静态非静态,应该是一个疏漏

静态/非静态同步方法的锁对象

  • 静态方法的锁对象是,类名.class(表示的是当前类对应的字节码文件对象)

  • 非静态方法的锁对象是,this(当前类的一个对象)

线程安全类的特点

  • 该类的对象可以被多线线程安全的访问

  • 每个线程调用该对象的任意方法,都将得到正确的结果

  • 每个线程调用该对象的任意方法,该对象仍处在合理状态

面向对象内的设计方法:Domain Driven Design(领域驱动设计,DDD)

java中的不可变类不存在线程竞争问题,而可变类存在

可变类牺牲了部分运行效率,来保证线程安全

应该为一个类提供两种环境的类,单线程和多线程

如JDK提供的StringBuilder在单线程时使用

StringBuffer在多线程使用


11.5.4 释放同步监视器的锁定(解锁)

在进入同步代码块之前,需要先获得对同步监视器的锁定,但释放时,程序无法显示的释放对同步监视器的锁定

以下情况,线程将解锁(释放同步监视器)

  • 当前线程在同步方法/同步代码块执行结束,线程释放同步监视器

  • 当前线程在同步代码块,同步方法中遇到了break,return终止了该代码块或同步方法,当前线程会释放同步监视器

  • 当前线程在同步代码块内/同步方法内,出现了未处理的Error或Exception,导致该代码块异常结束,线程释放同步监视器

  • 当前线程在同步代码块/同步方法内,程序执行了同步监视器对象的wait()方法,当前线程暂停,并释放同步监视器

以下情况,线程将不会解锁

  • 线程执行同步代码块时,程序调用了Thread.sleep()睡眠方法,Thread.yield()暂停方法

  • 线程执行同步代码块时,其他线程将该线程挂起(调用了该线程的suspend()方法)


11.5.5 同步锁(Lock)

java5提供的线程同步机制——通过显示定义同步锁对象,来实现同步,同步锁就是Lock对象

Lock实现允许更灵活的结构,可以具有差别很大的属性,支持多个相关的Condition对象

Lock接口的实现类ReentrantLock(可重入锁)

ReadWriteLock(读写锁)接口的实现类ReentrantReadWriteLock,该实现类允许对共享资源的并发访问

使用Lock

//在一个类中定义锁对象
private final ReentrantLock  lock = new  ReentrantLock();

//在一个需要保持同步的方法内,加锁
lock.lock();
……
//同步代码

//使用完毕之后在finally块中释放锁
finally{
    lock.unlock();
}

我的问题

  1. 是否可以将lock锁放到增强try语句的括号中,使它在try语句结束之后自动释放锁呢,

    • 查了API,发现ReentrantLock类并没有实现Autocloseable或Closeable接口,所以直接放肯定时不行的,如果继承然后重写该类呢?
  2. lock解锁操作为什么要放在finally中呢?

    • 原因是如果加锁和解锁中间出现了异常,会导致unlock()操作无法执行,导致死锁,所以放在finally块中,避免死锁问题
  3. 加锁操作为什么要放在try块外?

    • 考虑效率缘故,如果让在try块内,加锁产生异常时,解锁也会产生异常,但如果放在try块,就算加锁异常,解锁不会触发异常

当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。(就是释放锁时,要在加锁的作用域内)

Lock提供的方法

  • 用于非块结构的tryLock()

  • 视图获取可中断锁的lockInterruptibly()

  • 获取超时失效锁的tryLock(long TimeUnit)

ReentrantLock锁称为可重入锁,他具体表现在,一个线程可以对,已被加锁的ReentrantLock锁,再次加锁

ReentrantLock对象会维护一个计数器,来追踪lock()方法的嵌套调用,线程在每次调用lock()后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码,可以调用另一个被相同锁保护的方法


11.5.6 死锁

当两个线程,相互等待对方释放手里的资源时,就会发生死锁

java对死锁没有检测,也没有对应的处理措施,一但出现死锁,程序不会有任何异常,也没有任何提示,只是所有线程都处于阻塞状态,无法继续

这种错误是一种逻辑错误,应该在编程阶段避免这种错误

这里有一个非常经典的死锁案例,如有需要可以翻看这一小节


11.6 线程通信

因为线程的调度由JVM的线程调度器操作,具有一定的透明性,程序通常无法准确控制线程的轮转(就如同无法控制GC一样)


11.6.1 传统的线程通信

为了实现程序对线程轮转的控制,Object类提供了一些方法,但这些方法要由同步监视器对象来调用,所以以下三个方法,只有在使用synchronized来实现同步时,才能使用

使用情况

  • synchronized修饰的同步方法,因为该类默认实例,就是同步监视器,所以可以在该方法中,直接调用这三个方法

  • synchronized修饰的同步代码块,同步监视器是synchronized后,括号里的对象,要用该对象来调用这三个方法

Object类的线程协调方法

  • wait()让调用该方法的线程等待,并释放对该同步监视器的锁定(解锁),其他线程可调用,该同步监视器的notify()方法,或notifyAlly()方法来唤醒该线程

    • 无参,一直等待被唤醒

    • 毫秒,等待指定毫秒值后,自动苏醒

    • 毫秒,毫微秒,等待指定时间后,自动苏醒

  • notify()唤醒此同步监视器上,等待的单个线程,如果有多个线程在此同步监视器上等待,则任意选择其中一个线程唤醒(请注意此处的任意选择一个线程被唤醒,后面要讨论)

    • 只有当前线程放弃对该同步监视器的锁定后,也就是使用了wati()方法后,才可以执行被唤醒的线程
  • notifyAll()唤醒此同步监视器上,等待的所有线程

    • 只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程

notify方法在唤醒线程时,是从所有线程中随机唤醒一个线程,但这里的唤醒,其实并不是完全随机,这个随机,具体依赖JVM的实现,而hotspot,是对第一个元素进行出队操作,是遵循”先进先出“原则

具体参考:notify方法的随机唤醒


11.6.2 使用Condition控制线程通信

如果使用了Lock对象来保证线程同步,则不能使用wait(),notify()和notifyAll()方法来控制线程,要使用Condition类

Condition对象可让线程释放Lock对象,也可唤醒处于等待的线程

获得Condition实例

Condition实例被绑定再一个Lock对象上,要获得该实例,要调用Lock对象的newCondition()方法

Condition的方法

  • await():类似wait()方法,可使当前线程等待,该方法呦很多变体,如awaitNanos,awaitUninterruptibly,awaitUntil等

  • signal():唤醒此Lock对象上等待的单个线程,如有多个线程等待,随机唤醒其中一个

  • signalAll():唤醒在此Lock对象上等待的所有线程


11.6.3 使用阻塞队列(BlockingQueue)控制线程通信

Java5提供了一个BlockingQueue接口,该接口是Queue的子接口,该接口的主要作用是作为线程同步工具

BlockingQueue的特点

当生产者向BlockingQueue放入元素时,如果队列已满,则该线程被阻塞,

当消费者视图同BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞

BlockingQueue的阻塞方法

  • put(E e):将E元素放入BlockingQueue中,如队列元素已满,则阻塞该线程

  • take():尝试从BlockingQueue的头部取出元素,如队列已空,则阻塞该线程

因为该接口继承了Queue接口,所以也可以使用Queue接口中的方法

  1. 在队尾插入元素

    • 以下方法,当队列已满时

    • add(E e) 该方法会抛出异常

    • offer(E e) 返回false

    • put(E e)阻塞队列

  2. 在队列头删除并返回元素

    • 以下方法,当队列已空时,

    • remove() 抛出异常

    • poll() 返回false

    • take() 阻塞队列

  3. 从队列头取出但不删除元素

    • 以下方法,当队列已空时

    • element() 抛出异常

    • peek() 返回false

BlockingQueue的实现类

  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列

  • LinkedBlockingQueue:基于链表实现的BlockingQueue队列

  • PriorityBlockingQueue:该类取出元素时,并不是遵循队列的规则,而是每次取出元素值最小的,该大小按照自然排序,或比较器排序(定制排序)

  • SynchronousQueue:同步队列,该队列的存取操作必须交替进行

  • DelayQueue:该类底层基于PriorityBlockingQueue实现,该类要求集合元素实现Delay接口,该类根据元素的getDalay()方法的返回值排序(这个方法就是Delay接口中唯一的方法)

package java相关小实验;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 一些声明信息
 *
 * @author 谢柯
 * @date: 2022/8/16 20:32
 * @description:  使用BlockingQueue阻塞队列,来实现线程通信(生产者消费者)
 * @since JDK 1.8
 */


class Producer extends Thread {
    private BlockingQueue<String> bq;

    public Producer(BlockingQueue<String> bq) {
        this.bq = bq;
    }

    public void run() {
        String[] strArr = new String[]
                {       "Java",
                        "Struts",
                        "Spring"};

        for (int i = 0; i < 999999999; i++) {

            System.out.println(getName() + "生产者准备生产集合元素!");

            try {

                Thread.sleep(200);

                // 尝试放入元素,如果队列已满,则线程被阻塞
                bq.put(strArr[i % 3]);

            } catch (Exception ex) {

                ex.printStackTrace();
            }

            System.out.println(getName() + "生产完成:" + bq);
        }
    }
}

class Consumer extends Thread {
    private BlockingQueue<String> bq;

    public Consumer(BlockingQueue<String> bq) {
        this.bq = bq;
    }

    public void run() {
        while (true) {

            System.out.println(getName() + "消费者准备消费集合元素!");
            try {

                Thread.sleep(200);

                // 尝试取出元素,如果队列已空,则线程被阻塞
                bq.take();

            } catch (Exception ex) {

                ex.printStackTrace();
            }

            System.out.println(getName() + "消费完成:" + bq);
        }
    }
}

public class BlockingQueueTest2 {
    public static void main(String[] args) {


        // 创建一个容量为1的BlockingQueue
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);

        // 启动3个生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();

        // 启动一个消费者线程
        new Consumer(bq).start();
    }
}

11.7 线程组和未处理的异常

java使用ThreadGroup来表示线程组,可以对一批线程进行分类管理,java允许程序直接对线程组进行控制,对线程组的控制,相当于同时控制这批线程

程序创建的线程,属于默认的线程组,子线程默认,和创建它的父线程处在同一组内

一旦线程加入某个指定的线程组后,将不能更改它所属的线程组

Thread类的构造器,创建线程,指定线程组

  • Thread(ThreadGroup group,Runable target):以target的run()方法,作为线程执行体,创建新线程,属于group线程组

  • Thread(ThreadGroup group,Runnable target,String name):以target的run()方法作为线程执行体,创建新线程,该线程属于group线程组,线程名为name

  • Thread(ThreadGroup group,String name):创建新线程,属于group线程组

获取线程所属线程组

  • getThreadGroup()

该方法会返回一个ThreadGroup对象,表示一个线程组

创建线程组(创建ThreadGroup实例)

  • ThreadGroup(String name):创建线程组,指定线程组名

  • ThreadGroup(ThreadGroup parent,String name):以指定名字,指定父线程组,创建一个新的线程组

操作线程组内的线程

  • int activeCount():返回此线程组中,活动的线程数目

  • interrupt():中断此线程组中的所有线程

  • isDaemon():判断该线程组是否是后台线程组

  • setDaemon(boolean daemon):把该线程组设置为后台线程组,当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,该线程组将自动销毁

  • setMaxPriority(int pri):设置线程组饿最高优先级

  • uncaughtException(Thread t,Throwable e):该方法可以处理线程组内,任意线程所抛出的未处理的异常(该方法是,Thread的内部静态接口,Thread.UncaughtExceptionHandler的抽象方法,也就是说,ThreadGroup实现了该接口)


从java5开始,java加强了线程的异常处理,如果线程执行过程中,抛出了一个未处理的异常,JVM会在结束该线程之前,会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到了,会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常


Thread.UncaughtExceptionHandler是Thread类的一个静态内接口,该接口内只有一个方法

  • void uncaughtException(Thread t,Throwable e)

    t 代表出现异常的线程,e代表该线程抛出的异常

Thread类,设置异常处理器的方法

  • setDefaultUncaughtExceptionHandler(Thread.UncaughtException eh):为该线程类的所有线程实例,设置默认的异常处理器,(默认处理器2)

  • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为指定的线程实例,设置异常处理器(默认处理器1)

线程异常处理流程

当一个线程抛出未处理的异常时

  1. JVM查找对应的异常处理器(默认处理器1),如没有,向下走

  2. JVM调用该线程所属线程组的方法来处理(unacughtException()方法设置的组默认处理器)

    1. 如该线程有父类线程组,调用父类线程组的方法来处理(父类线程组的,组默认处理器)

    2. 找该线程实例所属的线程类的默认异常处理器(默认处理器2)

    3. 如该异常时ThreadDeath的对象,不做任何处理,否则,打印异常跟踪栈,结束线程

需要说明的时,异常处理器不同于try……catch,异常处理器在处理完异常之后,该异常还是会传播给调用者


11.8 线程池

线程池的好处

  1. 系统创建一个新线程的成本比较高,所以可使用线程池来提高性能

  2. 控制系统中,并发线程的数量,因为线程池规定了最大线程数

线程池在程序启动时,不创建线程,而是程序将一个Runnable对象或Callable对象传递给线程池时,线程池才会创建线程,当run()方法或call()方法执行结束后,该线程不会死亡,而是再次返回线程池中,称为空闲状态,等待下一个Runnable对象或Callable对象(也就是下一个任务)


11.8.1 Java 5实现的线程池

java新增了一个Executors工厂,来产生线程池

以下方法返回一个ExecutorService对象

  • newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程

  • newFixedThreadPool(int):创建一个线程数固定的线程池

  • newSingleThreadExecutor():创建一个只有单线程的线程池

ExecutorService对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程,该线程池代表尽快执行线程,程序只要将一个Runnable对象或Callable对象提交给线程池,该线程池就会尽快执行该任务

ExecutorService提供的方法

  • Future<?> submit(Runnable task)

    • 将一个Runnable对象提交给线程池,线程池将在有空闲线程时,执行该线程任务,其中Future对象代表Runnable任务的返回值,又因为Runnable的run()方法没有返回值,所以该Future对象将在run()方法执行结束后,返回null
  • Future submit(Runnable task,T result)

    • 将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时,执行Runnable对象代表的任务,result显式指定线程执行结束后的返回值,所以Future对象将在run()方法执行结束后,返回result
  • Future submit(Callable task)

    • 将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里call()方法的返回值。

以下方法返回一个ScheduledExecutorService线程池

  • newScheduledThreadPool(int):创建一个具有指定线程数的线程池,它可以在指定延迟后,执行线程任务,方法参数为线程池中所保存的线程数

  • newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后,执行线程任务

ScheduleExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后,或周期性的,执行线程任务


ScheduledExecutorService 提供的方法

  • ScheduledFuture schedule(Callable callable,long delay, TimeUnit unit)

    • 指定callable任务将在delay延迟后执行
  • ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit)

    • 指定command任务将在delay延迟后执行
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)

    • 指定command任务将在delay延迟后执行,而且以设定频率重复执行,TimeUnit为时间单位

    • 在initialDelay后开始执行,依次在initialDelay+period,initialDelay+2*period……处重复执行

  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)

    • 创建并执行一个在给定初始延迟后,首次启用的定期操作,随后的每一次执行终止,和下一次执行开始之间,都会存在给定的延迟。如果任务在任一次执行时遇到异常,将会取消后续执行,否则,只能通过程序来显式取消或终止该任务

关闭线程池

  • shutdown():当前线程池不再接收新任务,但会将以前所有已提交的任务执行完成,所有任务完成之后,池中的所有线程都会死亡

  • shutdownNow():该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表


11.8.2 Java 7新增的ForkJoinPool

java7提供了ForkJoinPool,来支持将一个任务拆分成多个”小任务“并行计算(需要cpu为多核),再把多个”小任务“的结果合并为总的计算结果。

ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池

ForkJoinPool的构造方法

  • ForkJoinPool(int):创建一个指定数量线程并行的线程池

  • ForkJoinPool():以Runntime.availableProcessors()方法的返回值作为参数,创建一个ForkJoinPool

加载线程任务

  • submit(ForkJoinTask)

  • incoke(ForkJoinTask)

    • 该方法中的ForkJoinTask表示一个可以并行,合并的任务

ForkJoinTask是一个抽象类,它有两个抽象子类

  1. RecursiveAction:无返回值的任务

  2. RecursiveTask:有返回值的任务


以下内容书上没有

自定义线程池

public ThreadPoolExecutor(

int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler)



参数解释🌈

corePoolSize——核心线程最大值,不能小于0

maximumPoolSize——最大线程数,不能小于等于0 maximumPoolSize>=corePoolSize

keepAliveTime——空闲线程最大存活时间,不能小于0

unit——时间单位

workQueue——任务队列,不能为null

threadFactory——创建线程工厂,不能为null

handler——任务拒绝策略,不能为null


参数解释

  • unit时间单位在TimeUnit类中定义了常量,直接用这些值就可

  • 任务队列也就是阻塞队列,需要用该接口的实现类,也就是

ArrayBlockkingQueue和LinkedBlockingQueue等

  • 线程工厂,Executors的defaultThreadFactory方法

  • 拒绝策略有四种,是ThreadPoolExecutor的内部类,默认为AbortPolicy()

拒绝策略

  • AbortPolicy —— 丢弃任务并抛出RehectedExecutionException异常,默认

  • DiscardPolicy —— 丢弃任务,不抛出异常

  • DiscardLdestPolicy —— 抛弃 队列中等待最久的任务,然后将当前任务加入队列

  • CallerRunaPolicy —— 调用任务的run()方法,绕过线程池直接执行(直接执行run()方法)

该方法最大能处理线程数为,maximumPoolSize+workQueue


使用该方法更容易对线程池控制,调优,并且该方法在实际开发中更常用


11.9 线程相关类

java提供了ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中,可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题


11.9.1 ThreadLocal类

ThreadLocal,是Thread Local Variable(线程局部变量),该类为每个使用此变量的线程都提供了一个变量值的副本,每个线程独立的修改自己的副本,从而避免冲突问题

ThreadLocal的方法

  • T get():获取线程副本的值

  • void remove():删除此线程局部变量中,当前线程的值

  • void set(T value):设置副本值

使用场景

ThreaLocal隔离了多个线程的数据贡献,避免了多个线程对共享资源的竞争(所以也就不需要同步了),所以如果仅需要隔离多个线程之间的共享冲突,可以考虑使用ThreadLocal

同步机制是为了同步多个线程对相同资源的并发访问,如果多个线程之间需要共享资源,以达到线程之间通信,就要考虑同步机制


11.9.2 包装线程不安全的集合

使用Collections的方法,将集合包装成线程安全的集合,从而保证多线程访问同一集合时,该集合数据的完整性

Collections的静态方法

  • Collection synchronizedCollection(Collection c):返回指定collection对应的,线程安全的collection

  • static List synchronizedList(List list):返回指定List对象,对应的线程安全的版本

  • static<K,V> Map<K,V> synchronizedMap(Map<K,V>m):返回指定Map对象对应的线程安全的Map对象。

  • static Set synchronizedSet(Set s):返回指定Set对象对应的线程安全的Set对象。

  • static<K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定SortedMap对象对应的线程安全的SortedMap对象。

  • static SortedSet synchronizedSortedSet(SortedSet s):返回指定SortedSet对象对应的线程安全的SortedSet对象

如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装


11.9.3 线程安全的集合类

从java5开始,java在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类,分为两类

  • Concurrent开头的集合(支持并发访问)
    • ConcurrentHashMap
    • ConcurrentSkipListMap
    • ConcurrentSkipSet
    • ConcurrentLinkedQueue
    • ConcurrentLikedDeque
  • CopyOnWrite开头的集合
    • CopyOnWriteArrayList(读快,写慢)
    • CopyOnWriteArraySet

Concurrent开头的类支持并发访问集合,写入的线程的所有操作都是安全的,读取操作不必锁定,以Concurrent开头的集合类,采用了更加复杂的算法来保证,永远不会锁住整个集合


Concurrent类集合的特点

  • 当多线程共享访问一个集合时,可使用ConcurrentLinkedQueue,该集合不允许使用null元素,实现了多线程高效访问,多个线程访问时,无需等待
  • 当多线程写入时,可用ConcurrentHashMap,该集合支持16个线程并发写入,可通过设置 concurrentcyLevel 构造参数,来支持更多的并发写入线程
  • ConcurrentLinkedQueue和ConcurrentHashMap支持多线程并发访问,如果在获取了这两个集合的迭代器后,对集合元素修改,则不会抛出任何异常,迭代器也无法反应,创建迭代器之后所作的修改

Copy类集合特点

  • CopyOnWriteArraySet的底层封装了CopyOnWriteArrayList
  • CopyOnWriteArrayList采用复制底层数组的方式,来实现写操作
  • CopyOnWriteArrayList
    • 读会直接读集合本身,无需加锁与阻塞
    • 写时,会在底层复制一份当前数组,作为副本,接下来对副本执行写入操作,所以线程安全t

也是因为CopyOnWriteArrayList执行写时,要频繁的复制数组,所以性能较差,但读操作不需要加锁,所以读操作就很快,还安全,所以该集合适合用在缓存等,这种读操作>写操作的场景


后言

我突然有些慰藉,我身边竟然已经有了这么多朋友,他们走到了好远的地方,我们却还保持着联系,是否说明,我也走了很远的路才到达这里,今天下午梦见了我的一位朋友,种种原因我们不再熟络,我常常想,如果当初的那个选择,选另一条路现在会是什么样,现在的我有很多的遗憾,也怀有很多的希望,恐惧,和快乐,迷雾始终围绕在我身边,未来的道路也不知在何方,只是,不敢奢求,也不甘如此,我该如何认识世界,世界又如何对待我,
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值