编程知识 cdmana.com

《JAVA编程思想》学习笔记:第21章(并发)

目录
Java编程思想(一)第1~4章:概述
Java编程思想(二)第5章:初始化和清理
Java编程思想(三)第6章:访问权限
Java编程思想(四)第7章:复用类
Java编程思想(五)第8章:多态
Java编程思想(六)第9章:接口
Java编程思想(七)第10章:内部类
Java编程思想(八)第11章:持有对象
Java编程思想(九)第12章:异常
Java编程思想(十)第13章:字符串
Java编程思想(十一)第14章:类型信息
Java编程思想(十二)第15章:泛型
Java编程思想(十三)第16章:数组
Java编程思想(十四)第17章:深入研究容器
Java编程思想(十五)第18章:Java I/O系统
Java编程思想(十六)第19章:枚举
Java编程思想(十七)第20章:注解
Java编程思想(十八)第21章:并发

第二十章、并发

目录

21.1 并发的多面性

21.2 基本的线程机制

21.3 共享受限资源(并发领域问题)

21.4 终结任务

21.5 线程之间的协作

21.6 死锁

21.7 新类库中的构件

21.8 仿真

21.9 性能调优(Performance Tuning)


前言

  • 顺序编程:即程序中的所有事物在任意时刻都只能执行一个步骤。
  • 并发编程:程序能够并行地执行程序中的多个部分。

21.1 并发的多面性

并发编程的难点:

  • 并发需要解决的问题有多个;实现并发的方式有多种;
  • 并且,上述两者之间没有明确的映射关系。

21.1.1 更快的执行

  • 并发目的:通常是为了提高运行在单处理器上的程序性能;
  • 并发实现1:最直接的方式是操作系统级别使用进程;
  • 并发实现2:Java采用的是更加传统的方式:在顺序型语言的基础上提供对线程的支持。

并发和并行的区别:

  • 并发:轮流处理多个任务:其实是按顺序执行的,cpu在任一时间只执行一个线程,通过给不同线程分配时间段的形式来进行调度,只是看起来好像多个任务是同时执行的;
  • 并行:同时处理多个任务:就是多个任务同一时刻在同时进行;

21.1.2 改进代码设计

  • 抢占式线程机制:调度机制会周期性的中端线程,将上下文切换到另一个线程,从而为每个线程提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。
  • 非抢占线程机制:每个线程可以需要CPU多少时间就占用CPU多少时间。在这种调度方式下,可能一个执行时间很长的线程使得其他所有需要CPU的线程”饿死”。在处理机空闲,即该进程没有使用CPU时,系统可以允许其他的进程暂时使用CPU。占用CPU的线程拥有对CPU的控制权,只有它自己主动释放CPU时,其他的线程才可以使用CPU。
  • 线程机制的选择:Java采用的是抢占式线程机制。

 

21.2 基本的线程机制

  • 并发编程:使得我们可以将程序划分为多个分离的、独立运行的任务。
  • 多线程机制:通过多线程机制,这些独立子任务中的每一个都将由执行线程驱动;

21.2.1 定义任务

  • 任务:线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。
  • 线程执行:当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处。它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。

21.2.2 Thread类(线程)

任务类型:

  • 无结果返回的任务Runnable:将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread的构造器(作为参数传入)
  • 有结果返回的任务Callable:配合使用Future实现获取任务结果。

Thread线程对象t包含的功能如下:

  • new Thread(runnable) 创建线程对象
  • t.start() 启动线程
  • Thread.sleep(100) 休眠
  • Thread.yield() 让步(暗示cpu可转为调度其他线程) --慎用!
  • Thread.currentThread() 获取当前thread对象
  • t.setProirity(proitity) 设置线程优先级
  • t.setDaemon(true or false) 设置是否为后台任务
  • t.isDaemon() 是否为后台任务
  • t.join() 特定线程在另一线程上调用t.join(),特定线程被挂起,直到另一线程执行结束才恢复执行-慎用(建议改为使用CyclicBarrier)!
  • t.interrupt(): 中断join()方法的调用
  • t.isAlive() 线程是否结束
  • t.isInterrupted() 线程是否已中断

21.2.3 使用Executor

  • Executor(执行器):Java SE5的 java.util.concurrent包中定义的执行器,用作管理 Thread对象,从而简化并发编程。并在客户端和任务执行之间提供了间接层;
  • Executor:允许你管理异步任务的执行,而无须显示的管理线程的生命周期;非常常见的场景:单个Executor被用来创建&管理系统中的所有任务。

四种不同的线程池(Executor的静态工厂构造实现):

21.2.3.1 newCachedThreadPool

  • 概念:可缓存的线程池。没有固定大小,如果线程池中的线程数量超过任务执行的数量,会回收60秒不执行的任务的空闲线程。当任务数量增加时,线程池自己会增加线程来执行任务。而能创建多少,就得看jvm能够创建多少。
  • 应用场景:default:合理的Executor的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool。
  • 实现方式:
ExecutorService exec = Exectors.newCachedThreadPool();

for(int i = 0; i<5, i++){

    exec.excute(runnble);

}

exec.shutdown();

21.2.3.2 newFixedThreadPool

  • 概念:可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量了。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。
  • 应用场景:这个实例会复用 固定数量的线程 处理一个 共享的无边界队列 。任何时间点,最多有 nThreads 个线程会处于活动状态执行任务。如果当所有线程都是活动时,有多的任务被提交过来,那么它会一致在队列中等待直到有线程可用。如果任何线程在执行过程中因为错误而中止,新的线程会替代它的位置来执行后续的任务。所有线程都会一致存于线程池中,直到显式的执行 ExecutorService.shutdown() 关闭。
  • 实现方式:ExecutorService exec = Exectors.newFixedThreadPool(5);

21.2.3.3 SingleThreadExecutor

  • 概念:单例线程池。就像是线程数量为1的FixedThreadPool。(它还提供了一种重要的并发保证,其他线程不会(即没有两个线程会)被调用。
  • 每个任务都是按照它们被提交的顺序,并且是在下一个任务开始之前完成的。因此,SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己(隐藏)的悬挂任务队列。
  • 应用场景:独占资源:多个线程都需要使用共用的文件资源时,使用此机制可保证:任何时刻在任何线程中只有一个任务在运行。
  • 实现方式:ExecutorService exec = Exectors.newSingleThreadPool();

21.2.3.4 newScheduledThreadPool

  • 概念:也是固定线程数量大小的线程池,可以延迟或者定时周期执行任务

 

21.2.4 从任务中产生返回值

  • Runnable:是执行工作的独立任务,但是它不返回任务值。
  • Callable:如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它。
public class TaskWithResult implements Callable<String> { //泛型类型:<String>
    private int id;
    public TaskWithResult(int id) {
        this.id = id;
    }
    @Override
    public String call() throws Exception {  // Callable.call(),类似于Runnable.run()
        return "TaskWithResult ---> " + id;
    }
}

//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//Future集合
ArrayList<Future<String>> list = new ArrayList<>();
for (int i = 0; i < 5;i++){
	Future<String> future = executorService.submit(new TaskWithResult(i)); //必须使用submit()
	list.add(future); //通过Future作为获取任务返回值
}
//遍历
for (Future<String> future : list){
	try {
		System.out.println(future.get()); //future.get():阻塞方式获取返回值
	} catch (InterruptedException | ExecutionException e) {
		e.printStackTrace();
	} finally {
		//记得关闭线程池
		executorService.shutdown();
	}
}

21.2.5 休眠

  • 休眠:.sleep() 将使任务中止执行(线程阻塞)指定的时间。
  • 应用场景:阻塞当前线程,用于等待I/O或其他耗时线程完成操作。

Thread.sleep(100) //Java SE5 new style : TimeUnit.MILLISECOND.sleep(100);

21.2.6 优先级

  • 优先级:.setPriority(priority) :将当前线程的优先级(重要性)传递给调度器。
  • 说明:绝大多数场景建议:线程按照默认的优先级执行,试图操控线程优先级通常是错误的。

21.2.7 让步--慎用!

  • 让步:.yield() : 暗示线程调度器:本任务已完成当前工作,可以让其他线程使用CPU了。
  • 说明:yield()让步机制经常被误用,此机制不保证一定会被线程调度器采用。--慎用!

21.2.8 后台线程

  • 后台线程:(damon线程): 指在程序运行的时候在后台提供一种通用服务的线程,并且此线程并不属于程序不可或缺的部分。
  • 区别1:当所有非后台线程结束时,程序已结束,同时会杀死进程中所有的后台进程。(有非后台线程运行,则进程不会结束)。
  • 区别2:后台线程的try-finally{}块部分在线程结束时不一定会被执行。
  • 设置方法:必须在.start()前setDaemon(),才能设置进程为后台进程。

21.2.9 编码(任务+线程)的变体  

  • Thread
  • Thread+Runnable
  • Thread+Callable+Future

21.2.10 常用术语

  • 任务:Java中,任务(Runnable&Callable ≠ 线程Thread),
  • Thread(线程):Java中,Thread本身并不执行任何操作,它只是驱动赋予它的任务。Executor执行器将负责处理线程的创建和管理。
  • Java的线程机制:基于来自C的低级实现方式,开发者需要深入研究&并完全理解其所有实现细节。

21.2.11 加入一个线程(建议改为使用CyclicBarrier)

  • t.join (para): 特定线程可以在另一线程中调用t.join():特定线程被挂起,直到另一线程执行结束才恢复执行-慎用!
  • 方法参数说明:para 超时参数,如果目标线程在这段时间到期仍未结束的话,join()方法总能返回。
  • t.interrupt(): 中断join()方法的调用

21.2.11 创建有响应的用户界面

public class ResponsiveUI extends Thread{
    private static volatile double d =1 ;
    public ResponsiveUI(){
        setDaemon(true);
        start();
    }
    public void run(){
        while(true){
            d = d + (Math.PI + Math.E)/ d;
        }
    }
}

new ResponsiveUI(); //启动无限循环的线程,等待用户输入(TODO ?)

21.2.13 线程组(应skip此概念)

  • 线程组:持有一个线程集合。线程组的价值可以引用Joshua Bloch的话来总结:“最好把线程组看成是一次不成功的尝试,你只要忽略它就好了。
  • 不建议继续研究的原因:如果你花费了大量的时间和精力试图发现线程组的价值(就像我一样),那么你可能会惊异,为什么没有来自Sun的关于这个主题的官方声明,多年以来,相同的问题对于Java发生的其他变化也询问过无数遍。诺贝尔经济学将得主Joseph Stiglitz的生活哲学可以用来解释这个问题,它被称为承诺升级理论(The Theory of Escalating Commitment):“继续错误的代价由别人来承担,而承认错误的代价由自己承担。”

21.2.14 捕获异常

  • 由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常。
  • 解决方案:Java SE5新增的Executor解决此问题。(可同时忽略线程组和捕获异常的相关问题)

 

21.3 共享受限资源(并发领域问题)

  • 顺序编程无共享资源访问冲突问题:可以把单线程程序当作在问题域求解的单一实体,每次只能做一件事情。
  • 共享受限资源:有了并发后,就可以同时做多件事情。即会存在多个线程 同时访问 共享数据&资源的问题。

21.3.1 不正确地访问资源

  • boolean类型的赋值和返回值操作是原子性的:即诸如赋值和返回值这样的简单操作在发生时没有中断的可能,因此你不会看到这个域处于在执行这些简单操作的过程中的中间状态。
  • 在Java中,递增不是原子性的操作。因此,如果不保护任务,即使单一的递增也不是安全的。

21.3.2 解决共享资源竞争

  • 解决共享资源访问竞争的方案:任务访问资源时,加锁。
  • 序列化访问共享资源:基本上所有的并发模式在解决线程冲突问题的时候,都采用此方案。即给定时间只允许一个任务访问共享资源(实现方式:比如方法前添加synchronized)。
  • 互斥量(mutex):因为方法|代码块 添加了锁,产生的互相排斥的效果。此机制称为互斥量。
  • synchronized(锁):Java以此关键字,为防止资源冲突提供了内置支持。
  • 对象锁:所有的对象都自动含有一个单一的锁(也称监视器)。当在对象上调用任意synchronized方法,此对象都被加锁,此时该对象上其他synchronized方法只有等到前一个方法执行调用完毕并释放了锁以后才能被调用。
  • Class类锁:针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static 方法也可以在类的范围防止对static数据的并发访问。
  • 同步规则:Brian的同步规则:“如果你正在写一个变量,它可能接下来将被另一个线程读取;或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且读写线程都必须使用相同的监视器锁同步。

21.3.2.1 使用显式的Lock对象

  • 显式Lock对象:Java SE5的concurrent类库包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式的创建,锁定和释放(与内置锁机制相比,代码缺乏优雅性)。
  • 显式锁选择原则:优先选择内置锁,只有解决特殊问题才改用显式锁。
  • ReentrantLock:显式重入锁:允许尝试着获取但最终未获取锁,如其他人已经获得此锁,那你就可以离开一段时间执行其他事情,不用一直等直至此锁被释放。

21.3.3 原子性&可见性&易变性

  • 原子性:是不能被线程调度机制中断的操作。一旦操作开始,它一定可以在可能发生的“上下文切换”前(切换到其他线程)执行完毕。
  • 基本数据类型的原子性:原子性可以应用于除了long&double之外的所有基本类型之上的“简单操作”(简单的赋值和返回操作,java递增操作不是原子性的)。
  • long&double变量的原子性:可以结合volatile 保证long&double变量“简单操作”的原子性(???如何理解)。
  • 可见性:即对变量进行写操作,那么所有读取此变量的线程都能立即读取到此修改。volatile 关键字可以保证变量的可见性。

21.3.4 原子类

  • 原子性:是不能被线程调度机制中断的操作。一旦操作开始,它一定可以在可能发生的“上下文切换”前(切换到其他线程)执行完毕。
  • 基本数据类型的原子性:原子性可以应用于除了long&double之外的所有基本类型之上的“简单操作”(简单的赋值和返回操作,java递增操作不是原子性的)。
  • long&double变量的原子性:可以结合volatile 保证long&double变量“简单操作”的原子性(???如何理解)。
  • 可见性:可见性:即对变量进行写操作,那么所有读取此变量的线程都能立即读取到此修改。volatile 关键字可以保证变量的可见性。

21.3.5 临界区

  • 临界区(critical section):也称同步代码块,使用synchronized{}包含的代码块。
  • 当只希望防止多个线程同时访问方法内部的部分代码块(而不是整个方法)时,通过这个方式分离出来的代码块称为“临界区”。

21.3.6 在其他对象上同步

  • 本对象的同步块:必须给定一个在其上进行同步的对象。最合理的方式:使用方法正在被调用的当前对象:synchronized(this){}
  • 其他对象上的同步块:有时必须在另外一个对象上同步,此场景下,必须保证所有相关的任务都在同一个对象上同步。synchronized(otherObject){}

21.3.7 线程本地存储(ThreadLocal类)

  • 防止共享资源冲突的方法二:根除对变量的共享
  • 线程本地存储:是一种自动化机制,可以为相同变量的每个线程都创建不同的存储。
  • ThreadLocal类:实现创建&管理线程的本地存储。ThreadLocal<Integer> value;

 

21.4 终结任务

21.4.1 装饰性花园

21.4.2 在阻塞时终结线程状态(四种状态):

  • 新增(new):当线程被创建时,只短暂的处于此状态;
  • 就绪(Runnable):在此状态,只要调度器将时间片分配给线程,线程就会运行。
  • 阻塞(Blocked):线程能够运行,但是某个条件阻止它的运行。当线程处于阻塞状态时,调度器会忽略此线程,不会分配线程任何 CPU时间。直到线程重新进入就绪状态,它才有可能执行操作。
  • 死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是run()方法返回。

21.4.3 中断

  • 方法1:Thread.interrupt():可以终止被阻塞的任务;此方法将设置线程的中断状态。
  • 方法2:Executor.shutdownNow(),它将发送一个interrupt()调用给它启动的所有线程。
  • 方法3:Future.cancel():是一种中断由Executor启动的单个线程的方式。

Executor 通过调用submit()而不是excutor()来启动任务,就可以持有该任务的上下文。submit()将返回一个泛型的Future<?>,持有这种Future的关键在于你可以在其上调用cancel(),并因此可以使用它来中断某个特定任务。

 

21.5 线程之间的协作

  • 线程协作:当使用线程来同时运行多个任务时,可以通过使用锁(同步)来同步不同任务的行为,从而使得一个任务不会干涉另一个任务的资源。即不同任务交替访问某个共享资源(通常是内存),可以使用互斥使得任何时刻只有一个任务可以访问这项资源。
  • 任务间握手:任务协作的关键问题,可以通过Object的wait()&notify(),或者Condition对象的await()&signal()方法来实现。

21.5.1 wait()与notifyAll()

  • wait():使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。--可避免采用忙等待的方式。
  • 忙等待(空循环):不断地进行空循环,这被称为忙等待, 通常是一种不良的周期使用方式。
  • notify()或notifyAll() :发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化。因此,wait()提供了一种在任务之间对活动同步的方式。

sleep()&wait()的区别:

  • sleep():调用时,锁没有被释放,调用yield()也属于此情况;
  • wait():将释放锁(并且线程被挂起),意味着另外一个任务可以获得这个锁;并且再通过notify()或notifyAll()或者时间到期,则会从wait()中恢复执行。
  • wait(), notify()以及notifyAll()有一个比较特殊的方面:那就是这些方法是基类Object的一个部分,而不是属于Thread的一部分。

错失的信号:

  • 当两个线程使用notify()+wait() 或者notifyAll()+wait()进行协作时,可能会错过某个信号。

21.5.2 notify() 与 notifyAll()

  在有关Java的线程机制的讨论中,有一个令人困惑的描述: notifyAll()将唤醒“所有下在等等的任务”。这是否意味着在程序中任何地方,任何处于wait()状态中的任务都将被任何对notifyAll()的调用唤醒呢?有示例说明情况并非如此——事实上,当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。

21.5.3 生成者与消费者(TODO!!!)

使用显式的Lock和Condition对象:

  • 显式Lock:Lock lock = new ReentrantLock();
  • Condition对象:使用互斥并允许任务挂起的基本类。
  • Condition.await():挂起任务(类似于Object.wait())
  • Condition.signal()&signalAll():外部条件变化时,通知这个&所有任务,从而唤醒对应任务(类似于Object.notify() &notifyAll())

21.5.4 生成者与消费者的实现(TODO!!!)

  • 方法1:使用wait()和notifyAll()
  • 方法2:使用高级容器(同步队列):LinkedBlockingQueue 或者ArrayBlockingQueue

21.5.5 任务间使用管道进行输入&输出(已废弃!)

  • 管道:基本是一个阻塞队列,存在于引入BlockingQueue之前的Java版本。
  • PipedWriter:(允许任务向管道写)
  • PipedReader:(允许不同任务从同一个管道读取)

 

21.6 死锁

  • 死锁:锁场景下,任务之间互相等待的连续循环,没有哪个线程能继续,称为“死锁”。

 由Edsger Dijkstrar提出的哲学家就餐问题是一个经典的死锁例证。要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:

  • 1. 互斥条件。任务使用的资源中至少有一个是不能共享的。这里,一根Chopstick一次就只能被一个Philosopher使用。
  • 2. 至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。也就是说,要发生死锁,Philosopher必须拿着一根Chopstick并且等待另一根。
  • 3. 资源不能被任务抢占,任务必须把资源释放当作普通事件。Philosopher很有礼貌,他们不会从其他Philosopher那里抢占Chopstick。
  • 4. 必须有循环等待,这时,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的浆,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。在DeadlockingDiningPhilosophers.java中,因为每个Philosopher都试图先得到右边的Chopstick,然后得到左边的Chopstick,所以发徨了循环等待

备注:所以要防止死锁的话,只需破坏其中一个即可。防止死锁最容易的方法是破坏第4个条件。

 

21.7 新类库中的构件

21.7.1 CountDownLatch(减法计数门闩)

  • 适用场景:它被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。即一个或多个任务需要等待,等待到其它任务,比如一个问题的初始部分,完成为止。
  • 你可以向CountDownLatch对象设置一个初始值,任何在这个对象上调用wait()的方法都将阻塞,直到这个计数值到达0.其他因结束其工作时,可以在访对象上调用countDown()来减小这个计数值。CountDownLatch被设计为只解发一次,计数值不能被重置。如果你需要能够重置计数值的版本,则可以使用CyclicBarrier。
  • 调用countDown()的任务在产生这个调用时并没有被阻塞,只有对await()的调用会被阻塞,直至计数值到达0。
  • CountDownLatch的典型用法是将一个程序分为n个互相独立的可解决任务,并创建值为n的CountDownLatch。当每个任务完成时,都会在这个锁存器上调用countDown()。等待问题被解决的任务在这个锁存器上调用await(),将它们自己挂起,直至锁存器计数结束。

21.7.2 CyclicBarrier(栅栏)

  • 适用场景:你希望创建一组任务,它们并行地执行工作,然后在进行下一下步骤之前等待,直至所有任务都完成(看起来有些像Join())。它使得所有的并行任务都将在栅栏处列队,因此可以一致地向前移动。
  • 例如程序赛马程序:HorseRace.java

备注:CountDownLatch&CyclicBarrier的区别:

  • CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;
  • CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。

21.7.3 DelayQueue

  • DelayQueue:是一个无界的BlockingQueue(同步队列),用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。
  • 这种队列是有序的,即队头对象是最先到期的对象。如果没有到期的对象,那么队列就没有头元素,所以poll()将返回null(也正因为此,我们不能将null放置到这种队列中)。
  • 如上所述,DelayQueue就成为了优先级队列的一种变体。

21.7.4 PriorityBlockingQueue

  • PriorityBlockingQueue:这是一个很基础的优先级队列,它具有可阻塞的读取操作。
  • 这种队列的阻塞特性提供了所有必需的同步,所以你应该注意到了,这里不需要任何显式的同步。--不必考虑当你从这种队列中读取时,其中是否有元素,因为这个队列在没有元素时,将直接阻塞读取者。

21.7.5 使用ScheduledExecutor的室温控制器

  “温室控制系统”可以被看作是一种并发问题,每个期望的温室事件都是一个预定时间运行的任务。

ScheduledThreadPoolExecutor可以解决这种问题:

  • schedule() 用来运行一次任务
  • scheduleAtFixedRate() 每隔规定的时间重复执行任务。
  • 两个方法接收delayTime参数。可以将Runnable对象设置为在将来的某个时刻执行。

21.7.6 Semaphre(计数信号量)

  • 正常的锁:(来自concurrent.locks显式锁,或者内建的synchronized锁):在任何时刻只允许一个任务访问。
  • Semaphre(计数信号量):允许n个任务同时访问这个资源。可以看作向外分发使用资源的许可证,尽管实际上并没有任何许可证对象。

21.7.7 Exchanger

  • Exchanger:在两个任务之间交换对象的栅栏。
  • 典型场景:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象。通过这个方式,可以有更多的对象在被创建的同时被消费。

 

21.8 仿真

21.8.1 银行出纳员仿真(ArrayBlockingQueue)

21.8.2 饭店仿真(SynchronousQueue)

21.8.3 分发工作(LinkedBlockingQueue)

不同BlockingQueue的区别:

  • ArrayBlockingQueue:是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
  • LinkedBlockingQueue:是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
  • SynchronousQueue:没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。

 

21.9 性能调优(Performance Tuning)

21.9.1 比较各类互斥技术(Comparing mutex technologies)

  • “微基准测试(microbenchmarking)”危险:这个术语通常指在隔离的、脱离上下文环境的情况下对某个特性进行性能测试。当然,你仍旧必须编写测试来验证诸如“Lock比synchronized更快”这样的断言,但是你需要在编写这些测试的进修意识到,在编译过程中和在运行时实际会发生什么。
  • 不同的编译器和运行时系统在这方面会有所差异,因此很难确切了解将会发生什么,但是我们需要防止编译器去预测结果的可能性。

使用Lock通常会比使用synchronized要高效许多,而且synchronized的开销看起来变化范围太大,而Lock相对比较一致。这是否意味着你永远都不应该使用synchronized关键字呢?这里有两个因素需要考虑:

  • 1. 互斥方法的方法体的大小。
  • 2. 代码可读性:synchronized关键字所产生的代码与Lock所需的“加锁-try/finally-解锁”惯用法所产生的代码相比,可读性提高了很多。
  • 总结1:关于代码可读性:代码被阅读的次数远多于被编写的次数。在编程时,与其他人交流相对于与计算机交流而言,要重要得多,因此代码的可读性至关重要。
  • 总结2:优先选择synchronized关键字,只有在性能调优时才替换为Lock对象这种做法,是具有实际意义的。

 

 

 

 

 

 

 

 

 

版权声明
本文为[架构师训练营]所创,转载请带上原文链接,感谢
https://cbk419323.blog.csdn.net/article/details/104120458

Scroll to Top