阿里架构师告诉你一些多线程的使用技巧

Java中线程的状态

NEW、RUNNABLE(RUNNING or READY)、BLOCKED、WAITING、TIME_WAITING、TERMINATED

图片描述(最多50字)

Java将操做系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,可是阻塞在JUC包中Lock接口的线程状态倒是等待状态,由于JUC中Lock接口对于阻塞的实现是经过LockSupport类中的相关方法实现的。

线程的优先级

Java中线程的优先级分为1-10这10个等级,若是小于1或大于10则JDK抛出IllegalArgumentException()的异常,默认优先级是5。在Java中线程的优先级具备继承性,好比A线程启动B线程,则B线程的优先级与A是同样的。注意程序正确性不能依赖线程的优先级高低,由于操做系统能够彻底不理会Java线程对于优先级的决定。

守护线程

Java中有两种线程:一种是用户线程,另外一种是守护线程。当进程中不存在非守护线程了,则守护线程自动销毁。经过setDaemon(true)设置线程为后台线程。注意thread.setDaemon(true)必须在thread.start()以前设置,不然会报IllegalThreadStateException异常;在Daemon线程中产生的新线程也是Daemon的;在使用ExecutorService等多线程框架时,会把守护线程转换为用户线程,而且也会把优先级设置为Thread.NORM_PRIORITY。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

构造线程

一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级、ThreadGroup、加载资源的contextClassLoader以及可继承的ThreadLocal(InheritableThreadLocal)、同时还会分配一个惟一的ID来标识这个child线程。

同步不具有继承性

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。同步不具备继承性(声明为synchronized的父类方法A,在子类中重写以后并不具有synchronized的特性)。

使用多线程的方式

extends Thread
implements Runnable
使用Future和Callablejava

Executor框架使用Runnable做为基本的任务表示形式。Runnable是一种有很大局限的抽象,虽然run能写入到日志文件或者将结果放入某个共享的数据结构,但它不能返回一个值或抛出一个受检查的异常。许多任务实际上都是存在延迟的计算——执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,Callable是一种更好的抽象:它认为主入口点(call())将返回一个值,并可能抛出一个异常。Runnable和Callable描述的都是抽象的计算任务。这些任务一般是有范围的,即都有一个明确的起始点,而且最终会结束。

Thread.yield()方法

yield()方法的做用是放弃当前的CPU资源,将它让给其余的任务去占用CPU执行时间。但放弃时间不肯定,有可能刚刚放弃,立刻又得到CPU时间片。这里须要注意的是yield()方法和sleep()方法同样,线程并不会让出锁,和wait()不一样,这一点也是为何sleep()方法被设计在Thread类中而不在Object类中的缘由。

Thread.sleep(0)

在线程中,调用sleep(0)能够释放CPU时间,让线程立刻从新回到就绪队列而非等待队列,sleep(0)释放当前线程所剩余的时间片(若是有剩余的话),这样可让操做系统切换其余线程来执行,提高效率。

The semantics of Thread.yield and Thread.sleep(0) are undefined [JLS17.9]; the JVM is free to implement them as no-ops or treat them as scheduling hints. In particular, they are not required to have the semantics of sleep(0) on Unix systems — put the current thread at the end of the run queue for that priority, yielding to other threads of the same priority — though some JVMs implement yield in this way.面试

Thread.join()

若是一个线程A执行了thread.join语句,其含义是:当前线程A等待thread线程终止以后才从thread.join()返回。join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”作为同步。join提供了另外两种实现方法:join(long millis)和join(long millis, int nanos),至多等待多长时间而退出等待(释放锁),退出等待以后还能够继续运行。内部是经过wait方法来实现的。

wait, notify, notifyAll用法

只能在同步方法或者同步块中使用wait()方法。在执行wait()方法后,当前线程释放锁(这点与sleep和yield方法不一样)。调用了wait函数的线程会一直等待,直到有其它线程调用了同一个对象的notify或者notifyAll方法才能被唤醒,须要注意的是:被唤醒并不表明马上得到对象的锁,要等待执行notify()方法的线程执行完,即退出synchronized代码块后,当前线程才会释放锁,而呈wait状态的线程才能够获取该对象锁。若是调用wait()方法时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,所以,不须要try-catch语句进行捕获异常。notify()方法只会(随机)唤醒一个正在等待的线程,而notifyAll()方法会唤醒全部正在等待的线程。若是一个对象以前没有调用wait方法,那么调用notify方法是没有任何影响的。带参数的wait(long timeout)或者wait(long timeout, int nanos)方法的功能是等待某一时间内是否有线程对锁进行唤醒,若是超过这个时间则自动唤醒。

setUncaughtExceptionHandler

当单线程的程序发生一个未捕获的异常时咱们能够采用try….catch进行异常的捕获,可是在多线程环境中,线程抛出的异常是不能用try….catch捕获的,这样就有可能致使一些问题的出现,好比异常的时候没法回收一些系统资源,或者没有关闭当前的链接等等。Thread的run方法是不抛出任何检查型异常的,可是它自身却可能由于一个异常而被停止,致使这个线程的终结。在Thread ApI中提供了UncaughtExceptionHandler,它能检测出某个因为未捕获的异常而终结的状况。

thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){});数据库

一样能够为全部的Thread设置一个默认的UncaughtExceptionHandler,经过调用Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法,这是Thread的一个static方法。在线程池中,只有经过execute()提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而经过submit()提交的任务,不管是抛出的未检测异常仍是已检查异常,都将被认为是任务返回状态的一部分。若是既包含setUncaughtExceptionHandler又包含setDefaultUncaughtExceptionHandler,那么会被setUncaughtExceptionHandler处理,setDefaultUncaughtExceptionHandler则忽略。

关闭钩子

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

终结器finalize

终结器finalize:在回收器释放它们后,调用它们的finalize方法,从而保证一些持久化的资源被释放。在大多数状况下,经过使用finally代码块和显示的close方法,可以比使用终结器更好地管理资源。惟一例外状况在于:当须要管理对象,而且该对象持有的资源是经过本地方法得到的。可是基于一些缘由(譬如对象复活),咱们要尽可能避免编写或者使用包含终结器的类。

管道

在Java中提供了各类各样的输入/输出流Stream,使咱们可以很方便地对数据进行操做,其中管道流(pipeStream)是一种特殊的流,用于在不一样线程间直接传送数据。一个线程发送数据到输出管道,另外一个线程从输入管道中读数据,经过使用管道,实现不一样线程间的通讯,而无须借助相似临时文件之类的东西。在JDK中使用4个类来使线程间能够进行通讯:PipedInputStream, PipedOutputStream, PipedReader, PipedWriter。使用代码相似inputStream.connect(outputStream)或outputStream.connect(inputStream)使两个Stream之间产生通讯链接。

几种进程间的通讯方式

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

synchronized的类锁与对象锁

类锁:在方法上加上static synchronized的锁,或者synchronized(xxx.class)的锁。以下代码中的method1和method2:

对象锁:参考method4,method5,method6。

public class LockStrategy
{数据结构

public Object object1 = new Object();
public static synchronized void method1(){}
public void method2(){
    synchronized(LockStrategy.class){}
}
public synchronized void method4(){}
public void method5()
{
    synchronized(this){}
}
public void method6()
{
    synchronized(object1){}
}

}多线程

注意方法method4和method5中的同步块也是互斥的。

下面作一道习题来加深一下对对象锁和类锁的理解,有一个类这样定义:

public class SynchronizedTest
{架构

public synchronized void method1(){}
public synchronized void method2(){}
public static synchronized void method3(){}
public static synchronized void method4(){}

}并发

那么,有SynchronizedTest的两个实例a和b,对于一下的几个选项有哪些能被一个以上的线程同时访问呢?

A. a.method1() vs. a.method2()

B. a.method1() vs. b.method1()

C. a.method3() vs. b.method4()

D. a.method3() vs. b.method3()

E. a.method1() vs. a.method3()

答案是什么呢?BE。

ReentrantLock

ReentrantLock提供了tryLock方法,tryLock调用的时候,若是锁被其余线程持有,那么tryLock会当即返回,返回结果为false;若是锁没有被其余线程持有,那么当前调用线程会持有锁,而且tryLock返回的结果为true。

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit)app

能够在构造ReentranLock时使用公平锁,公平锁是指多个线程在等待同一个锁时,必须按照申请锁的前后顺序来一次得到锁。synchronized中的锁时非公平的,默认状况下ReentrantLock也是非公平的,可是能够在构造函数中指定使用公平锁。

ReentrantLock()
ReentrantLock(boolean fair)框架

对于ReentrantLock来讲,还有一个十分实用的特性,它能够同时绑定多个Condition条件,以实现更精细化的同步控制。ReentrantLock使用方式以下:

Lock lock = new ReentrantLock();

lock.lock();
try{
}finally{
    lock.unlock();
}
在finally块中释放锁,目的是保证在获取到锁以后,最终可以释放。不要将获取锁的过程写在try块中,由于若是在获取锁时发生了异常,异常抛出的同时也会致使锁无端释放。IllegalMonitorStateException。

公平锁和非公平锁只有两处不一样

非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,若是这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。

非公平锁在 CAS 失败后,和公平锁同样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,若是发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,可是公平锁会判断等待队列是否有线程处于等待状态,若是有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,若是这两次 CAS 都不成功,那么后面非公平锁和公平锁是同样的,都要进入到阻塞队列等待唤醒。相对来讲,非公平锁会有更好的性能,由于它的吞吐量比较大。固然,非公平锁让获取锁的时间变得更加不肯定,可能会致使在阻塞队列中的线程长期处于饥饿状态。

synchronized

在Java中,每一个对象都有两个池,锁(monitor)池和等待池:

锁池(同步队列SynchronizedQueue):假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),因为这些线程在进入对象的synchronized方法以前必须先得到该对象的锁的拥有权,可是该对象的锁目前正被线程A拥有,因此这些线程就进入了该对象的锁池中。
等待池(等待队列WaitQueue):假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(由于wait()方法必须出如今synchronized中,这样天然在执行wait()方法以前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。若是另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会所有进入该对象的锁池中,准备争夺锁的拥有权。若是另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。

synchronized修饰的同步块使用monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。不管采用哪一种方式,其本质上是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到synchronized所保护对象的监视器。任意一个对象都拥有本身的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

任意线程对Object(Synchronized)的访问,首先要得到Object的监视器。若是获取失败,线程进入同步队列(同步队列SynchronizedQueue),线程状态变为BLOCKED。当访问Object的前驱(得到了锁的线程)释放了锁,则该释放操做唤醒阻塞在同步队列中的线程,使其从新尝试对监视器的获取。

图片描述

图片描述(最多50字)

wait方法调用后,线程状态由Runnable变为WAITING/TIME_WAITING,并将当前线程放置到对象的等待队列(等待队列WaitQueue)中。notify()方法是将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll方法是将等待队列中全部的线程所有移到同步队列,被移动的线程状态由WAITING变为BLOCKED。

图片描述

图片描述(最多50字)

在锁对象的对象头中有一个threadId字段,当第一个线程访问锁时,若是该锁没有被其余线程访问过,即threadId字段为空,那么JVM让其持有偏向锁,并将threadId字段的值设置为该线程的ID。当下一次获取锁时,会判断当前线程ID是否与锁对象的threadId一致。若是一致,那么该线程不会再重复获取锁,从而提升了程序的运行效率。若是出现锁的竞争状况,那么偏向锁会被撤销并升级为轻量级锁。若是资源的竞争很是激烈,会升级为重量级锁。

Condition

一个Condition和一个Lock关联在一块儿,就像一个条件队列和一个内置锁相关联同样。要建立一个Condition,能够在相关联的Lock上调用Lock.newCondition方法。正如Lock比内置加锁提供了更为丰富的功能,Condition一样比内置条件队列提供了更丰富的功能:在每一个锁上可存在多个等待、条件等待能够是可中断的或者不可中断的、基于时限的等待,以及公平的或非公平的队列操做。对于每一个Lock,能够有任意数量的Condition对象。Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会依照FIFO顺序从Condition.await中释放。注意:在Condition对象中,与wait,notify和notifyAll方法对应的分别是await,signal,signalAll。可是Condition对Object进行了扩展,于是它也包含wait和notify方法。必定要确保使用的版本——await和signal。

Condition接口的定义:

public interface Condition{

void await() throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUniterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();

}

AQS 中有一个同步队列(CLH),用于保存等待获取锁的线程的队列。这里咱们引入另外一个概念,叫等待队列(condition queue)。

图片描述

图片描述(最多50字)

基本上,把这张图看懂,你也就知道 condition 的处理流程了:1. 咱们知道一个 ReentrantLock 实例能够经过屡次调用 newCondition() 来产生多个 Condition 实例,这里对应 condition1 和 condition2。注意,ConditionObject 只有两个属性 firstWaiter 和 lastWaiter;2. 每一个 condition 有一个关联的等待队列,如线程 1 调用 condition1.await() 方法便可将当前线程 1 包装成 Node 后加入到等待队列中,而后阻塞在这里,不继续往下执行,等待队列是一个单向链表;3. 调用 condition1.signal() 会将condition1 对应的等待队列的 firstWaiter 移到同步队列的队尾,等待获取锁,获取锁后 await 方法返回,继续往下执行。

ReentrantLock与synchonized区别

ReentrantLock能够中断地获取锁(void lockInterruptibly() throws InterruptedException)
ReentrantLock能够尝试非阻塞地获取锁(boolean tryLock())
ReentrantLock能够超时获取锁。经过tryLock(timeout, unit),能够尝试得到锁,而且指定等待的时间。
ReentrantLock能够实现公平锁。经过new ReentrantLock(true)实现。
ReentrantLock对象能够同时绑定多个Condition对象,而在synchronized中,锁对象的的wait(), notify(), notifyAll()方法能够实现一个隐含条件,若是要和多于一个的条件关联的对象,就不得不额外地添加一个锁,而ReentrantLock则无需这样作,只须要屡次调用newCondition()方法便可。

Lock接口中的方法:

void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();

重入锁的实现原理

为每一个锁关联一个请求计数和占有他的线程

synchronized与ReentrantLock之间进行选择

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

读写锁ReentrantReadWriteLock

读写锁表示也有两个锁,一个是读操做相关的锁,也称为共享锁;另外一个是写操做相关的锁,也叫排它锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有Thread进行写操做时,进行读取操做的多个Thread均可以获取读锁,而进行写入操做的Thread只有在获取写锁后才能进行写入操做。即多个Thread能够同时进行读取操做,可是同一时刻只容许一个Thread进行写入操做。(lock.readlock.lock(), lock.readlock.unlock, lock.writelock.lock, lock.writelock.unlock)

锁降级是指写锁降级成读锁。若是当前线程拥有写锁,而后将其释放,最后获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,最后释放(先前拥有的)写锁的过程。锁降级中的读锁是否有必要呢?答案是必要。主要是为了保证数据的可见性,若是当前线程不获取读锁而是直接释放写锁,假设此刻另外一个线程(T)获取了写锁并修改了数据,那么当前线程没法感知线程T的数据更新。若是当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁以后,线程T才能获取写锁进行数据更新。

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关系来执行的结果一致,那么这种重排序并不非法。

重排序

是指编译器和处理器为了优化程序性能而对指令序列进行从新排序的一种手段。

as-if-serial

无论怎么重排序,单线程程序的执行结构不能被改变。

说到最后给你们免费分享一波福利吧!我本身收集了一些Java资料,里面就包涵了一些BAT面试资料,以及一些 Java 高并发、分布式、微服务、高性能、源码分析、JVM等技术资料

资料获取方式:请加群BAT架构技术交流群:171662117

相关文章
相关标签/搜索