Java多线程知识小抄集(三)

51. SimpleDateFormat非线程安全

当多个线程共享一个SimpleDateFormat实例的时候,就会出现难以预料的异常。

主要原因是parse()方法使用calendar来生成返回的Date实例,而每次parse之前,都会把calendar里的相关属性清除掉。问题是这个calendar是个全局变量,也就是线程共享的。因此就会出现一个线程刚把calendar设置好,另一个线程就把它给清空了,这时第一个线程再parse的话就会有问题了。

解决方案:1. 每次使用时创建一个新的SimpleDateFormat实例;2. 创建一个共享的SimpleDateFormat实例变量,并对这个变量进行同步;3. 使用ThreadLocal为每个线程都创建一个独享的SimpleDateFormat实例变量。

52. CopyOnWriteArrayList

在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变现。CopyOnWriteArrayList的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需确保数组内容的可见性。因此,多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改容器的线程相互干扰。“写时复制”容器返回的迭代器不会抛出ConcurrentModificationException并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时,仅当迭代操作远远多于修改操作时,才应该使用“写入时赋值”容器。

53. 工作窃取算法(work-stealing)

工作窃取算法是指某个线程从其他队列里窃取任务来执行。在生产-消费者设计中,所有消费者有一个共享的工作队列,而在work-stealing设计中,每个消费者都有各自的双端队列,如果一个消费者完成了自己双端队列中的全部任务,那么它可以从其他消费者双端队列末尾秘密地获取工作。

优点:充分利用线程进行并行计算,减少了线程间的竞争。 缺点:在某些情况下还是存在竞争,比如双端队列(Deque)里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。

54. Future & FutureTask

FutureTask表示的计算是通过Callable来实现的,相当于一种可生产结果的Runnable,并且可以处于一下3种状态:等待运行,正在运行和运行完成。运行表示计算的所有可能结束方式,包括正常结束、由于取消而结束和由于异常而结束等。当FutureTask进入完成状态后,它会永远停止在这个状态上。Future.get的行为取决于任务的状态,如果任务已经完成,那么get会立刻返回结果,否则get将阻塞知道任务进入完成状态,然后返回结果或者异常。FutureTask的使用方式如下:

运行结果:yes no Callable表示的任务可以抛出受检查或未受检查的异常,并且任何代码都可能抛出一个Error.无论任务代码抛出什么异常,都会被封装到一个ExecutionException中,并在Future.get中被重新抛出。

55. Executors

newFixedThreadPool:创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。(LinkedBlockingQueue) newCachedThreadPool:创建一个可换成的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。(SynchronousQueue) newSingleThreadExecutor:是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。能确保一组任务在队列中的顺序来串行执行。(LinkedBlockingQueue) newScheduledThreadPool:创建了一个固定长度的线程池,而且以延迟或者定时的方式来执行任务,类似于Timer。

56. ScheduledThreadPoolExecutor替代Timer

由第17项可知Timer有两个缺陷,在JDK5开始就很少使用Timer了,取而代之的可以使用ScheduledThreadPoolExecutor。使用实例如下:

运行结果:1 Callable

57. Callable & Runnable

Executor框架使用Runnable作为基本的任务表示形式。Runnable是一种有很大局限的抽象,虽然run能写入到日志文件或者将结果放入某个共享的数据结构,但它不能返回一个值或抛出一个受检查的异常。

许多任务实际上都是存在延迟的计算——执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,Callable是一种更好的抽象:它认为主入口点(call())将返回一个值,并可能抛出一个异常。

Runnable和Callable描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束。

58. CompletionService

如果想Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复使用get方法,同事将参数timeout指定为0,从而通过轮询来判断任务是否完成。这种方法虽然可行,但却有些繁琐。幸运的是,还有一种更好的方法:CompletionService。CompletionService将Executor和BlockingQueue的功能融合在一起。你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时被封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托到一个Executor。代码示例如下:

运行结果:

可以通过ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)构造函数指定特定的BlockingQueue(如下代码剪辑),默认为LinkedBlockingQueue。

ExecutorCompletionService的JDK源码只有100行左右,有兴趣的朋友可以看看。

59. 通过Future来实现取消

ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,表示取消操作是否成功。如果mayInterruptIfRunning为true并且任务当前正在某个线程运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Futuure.cancel来取消任务。

60. 处理不可中断的阻塞

对于一下几种情况,中断请求只能设置线程的中断状态,除此之外没有其他任何作用。

  • Java.io包中的同步Socket I/O:虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException。
  • Java.io包中的同步I/O:当中断一个在InterruptibleChannel上等待的线程时会抛出ClosedByInterrptException并关闭链路。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。
  • Selector的异步I/O:如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
  • 获得某个锁:如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求,但是在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

61. 关闭钩子

JVM既可以正常关闭也可以强制关闭,或者说非正常关闭。关闭钩子可以在JVM关闭时执行一些特定的操作,譬如可以用于实现服务或应用程序的清理工作。关闭钩子可以在一下几种场景中应用:1. 程序正常退出(这里指一个JVM实例);2.使用System.exit();3.终端使用Ctrl+C触发的中断;4. 系统关闭;5. OutOfMemory宕机;6.使用Kill pid命令干掉进程(注:在使用kill -9 pid时,是不会被调用的)。使用方法(Runtime.getRuntime().addShutdownHook(Thread hook))。

62. 终结器finalize

终结器finalize:在回收器释放它们后,调用它们的finalize方法,从而保证一些持久化的资源被释放。在大多数情况下,通过使用finally代码块和显示的close方法,能够比使用终结器更好地管理资源。唯一例外情况在于:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。但是基于一些原因(譬如对象复活),我们要尽量避免编写或者使用包含终结器的类。

63. 线程工厂ThreadFactory

每当线程池(ThreadPoolExecutor)需要创建一个线程时,都是通过线程功夫方法来完成的。默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。通过指定一个线程工厂方法,可以定制线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。默认的线程工厂(DefaultThreadFactory 是Executors的内部类)如下:

通过implements ThreadFactory可以定制线程工厂。譬如,你希望为线程池中的线程指定一个UncaughtExceptionHandler,或者实例化一个定制的Thread类用于执行调试信息的记录。

64. synchronized与ReentrantLock之间进行选择

由第21条可知ReentrantLock与synchronized想必提供了许多功能:定时的锁等待,可中断的锁等待、公平锁、非阻塞的获取锁等,而且从性能上来说ReentrantLock比synchronized略有胜出(JDK6起),在JDK5中是远远胜出,为嘛不放弃synchronized呢?ReentrantLock的危险性要比同步机制高,如果忘记在finnally块中调用unlock,那么虽然代码表面上能正常运行,但实际上已经埋下了一颗定时炸弹,并很可能伤及其他代码。仅当内置锁不能满足需求时,才可以考虑使用ReentrantLock.

65. Happens-Before规则

程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前。 监视器锁规则:一个unlock操作现行发生于后面对同一个锁的lock操作。 volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。 线程终止规则:线程的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等于段检测到线程已经终止执行。 线程中断规则:线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

注意:如果两个操作之间存在happens-before关系,并不意味着java平台的具体实现必须要按照Happens-Before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。

66. as-if-serial

不管怎么重排序,程序执行结果不能被改变。

67. ABA问题

ABA问题发生在类似这样的场景:线程1转变使用CAS将变量A的值替换为C,在此时,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍为A,所以CAS成功。但实际上这时的现场已经和最初的不同了。大多数情况下ABA问题不会产生什么影响。如果有特殊情况下由于ABA问题导致,可用采用AtomicStampedReference来解决,原理:乐观锁+version。可以参考下面的案例来了解其中的不同。

输出结果:true false

68. 如何避免死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足一下4个条件: 互斥条件:一个资源每次只能被一个进程使用。 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序做操作来避免死锁。

69. 怎么检测一个线程是否拥有锁

java.lang.Thread中有一个方法:public static native boolean holdsLock(Object obj). 当且仅当当前线程拥有某个具体对象的锁时返回true

70. 如何查看线程快照?

jstack命令用来生成虚拟机当前的线程快照信息,线程快照就是当前虚拟机每一个线程正在执行的方法堆栈的集合。生成线程快照的目的主要是为了定位线程长时间没有响应的原因,如线程死锁、网络请求没有设置超时时间而长时间没有返回、死循环、信号量没有释放等,都有可能导致线程长时间停顿。这是如果能够dump出当前JVM的线程快照,就能够看出没有响应的线程究竟在做什么事情,从而定位问题。 语法:

稍加翻译一下: -F 用来在输出不被响应时强制生成线程的快照 -m用来答应出包含Java和native代码的所有堆栈信息 -l 打印出锁的附加信息 可以配合jps命令找出pid

71. JAVA中的线程调度算法

抢占式。一个线程用完CPU之后,操作系统会根据现场优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

72. Thread.sleep(0)有什么作用?

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程尝尝获取到CPU控制权的情况,为了让某些优先级比较低的线程能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

73. CAS

全称CompareAndSwap。假设有三个操作数:内存值V,旧的预期值A,要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要配合volatile变量,这样才能保证每次拿到的遍历是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

74. AQS

全称AbstractQueuedSynchronizer。如果说JUC(java.util.concurrent)的基础是CAS的话,那么AQS就是整个JAVA并发包的核心了,ReentrantLock, ReentrantReadWriteLock, CountDownLatch, Semaphore等都用到了它。

75.合理地配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。 - 对于CPU密集型任务:线程池中线程个数应尽量少,不应大于CPU核心数; - 对于IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率; - 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

76. 多线程三大定律

  1. Amdahl 定律 –Gene Amdahl 发现在计算机体系架构设计过程中,某个部件的优化对整个架构的优化和改善是有上限的。这个发现后来成为知名的Amdahl 定律。 比如:即使你有10个老婆,也不能一个月把孩子生下来。
  2. Gustafson 定律 –Gustafson假设随着处理器个数的增加,并行与串行的计算总量也是可以增加的。Gustafson定律认为加速系数几乎跟处理器个数成正比,如果现实情况符合Gustafson定律的假设前提的话,那么软件的性能将可以随着处理个数的增加而增加。 比如:当你有10个老婆,就会要生更多的孩子。
  3. Sun-Ni 定律 –充分利用存储空间等计算资源,尽量增大问题规模以产生更好/更精确的解。 比如:你要设法让每个老婆都在干活,别让她们闲着。

77. 进程间通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • # 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  • # 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

(完)

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢

原文发布于微信公众号 - IT技术精选文摘(ITHK01)

原文发表时间:2017-07-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏深度学习自然语言处理

实例快速上手shell脚本

昨天老师给了宗林师兄任务,让我跑一个机器翻译的程序。我看了看就是跑shell脚本。刚开始一看。。我的天。。好长的代码,但是觉得这个时候就更不能怕,得迎难而上,趁...

3769
来自专栏JAVA技术zhai

干货:Java并发编程必懂知识点解析(内附面试题)

2665
来自专栏JetpropelledSnake

Python面试题之Python面试题汇总

(1)与java相比:在很多方面,Python比Java要简单,比如java中所有变量必须声明才能使用,而Python不需要声明,用少量的代码构建出很多功能;...

5.7K4
来自专栏性能与架构

Redis案例 - 事件提醒

场景 任务是 当 redis set 中有新元素时及时处理 需要在set有新元素后自动得到通知,省得使用轮询的方式来查看是否有新元素 相当于对set做一...

3638
来自专栏Seebug漏洞平台

从补丁到漏洞分析——记一次joomla漏洞应急

2018年1月30日,joomla更新了3.8.4版本,这次更新修复了4个安全漏洞,以及上百个bug修复。

4268
来自专栏信安之路

二进制漏洞学习笔记

这个程序非常简单,甚至不需要你写脚本,直接运行就能获得shell。 写这个程序的目的主要是为了使第一次接触漏洞的同学更好地理解栈溢出的原理。

1390
来自专栏java一日一条

Java高效并发之乐观锁悲观锁、(互斥同步、非互斥同步)

首先我们理解下两种不同思路的锁,乐观锁和悲观锁。 这两种锁机制,是在多用户环境并发控制的两种所机制。下面看百度百科对乐观锁和悲观锁两种锁机制的定义:

1723
来自专栏JAVA高级架构

JAVA后端面试100 Q&amp;A之第一篇

1111
来自专栏Albert陈凯

2018-08-02 IntelliJ IDEA - Debug 调试多线程程序IntelliJ IDEA - Debug 调试多线程程序

https://blog.csdn.net/nextyu/article/details/79039566

2092
来自专栏蓝天

redis的一些简介

Redis是Remote Dictionary Server的缩写,他本质上一个Key/Value数据库,与Memcached类似的NoSQL型数据库。

1101

扫码关注云+社区

领取腾讯云代金券