9.实战java高并发程序设计(全书总结)

1.了解同步(Synchronous)异步(Asynchronous)的区别
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,

2. 并发(Concurrency)和并行(Parallelism)的区别

并行的多个任务是真的同时执行,而对于并发来说,这个过程只是交替的,一会儿执行任务A,一会儿执行任务B,系统会不停地在两者之间切换。但对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间并行执行的错觉。

3.临界区

临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。

4 阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用来形容多线程间的相互影响

5.死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁是一个很严重的并且应该避免和时时小心的问题,都在等待对方的锁,不愿释放自己的锁

饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作.饥饿还是有可能在未来一段时间内解决的(比如,高优先级的线程已经完成任务,不再疯狂执行)。

如果线程的智力不够,且都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。

6.并发级别

阻塞、无饥饿、无障碍、无锁、无等待

当我们使用synchronized关键字或者重入锁时(我们将在第2、3章介绍这两种技术),我们得到的就是阻塞的线程。

对于非公平锁来说,系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿。但如果锁是公平的,按照先来后到的规则,那么饥饿就不会产生,不管新来的线程优先级多高,要想获得资源,就必须乖乖排队,这样所有的线程都有机会执行。

无障碍是一种最弱的非阻塞调度。两个线程如果无障碍地执行,那么不会因为临界区的问题导致一方被挂起。换言之,大家都可以大摇大摆地进入临界区了。那么大家一起修改共享数据,把数据改坏了怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。一种可行的无障碍实现可以依赖一个“一致性标记”来实现.线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。

无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。

要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。

7.JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。

原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,对于32位系统来说,long型数据的读写不是原子性的

可见性是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改

指令重排对于提高CPU处理性能是十分必要的。虽然确实带来了乱序的问题,但是这点牺牲是完全值得的。

8.进程与线程

进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程

9.线程状态

新建线程new
开启线程start

线程中断(interrupt线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦)

暴力终止线程stop(原因是stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题.)

等待(wait)和通知(notify:这两个方法并不是在Thread类中的,而是输出Object类。

注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另外一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

挂起(suspend)和继续执行(resume)线程:废弃方法,并不推荐使用。

 等待线程结束(join,也可以加入等待时间)和谦让(yeild):

join():)一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能。

Thread.yeild()静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到就不一定了。

10.volatile与Java内存模型(JMM):为了在适当的场合,确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或者关键字来声明、告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一。当你用关键字volatile声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。

11.分门别类的管理:线程组(ThreadGroup,注意:是线程组不是线程池,可以创建线程加入线程组中)

11.驻守后台:守护线程(Daemon)(守护线程守护的是用户线程,xxThread.setDaemom(true)):

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部结束,则意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机就会自然退出。
12.先做重要的事:线程优先级(xxxThread.setPriority()):Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这只是一个概率问题

13.线程安全的概念与关键字synchronized

关键字synchronized可以有多种用法,这里做一个简单的整理。● 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。● 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。● 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

14.程序中的幽灵:隐蔽的错误

  • 无提示的错误案例
  • 并发下的ArrayList,:改进的方法很简单,使用线程安全的Vector代替ArrayList即可。
  • 并发下诡异的HashMap,最简单的解决方案就是使用ConcurrentHashMap代替HashMap。
  • 初学者常见的问题:错误的加锁

15.多线程的团队协作:同步控制,重入锁ReentrantLock

关键字synchronized的功能扩展:重入锁,从JDK 6.0开始,JDK在关键字synchronized上做了大量的优化,使得两者的性能差距并不大。

与关键字synchronized相比,重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远优于关键字synchronized。但值得注意的是,在退出临界区时,必须记得释放锁,否则,其他线程就没有机会再访问临界区了。

  • 中断响应lockInterruptibly():这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断
  • 锁申请等待限时tryLock()
  • 公平锁 new ReentrantLock(true):它不会产生饥饿现象,公平锁的实现成本比较高,性能却非常低下
  • unlock():释放锁。
  • lock():获得锁,如果锁已经被占用,则等待。

16. 重入锁的好搭档:Condition : lock.newCondition():利用Condition对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。类似wait(),notify()

● await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()方法或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法相似。

● awaitUninterruptibly()方法与await()方法基本相同,但是它并不会在等待过程中响应中断。

● singal()方法用于唤醒一个在等待中的线程,singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法很类似。


17. 允许多个线程同时访问:信号量(Semaphore):信号量却可以指定多个线程,同时访问某一个资源

18.ReadWriteLock读写锁 lock.writeLock(),lock.readLock()

读写分离锁可以有效地帮助减少锁竞争,提升系统性能。

● 读-读不互斥:读读之间不阻塞。

● 读-写互斥:读阻塞写,写也会阻塞读。

● 写-写互斥:写写阻塞。

19.倒计数器:CountDownLatch:CountDownLatch是一个非常实用的多线程控制工具类。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计数结束,再开始执行。

20.循环栅栏:CyclicBarrier

CyclicBarrier是另外一种多线程并发控制工具。和CountDownLatch非常类似,它也可以实现线程间的计数等待,但它的功能比CountDownLatch更加复杂且强大。

21.线程阻塞工具类:LockSupport

LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。与Thread.suspend()方法相比,它弥补了由于resume()方法发生导致线程无法继续执行的情况。和Object.wait()方法相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。
LockSupport的静态方法park()可以阻塞当前线程,类似的还有parkNanos()、parkUntil()等方法。它们实现了一个限时的等待。

22.线程复用:线程池

为了避免系统频繁地创建和销毁线程,我们可以让创建的线程复用。

JDK对线程池的支持Executor,java.util.concurrent包中,是JDK并发包的核心类。

● newFixedThreadPool()方法:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理任务队列中的任务。固定数量线程池

● newSingleThreadExecutor()方法:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。单个线程线程池

● newCachedThreadPool()方法:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。可自动扩充数量线程池

● newSingleThreadScheduledExecutor()方法:该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。可指定时间的单个线程线程池

● newScheduledThreadPool()方法:该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。可指定数量线程池,可以根据时间需要对线程进行调度。

23.核心线程池的内部实现,其内部实现均使用了ThreadPoolExecutor类\

24.拒绝策略

ThreadPoolExecutor类的最后一个参数指定了拒绝策略。也就是当任务数量超过系统实际承载能力时,就要用到拒绝策略了。拒绝策略可以说是系统超负荷运行时的补救措施,通常由于压力太大而引起的,也就是线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列中也已经排满了,再也放不下新任务了。这时,我们就需要有一套机制合理地处理这个问题。

● AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。

● CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

● DiscardOldestPolicy策略:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

● DiscardPolicy策略:该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,我觉得这可能是最好的一种方案了吧!
25. 自定义线程创建:ThreadFactory

自定义线程池可以帮助我们做不少事。比如,我们可以跟踪线程池究竟在何时创建了多少线程,也可以自定义线程的名称、组以及优先级等信息,甚至可以任性地将所有的线程设置为守护线程。总之,使用自定义线程池可以让我们更加自由地设置线程池中所有线程的状态。

26.扩展线程池

ThreadPoolExecutor是一个可以扩展的线程池。它提供了beforeExecute()、afterExecute()和terminated()三个接口用来对线程池进行控制

27.堆栈去哪里了:在线程池中寻找堆栈

一种最简单的方法就是放弃submit()方法,改用execute()方法\

28.并发集合简介

JDK提供的这些容器大部分在java.util.concurrent包中。我先提纲挈领地介绍一下它们,初次露脸,大家只需要知道它们的作用即可。有关具体的实现和注意事项,在后面我会一一道来。

● ConcurrentHashMap:这是一个高效的并发HashMap。你可以把它理解为一个线程安全的HashMap。

● CopyOnWriteArrayList:这是一个List,从名字看就知道它和ArrayList是一族的。在读多写少的场合,这个List的性能非常好,远远优于Vector(从这个类的名字我们可以看到,所谓CopyOnWrite就是在写入操作时,进行一次自我复制。换句话说,当这个List需要修改时,我并不修改原有的内容(这对于保证当前在读线程的数据一致性非常重要),而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后,再用修改完的副本替换原来的数据,这样就可以保证写操作不会影响读了)
 

● ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看作一个线程安全的LinkedList。

● BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合作为数据共享的通道。

● ConcurrentSkipListMap:跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。除以上并发包中的专有数据结构以外,java.util下的Vector是线程安全的(虽然性能和上述专用工具没得比),另外Collections工具类可以帮助我们将任意集合包装成线程安全的集合(Collections.synchronizedMap())。

跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整,而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。

使用跳表实现Map和使用哈希算法实现Map的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是有序的。

29.使用JMH进行性能测试

Maven进行导入

1.模式(Mode)Mode表示JMH的测量方式和角度,共有4种。

● Throughput:整体吞吐量,表示1秒内可以执行多少次调用。

● AverageTime:调用的平均时间,指每一次调用所需要的时间。

● SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”。

● SingleShotTime:以上模式都是默认一次Iteration是1秒,唯有SingleShotTime只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

30.有关性能的一些思考

HashMap的get()方法比ConcurrentHashMap的快,也不能说明它的put()方法或者size()方法同样也会更快。因此,快慢的比较不能离开具体的使用场景。在单线程下,ConcurrentHashMap的get()方法比HashMap的略快,但是size()方法却比HashMap的慢很多。当HashMap进行同步后,由于同步锁的开销,size()方法的性能急剧下降,与ConcurrentHashMap的size()方法在一个数量级上,但依然比ConcurrentHashMap快。
 

31.有助于提高锁性能的几点建议

  •  减少锁持有时间
  • 减小锁粒度
  • 用读写分离锁来替换独占锁
  •  锁粗化(虚拟机在遇到一连串连续地对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步的次数,这个操作叫作锁的粗化)

32.Java虚拟机对锁优化所做的努力

锁偏向:它的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能

自旋锁:锁膨胀后,为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力—自旋锁。当前线程暂时无法获得锁,而且什么时候可以获得锁是一个未知数,也许在几个CPU时钟周期后就可以得到锁。如果这样,简单粗暴地挂起线程可能是一种得不偿失的操作。系统会假设在不久的将来,线程可以得到这把锁。因此,虚拟机会让当前线程做几个空循环(这也是自旋的含义),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真的将线程在操作系统层面挂起。
 锁消除:锁消除是一种更彻底的锁优化。Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间

31.所谓逃逸分析就是观察某一个变量是否会逃出某一个作用域。变量v逃逸出了当前函数,也就是说变量v有可能被其他线程访问

32.人手一支笔:ThreadLocal

这是一个线程的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。

ThreadLocal的实现原理:我们需要关注的自然是ThreadLocal的set()方法和get()方法。先从set()方法说起:

其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合.  在ThreadLocal类中有一个ThreadLocalMap, 用于存放每一个线程的变量副本,Map中元素的key为线程对象,value为对应线程的变量副本。

  另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。 
在了解了ThreadLocal的内部实现后,我们自然会引出一个问题:那就是这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap.

因此,使用线程池就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals Map内),可能会使系统出现内存泄漏的可能(这里我的意思是:你设置了对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。

此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。
 

33.无锁

无锁的策略使用一种叫作比较交换(CAS,Compare And Swap)的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止

与锁相比,使用比较交换会使程序看起来更加复杂一些,但由于其非阻塞性,它对死锁问题天生免疫,并且线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

CAS算法的过程是:它包含三个参数CAS(V,E,N),其中V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
 

34.无锁的线程安全整数:AtomicInteger

JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。

与Integer不同,它是可变的,并且是线程安全的。使用AtomicInteger会比使用锁具有更好的性能

无锁的对象引用:AtomicReference

带有时间戳的对象引用:AtomicStampedReference(当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值及时间戳都必须满足期望值,写入才会成功。)

数组也能无锁:AtomicIntegerArray

35.有关死锁的问题

通常的表现就是相关的进程不再工作,并且CPU占用率为0(因为死锁的线程不占用CPU)

我们可以使用jps命令得到Java进程的进程ID,接着使用jstack命令得到线程的线程堆栈

37.探讨单例模式

(1)对于频繁使用的对象,可以省略new操作花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

(2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

一种不错的单例创建方式

上述代码实现了一个单例,并且同时拥有前两种方式的优点。首先getInstance()方法中没有锁,这使得在高并发环境下性能优越。其次,只有在getInstance()方法第一次被调用时,StaticSingleton的实例才会被创建。因为这种方法巧妙地使用了内部类和类的初始化方式。内部类SingletonHolder被声明为private,这使得我们不可能在外部访问并初始化它。而我们只可能在getInstance()方法内部对SingletonHolder类进行初始化,利用虚拟机的类初始化机制创建单例。
 

38.不变模式

为了尽可能地去除这些同步操作,提高并行程序性能可以使用一种不可改变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然保持内部状态的一致性和正确性。这就是不变模式。

不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,它的内部状态将永远不会发生改变。没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。基于这些特性,对不变对象的多线程操作不需要进行同步控制。

不变模式的主要使用场景需要满足以下

● 去除setter方法及所有修改自身属性的方法。

● 将所有属性设置为私有,并用final标记,确保其不可修改。

● 确保没有子类可以重载修改它的行为。

● 有一个可以创建完整对象的构造函数。以下代码实现了一个不变的产品对象,它拥有序列号、名称和价格三个属性

39.生产者-消费者模式

消费者线程并不直接与生产者线程通信,而是在共享内存缓冲区中获取任务,并进行处理

注意:生产者-消费者模式中的内存缓冲区的主要功能是数据在多线程间的共享,此外,通过该缓冲区,可以缓解生产者和消费者间的性能差。

生产者-消费者模式的核心组件是共享内存缓冲区,它作为生产者和消费者间的通信桥梁,避免了生产者和消费者直接通信,从而将生产者和消费者进行解耦。生产者不需要知道消费者的存在,消费者也不需要知道生产者的存在。

40.Java 8/9/10与并发

函数式编程:一个函数可以作为另外一个函数的返回值

特点:

  • 无副作用(显式函数指函数与外界交换数据的唯一渠道就是参数和返回值,显式函数不会去读取或者修改函数的外部状态。与之相对的是隐式函数,隐式函数除参数和返回值外,还会读取外部信息,或者修改外部信息。)
  • 声明式的(Declarative)对于声明式的编程范式,你不再需要提供明确的指令操作,所有的细节指令将会更好地被程序库封装,你要做的只是提出你的要求,声明你的用意即可。
  • 不变的对象:在函数式编程中,几乎所有传递的对象都不会被轻易修改。
  • 易于并行:由于对象都处于不变的状态,因此函数式编程更加易于并行
  • 更少的代码

41.FunctionalInterface注释

所谓函数式接口,简单地说,就是只定义了单一抽象方法的接口,,函数式接口只能有一个抽象方法,而不是只能有一个方法

42.接口默认方法

接口里面可以定义的方法里面可以有方法体,实现类可以不重写接口里的默认方法 ,实现类可以直接拿来用,不需要重写方法,有继承的味道

43. lambda表达式

lambda表达式可以说是函数式编程的核心。lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者,lambda表达式极大地增强了Java语言的表达能力。

44. 方法引用

法引用在Java 8中的使用非常灵活。总的来说,可以分为以下几种。

● 静态方法引用:ClassName::methodName。

● 实例上的实例方法引用:instanceReference::methodName。

● 超类上的实例方法引用:super::methodName。

● 类型上的实例方法引用:ClassName::methodName。

● 构造方法引用:Class::new。

● 数组构造方法引用:TypeName[]::new。

首先,方法引用使用“::”定义,“::”的前半部分表示类名或者实例名,后半部分表示方法名称。如果是构造函数,则使用new表示
 

lambda表达式。表达式由“->”分割,左半部分表示参数,右半部分表示实现体。因此,我们也可以简单地理解lambda表达式只是匿名对象实现的一种新的方式。

45.并行流与并行排序

parallel()方法得到一个并行流

Arrays.parallelSort()

46.ConcurrentHashMap的增强

foreach操作

reduce操作

条件插入computeIfAbsent()

search操作

我们需要理解ArrayList的工作方式。在ArrayList初始化时,默认会分配10个数组空间。当数组空间消耗完毕后,ArrayList就会进行自动扩容。在每次add()函数时,系统总要事先检查一下内部空间是否满足所需的大小,如果不满足,就会扩容,否则就正常添加元素。多线程共同访问ArrayList的问题在于:在ArrayList容量快用完时(只有1个可用空间),如果两个线程同时进入add()函数,并同时判断认为系统满足继续添加元素而不需要扩容,那么两者都不会进行扩容操作。之后,两个线程先后向系统写入自己的数据,那么必然有一个线程会将数据写到边界外,从而产生了ArrayIndexOutOfBoundsException。
 

发布了33 篇原创文章 · 获赞 1 · 访问量 5512

猜你喜欢

转载自blog.csdn.net/ashylya/article/details/104521243
今日推荐