面试必问的多线程-1.1:线程

1:名词解释volatile

volatile关键字的作用是:保证变量的可见性。

注意插播一条:

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

1.1:volatile

高速缓存

每个线程运行时有自己的高速缓存。

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

1.2:并发编程中的三个概念

1:原子性

2:可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

3:有序性:即程序执行的顺序按照代码的先后顺序执行。

指令重排序

什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,

它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,

但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

处理器在进行重排序时是会考虑指令之间的数据依赖性

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

1.3:Java内存模型:

那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。

Java内存模型规定所有的变量都是存在主存当中(类似于物理内存),

每个线程都有自己的工作内存(类似于高速缓存)。

线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。

并且每个线程不能访问其他线程的工作内存。

1:原子性:

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,

如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。

由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2:可见性:

Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,

当有其他线程需要读取时,它会去内存中读取新值。

另外,通过synchronized和Lock也能够保证可见性,

synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,

并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3:有序性:

可以通过volatile关键字来保证一定的“有序性”

另外可以通过synchronized和Lock来保证有序性,

很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,

相当于是让线程顺序执行同步代码,自然就保证了有序性。

1.4:volatile关键字的两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

注意1:

volatile可以保证可见性。

但是volatile不能保证对变量操作的原子性。

为什么不能保证原子性呢?

前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?

然后其他线程去读就会读到新的值,对,这个没错。

这个就是上面的happens-before规则中的volatile变量规则,

但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。

然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,

所以线程2根本就不会看到修改的值。

注意2:

volatile可以保证有序性                

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,

         且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,

         也不能把volatile变量后面的语句放到其前面执行。

总结:

valatile可以保证可见性,有序性,不能保证原子性。

1.5:使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,

而volatile关键字在某些情况下性能要优于synchronized,

 

但是要注意volatile关键字是无法替代synchronized关键字的,

因为volatile关键字无法保证操作的原子性。

下面列举几个Java中使用volatile的几个场景。

1.状态标记量

Volatile从来就不是用来保证操作原子性的关键字,他只负责保证可见性和有序性,他的原子性是需要依靠锁来保证的。

其实他也有一定的原子性,单个volatile变量的读操作和写操作是具有原子性的,但是一旦拥有多个操作,不再保证原子性。

所以Volatile的使用需要你参照具体的场景来使用,并不是什么场景都能用,它是不能替代锁的作用的。

之所以称之为轻量级锁,就是因为这个!

1.6:volatile的内存语义与AQS锁内存可见性

首先:当我们提到volatile首先想到的就是:

1:保证此变量对所有线程的可见性,这里的 “可见性” 是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

2:禁止指令重排序优化。

到这里感觉自己对volatile理解了吗? 

如果理解了,考虑这么一个问题:ReentrantLock(或者其它基于AQS实现的锁)是如何保证代码段中变量(变量主要是指共享变量,存在竞争问题的变量)的可见性?

private static ReentrantLock reentrantLock = new ReentrantLock();
private static intcount = 0;
//...
// 多线程 run 如下代码
reentrantLock.lock();
try
{
    count++;
} 
finally
{
    reentrantLock.unlock();
}
//...

既然提到了可见性,那就先熟悉几个概念:

1、Java Memory Model (JMM) 即 Java 内存模型

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量主要是指共享变量,存在竞争问题的变量。Java内存模型规定所有的变量都存储在主内存中,而每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(根据Java虚拟机规范的规定,volatile变量依然有共享内存的拷贝,但是由于它特殊的操作顺序性规定——从工作内存中读写数据前,必须先将主内存中的数据同步到工作内存中,所有看起来如同直接在主内存中读写访问一般,因此这里的描述对于volatile也不例外)。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。

2、重排序:

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

1:编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2:指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3:内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

3、happens-before规则

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

happens-before原则定义如下:

1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

happens-before本质是顺序,重点是跨越内存栅栏。

一句话理解什么是happens-beforehttp://www.threadworld.cn/archives/29.html

happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。下面我们就一个简单的例子稍微了解下happens-before ;

i = 1;       //线程A执行
j = i ;      //线程B执行

j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么可以确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成立。这就是happens-before原则的威力。

这里再说一遍happens-before的概念:

如果两个操作不存在上述happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。

如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

 

2:名词解释ThreadLocal

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。

可能很多人都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

2.1

ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。很多开发语言在语言级别都提供这种作用域的变量类型。

根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。

还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。

全局变量,范围很大;局部变量,范围很小。无论是大还是小,其实都是定死的。而线程变量,调用几个函数,则决定了它的作用域有多大。

ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。

ThreadLocal也是跨函数的,但是跨哪些函数呢,由线程来定,更灵活。

总之,ThreadLocal类是修饰变量的,是在控制它的作用域,是为了增加变量的种类而已,这才是ThreadLocal类诞生的初衷,它的初衷可不是解决线程冲突的。

总结:

ThreadLocal(线程变量副本)

Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。

采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,

每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。

ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。

ThreadLocal : 其实表示的就是线程的局部变量,

用法也比较简单。

之前出现的线程安全的问题,都是由于共享资源导致的,

那么我们有了这个ThreadLocal呢,线程的局部变量,

使用它也可以来解决线程安全性问题。

应用场景:就相当于每一个线程都有它自己的局部变量。

例如:数据库连接池,每个线程都有一个连接,

这个连接对于这个线程来说是独立的。

3:名词解释wait()、notify/notifyAll()

1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。

2、wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。

当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。

只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。

也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程 

4、wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。

5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

6、notify 和 notifyAll的区别

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

7、在多线程中要测试某个条件的变化,使用if 还是while?

要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。

有几个点要注意一下:

notify()方法

1:notify()方法会唤醒一个等待当前对象的锁的线程。

2:如果多个线程在等待,它们中的一个将会选择被唤醒。

这种选择是随意的,和具体实现有关。(线程等待一个对象的锁 是由于调用了wait方法中的一个)。

3:被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁。

4:被唤醒的线程将和其他线程以通常的方式进行竞争,来获得对象的锁。

也就是说,被唤醒的线程并没有什么优先权,也 没有什么劣势,

对象的下一个线程还是需要通过一般性的竞争。

5:notify()方法应该是被拥有对象的锁的线程所调用。

6:和wait()方法一样,notify方法调用必须放在synchronized方法或synchronized块中。

wait()和notify()方法要求在调用时线程已经获得了对象的锁,

因此对这两个方法的调用需要放在synchronized方法或 synchronized块中。

一个线程变为一个对象的锁的拥有者是通过下列三种方法:

1.执行这个对象的synchronized实例方法。

2.执行这个对象的synchronized语句块。这个语句块锁的是这个对象。

3.对于Class类的对象,执行那个类的synchronized、static方法。

其中还有一个关键字比较常用:Condition 

Condition - 条件对象 (condition -条件) 

condition-也是用于等待和唤醒

waitnotify的功能一样,只不过它的功能更加强大,也更加灵活。

condition好像是可以叫醒你想要叫醒的线程。这一点还是非常强大的。

 这个接口里面的方法:

              等待就是await

              唤醒就是signal

比如(多线程顺序执行)a执行完之后,b执行,之后再c执行。

改造之前的wait和notify方法,用condition来实现相同的功能:

其实改造wait和notify成condition非常简单:

无非就是把synchronized换成Lock(ReentrantLock)

把wait换成condition的await

把notify换成condition的signal

这样就行了,

非常简单的。

详细说明:

Condition的作用是对锁进行更精确的控制。

Condition中的await()方法相当于Object的wait()方法,

Condition中的signal()方法相当于Object的notify()方法,

Condition中的signalAll()相当于Object的notifyAll()方法。

不同的是,

Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;

而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。

互斥锁ReentrantLock

最后还要注意,Java 中有 signal 和 signalAll 两种方法,

signal 是随机解除一个等待集中的线程的阻塞状态,

signalAll 是解除所有等待集中的线程的阻塞状态。

signal 方法的效率会比 signalAll 高,但是它存在危险,因为它一次只解除一个线程的阻塞状态,因此,如果等待集中有多个线程都满足了条件,也只能唤醒一个,其他的线程可能会导致死锁。

但是好像这个condition实际开发中用的不多,反正我是没怎么用过,知道有这么个类似于wait和notify的东西就行了。

至于工作中用到,在具体去看相关的api即可。

4:名词解释 AQS CAS简单理解

谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!

类如其名,抽象的队列式的同步器AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…

Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。

AQS的核心是通过一个共享变量来同步状态,变量的状态由子类去维护。AQS所做的工作主要有两部分: 
(1)线程阻塞队列的维护 
(2)线程的阻塞与唤醒 
共享变量的修改都是通过Unsafe类提供的CAS操作完成的。

AQS(AbstractQueuedSynchronizer)

维护一个volatile int state(代表共享资源状态)和一个FIFO线程等待队列。

其中资源状态的修改,是CAS的,是原子操作。

CAS(Compare And Swap)

什么是CAS

CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可

CAS典型应用

java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的(eg. AtomicInteger.java,AtomicBoolean,AtomicLong)。下面以 AtomicInteger.java 的部分实现来大致讲解下这些原子类的实现。

一般称这些类为原子类。(操作数据具有原子性)

我们知道,悲观锁的效率是不如乐观锁的,上面说了Atomic下的原子类的实现是类似乐观锁的,效率会比使用 synchronized 关系字高

AQS内部通过一个CLH阻塞队列去维持线程的状态,并且使用LockSupport工具去实现线程的阻塞和和唤醒,同时里面大量运用了无锁的CAS算法去实现锁的获取和释放。

Unsafe类是CAS的核心类,提供硬件级别的原子操作

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。

AQS(抽象的队列式的同步器)

AQS定义了一套多线程访问共享资源的同步器框架

 

5:线程安全性问题解决方案

大家保证线程安全的方式都有:

1,使用线程安全的类,

2,使用锁,

3,避免使用和设置成员变量类,保持无状态等;

4,使用关键字保证线程安全(例:volatile)等

一般的处理方案有:

线程安全性问题:

解决方案:

        Synchronize

        Volatile

但是:volatile只能保证可见性和有序性,不能保证原子性,

自增,并不是一个原子性操作。

 

那么我们需要学习另外一种解决原子性的线程安全的方法:就是原子类。也是CAS的

原子类能够提供原子性操作。

因此可以使用原子类来解决线程安全性问题。(自增 ++ 或者 减减操作)

这个是jdk5的时候才出现的。

如何使用:jdk提供了很多的原子类

Java.util.concurrent.atomic      atomic 原子的,这个包下的类都是原子如:IntegerAutomic,LangAutomic等。

6:名词解释 join

线程通信之:join (加塞线程)

其实这个方法用的并不多。

Thread中,join()方法的作用是调用线程等待该线程完成后,才能继续用下运行。

也可以这样理解:

Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

public class JoinTest {
    public static void main(String [] args) throws InterruptedException {
        ThreadJoinTest t1 = new ThreadJoinTest("小明");
        ThreadJoinTest t2 = new ThreadJoinTest("小东");
        t1.start();
        /**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
         程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
         所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
         */
        t1.join();
        t2.start();
    }
}

上面程序结果是先打印完小明线程,在打印小东线程;  

上面注释也大概说明了join方法的作用:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

join方法必须在线程start方法调用之后调用才有意义。

join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。

基本上用不到join方法的,这里知道一下就行了。

7:并发工具类

并发的工具类:

7.1:CountDownLatch

一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。

比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

 

其实之前看过一个线程的方法,作用和这个CountDownLatch相似:都是等一个状态到了之后在执行。

Thread.activeCount() 判断存活线程

其实也就是

while(Thread.activeCount()  > 1){

}

做一个空的轮询,但是这个线程比较耗资源,所以我们用CountDownLatch。

其实CountDownLatch里面维护的就是一个锁存器的一个计数。

7.2:CyclicBarrier

它和CountDownLatch非常的相似,

他们之间的不同点在于:

1:CountDownLatch维护了一个寄存器的计数,当计数等于0时,其他的处于wait状态的线程可以执行。

2:而对于CycliBrrier它相当于一个所谓的屏障。

可以理解成开会,下午3点全体员工开会,

直到所有人都来了之后,开始开会。

来的早的人,就休息会(wait),一直等到3点就可以开会了。

它的使用也是非常简单的。

7.3:Semaphore

Semaphore:信号量

通过Semaphore,它就能够控制这个方法能同时被多少个线程所访问。

非常有用。

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

7.4:Exchanger

Exchanger:交换者

用于线程间数据的交换。

它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

如果两个线程有一个没有到达exchange方法,则会一直等待,

如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeout, TimeUnit unit)设置最大等待时长。

总结这几个并发工具类:

1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

8:线程池

什么情况下需要用到线程池?

1、需要大量的线程来完成任务,且完成任务的时间比较短。

例如,WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。这是因为单个任务小,而任务数量巨大,可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现内存溢出(OutOfMemory)的错误。

原文链接:http://ifeve.com/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E7%9A%84%E4%BD%BF%E7%94%A8%E5%92%8C%E7%90%86%E8%A7%A3%E7%BA%BF%E7%A8%8B%E6%B1%A0/

平时接触过多线程开发都或多或少了解过线程池,之前发布的《阿里巴巴 Java 手册》里也有一条:

可见线程池的重要性。

简单来说使用线程池有以下几个目的:

  • 线程是稀缺资源,不能频繁的创建。
  • 解耦作用;线程的创建于执行完全分开,方便维护。
  • 应当将其放入一个池子中,可以给其他任务进行复用。

线程池原理

谈到线程池就会想到池化技术,其中最核心的思想就是把宝贵的资源放到一个池子中;每次使用都从里面获取,用完之后又放回池子供其他人使用,有点吃大锅饭的意思。

那在 Java 中又是如何实现的呢?

在 JDK 1.5 之后推出了相关的 api,常见的创建线程池方式有以下几种:

  • Executors.newCachedThreadPool():(有缓存的线程池)无限线程池。
  • Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
  • Executors.newSingleThreadExecutor():创建单个线程的线程池。

其实看这三种方式创建的源码就会发现:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

实际上还是利用 ThreadPoolExecutor 类实现的。

所以我们重点来看下 ThreadPoolExecutor 是怎么玩的。

首先是创建线程的 api:

corePoolSize:表示池中所保存的线程数(包括空闲线程)

maximumPoolSize:池中允许的最大线程数

keepAliveTime 和 unit 则是线程空闲后的存活时间。

workQueue 用于存放任务的阻塞队列。

handler 当队列和最大线程池都满了之后的饱和策略。

threadFactory:执行程序创建新线程时使用的工厂

在JDK1.7的API中,关于ThreadPoolExecutor的使用有着一样一个说明:

强烈建议程序员使用较为方便的 Executors 工厂方法

Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)

Executors.newFixedThreadPool(int)(固定大小线程池)

Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预定义了设置。

如何配置线程

流程聊完了再来看看上文提到了几个核心参数应该如何配置呢?

有一点是肯定的,线程池肯定是不是越大越好。

通常我们是需要根据这批任务执行的性质来确定的。

  • IO 密集型任务:由于线程并不是一直在运行,所以可以尽可能的多配置线程,比如 CPU 个数 * 2
  • CPU 密集型任务(大量复杂的运算)应当分配较少的线程,比如 CPU 个数相当的大小。

当然这些都是经验值,最好的方式还是根据实际情况测试得出最佳配置。

四种创建线程池的方式:

newSingleThreadExecutor——创建一个单线程的线程池。

这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool——创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newCachedThreadPool——创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统能够创建的最大线程大小。

newScheduledThreadPool——创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池技术涉及到的类和方法比较多,这里简单说明下,之后我会在写个博客整理下线程池中的相关技术。【待定】

猜你喜欢

转载自blog.csdn.net/u010953880/article/details/87856003
今日推荐