Core Java Concurrency 多线程小手册(基本涵盖多线程所有要点)

关于 Java Concurrency

 
   自从 Java 诞生之时, Java 就支持并行的概念,比如线程和锁机制。这个教程帮助开发多线程 Java 程序员能够理解核心的 Java 并行理念以及如何使用他们。 内容涉及到 Java 语言中的线程, 重练级以及轻量级同步机制 以及 JavaSE 5 中的锁,原子量 并行容器,线程调度 以及线程执行者。 开发人员使用这些知识能够开发好并发线程安全的 Java 应用程序。  

Java 并行的概念( Java Concurrency Concepts

 

概念

描述

Java 内存模型

JavaSE5 JSR133 规范中详细定义了 Java 内存模型 Java Memory Model JMM ),该模型定义了相关的操作 比如读 , 写操作,以及在监视器上的同步。 这些操作按 Happens-before 的顺序。

这个定义保证了一个线程可以看到另一个线程操作的结果,同时保证了同步的程序, 以及如何定义一个不变的属性 等等。

监视器

Java 中,任何一个对象都有一个监视器,来排斥共享访问临界区域的代码。这些临界区可以是一个方法 或者是一段代码块,这些临界区域作为同步块。线程只有获取该监视器才能执行同步块的代码。当一个线程到达这块代码是,首先等待来确定是否其他线程已经释放这个监控器。监控器除了排斥共享访问,还能通过 Wait Notify 来协调线程之间的交互。

原子属性

除了 Double long 类型,其他的简单类型都是原子类型。 Double long 类型的修改在 JVM 分为两个不封。为了保证更新共享的 Double Long 类型,你应该将 Double long 的属性作为 Volatile 或者将修改代码放入同步块中。

竞争情况

当许多线程在一系列的访问共享资源操作中,并且结果跟操作顺便有关系的时候,就发生了竞争情况。

数据竞争

数据竞争涉及到当许多线程访问不是 non-final 或者 non-volatile 并没有合适的同步机制的属性时, JMM 不能保证不同步的访问共享的熟悉。数据竞争导致比个预知的行为。

 

自公布

 

还没有通过构造方法实例化对象之前,把这个对象的引用公布时不安全的。

一种是通过注册一个监听器,当初始化的时候回调来发布引用。

另一种是在构造方法里面启动线程。这两种都会导致其他线程引用部分初始化的对象。

Final 属性

Final 属性必须显示的赋值,否则就会有编译错误。一旦赋值,不能被修改。将一个对象引用标记为 Final 只能保证该引用不会被修改,但该对象可以被修改。比如一个 Final ArrayIist 不能改变为另一个 ArrayList 但你可以添加或者修改这个 List 的对象。在构造方法之后,一个对象的 Final 属性是冻结的,保证了对象被安全的发布。其他线程可以在构造方法时看到该变量,甚至在缺乏同步的机制下。

 

不变对象

Final 属性从语义上能够保证创建不变对象。而不变对象可以再没有同步机制下多线程共享和读取。为保证该对象是不变的,必须保证如下:

这个对象被安全的发布, this 引用不能在构造方法的时候被发布

所有的属性都是 Final

应用的对象必须保证在构造方法之后不能被修改。

这个对象需要声明为 Final 保证子类违法这些原则。

 

 

Protecting shared data

保护共享的数据

 

    线程安全的程序需要开发人员在需要修改共享的数据时使用合适的锁机制。锁机制建立的

适合 JMM 的顺序,保证对于其他程序的可视性。

  当在同步机制外修改共享的 data 时,JMM不能保证其一致性。 JVM 提供了一些方法来保证其可视性。

 

Synchronized

 

   每一个对象实体都有一个监视器(来之于 Object 对象),这个监视器能被再某一线程中锁定。 Synchronized 关键字来指定在方法或者代码块上持有该对象监视器上的锁定。 当某一线程同步修改一属性,后续线程将能看到被该线程修改的数据。

 

Lock

 

java.util.concurrent.locks 包提供了 Lock 的接口, ReentrantLock 实现了类似 Synchronized 关键字的功能。同时还提供了额外的功能,比如不是阻塞的 tryLock() 方法和释放锁。

 

public   class  Counter  {

private   final  Lock lock  =   new  ReentrantLock();

private   int  value  =   0 ;

public   int  increment()  {

lock.lock();

try   {

return   ++ value;

}
  finally   {

lock.unlock();

}


}


}

 

同时,在多线程高冲突的情况下, ReentrantLock 要比 Synchronized 效率好。

ReadWriteLock

 

java.util.concurrent.locks 包提供了一个读写锁的接口,这个接口定义了读和写的一对锁,

一般允许并行的读和排他的写。下面的代码展示了上述功能。

 

public   class  Statistic  {

private   final  ReadWriteLock lock  =   new  ReentrantReadWriteLock();

private   int  value;

public   void  increment()  {

lock.writeLock().lock();

try   {

value
++ ;

}
  finally   {

lock.writeLock().unlock();

}


}


public   int  current()  {

lock.readLock().lock();

try   {

return  value;

}
  finally   {

lock.readLock().unlock();

}


}


}

 

Volatile

 

Volatile 关键字使其属性对于后续的线程的可见性。

 

 

public   class  Processor  implements  Runnable  {

private   volatile   boolean  stop;

public   void  stopProcessing()  {

stop 
=   true ;

}


public   void  run()  {

while ( !  stop)  {

//  .. do processing

}


}


}

 

注意:将 array 标记为 Volatile 不能保证数组里面元素的 Volatile ,只能保证数组的引用时

可见的。使用 AtomicIntegerArray 来保证整个数组都是可见的。

 

原子类

 

Volatile 的缺点是只能保证可见性。不能保证修改结果的可见性。而 java.util.concurrent.atomic

包包含了一组支持原子操作的类来弥补 Volatile 的不足。

 

public   class  Counter  {

private  AtomicInteger value  =   new  AtomicInteger();

public   int  next()  {

return  value.incrementAndGet();

}


}

 

ThreadLocal

     ThreadLocal 存贮了该 线程所需要的数据,不需要锁的机制。一般而言, ThreadLocal 存放当前的事务和其他资源等。如下代码, TransactionManager 中, ThreadLocal   类型的 currentTransaction 存贮了当前事务。


public   class  TransactionManager  {

private   static   final  ThreadLocal < Transaction >  currentTransaction  =

new  ThreadLocal < Transaction > ()  {

@Override

protected  Transaction initialValue()  {

return   new  NullTransaction();

}


}
;

public  Transaction currentTransaction()  {

Transaction current 
=  currentTransaction.get();

if (current.isNull())  {

current 
=   new  TransactionImpl();

currentTransaction.put(current);

}


return  current;

}


}



并行容器

     合理维护共享数据一致性的核心技术是在访问数据时采取同步机制。这种技术使得所有访问共享数据的方式保证了同步的原则。 java.util.concurrent 提供了可以并行使用的数据结构。通常而言,使用这些数据结构优于通过 Synchronized 包装的非同步集合。

同步的 lists and sets

 

描述

CopyOnWriteArraySet

 

CopyOnWriteArraySet

提供 Copy-On-Write 的语义 即:每当修改某一数据时在整个容器内容拷贝上修改,然后将该备份同步入容器。

 

CopyOnWriteArrayList

 

类似 CopyOnWriteArraySet

 

ConcurrentSkipListSet

 

JSE6 提供的并行访问可以排序的 Set

 

 

并行   maps

 

java.util.concurrent 扩展 map 接口,提供了名叫 ConcurrentMap 的并行 Map

如下面所有的操作都是原子性的。

 

方法

描述

putIfAbsent(K key, V value) : V

 

如果 Key 没有在该 Map 中,将 Key Value 存入。

否则不做任何处理。

如果没有该 Key 返回 Null

如果有 返回以前的值。

remove(Object key, Object value)

: boolean

 

如果 Map 中包含该 key 则移出该 Value 否则不做任何操作。

replace(K key, V value) : V

 

如果 Map 中有该 Key 则用该 Value 值替换久值。

replace(K key, V oldValue, V

newValue) : boolean

 

如果 Map 中有该 Key 且值为 oldValue 时,用 newValue 替换该久值。

 

下面是具体实现类:

 

 

描述

ConcurrentHashMap

 

内部的 segment 实现了并行的读取。

ConcurrentSkipListMap

 

JSE6 提供的并行访问可排序的 Map

 

 

Queues

 

    作为生产者于消费者管道的 Queues ,生产的条目从一头放入,然后从另一头取出,典型的先进先出的顺序。 Queues 接口在 JSE5 加入 java.util 包里,应用於单线程的环境。最主要用于多生产者消费者的情况下。所有的读写操作都在同一 Queue 上。 Java.util.concurrent 包的 blockingQueues 接口扩张了 Queue 并处理了 Queue 可能已经被生产者添加慢的情况,或者消费者已经读取或者取出完, Queue 为空的情况。 在这些情况下, BlockingQueue 提供了阻塞的机制。可以设定阻塞的时间或者阻塞的条件。

下表反应了 Queue BlockingQueue 对处理特殊条件下的不同策略。

策略

插入

移除

检查

Queue

扔出异常

Add

remove

element

返回特定的值

Offer

poll

peek

Blocking Queue

永远的阻塞

Put

take

n/a

在设定的时间阻塞

Offer

poll

n/a

 

下面是具体的实现类。

 

PriorityQueue

唯一非并行的 Queue 。用於单线程 处理排序的集合。

ConcurrintlinkedQueue

没有容量限制的的并行实现,不支持阻塞。

ArrayBlockingQueue

基于数组 有容量限制的 阻塞 Queue

LinkedBlockingQueue

最通用的实现阻塞容量限制的 Queue

PriorityBlockingQueue

相对于先进先出,该 Queue 的顺序基于 Comparator 的优先级别,没有容量限制。

DelayQueue

没有容量限制的 Queue ,有一个延迟值。

只有延迟时间超过时才能被移除。

SynchronousQueue

容量为 0 的队列,只到下一个到来之前,生产者和消费者被阻塞。适合在线程中交换数据。

 

 

Deques

 

     Deques JSE6 加入,为双头 Queue 。它不仅支持在对头添加,在队尾移除的功能,还提供在双头添加和移除。类似于 BlockingQueue ,也有一个 BlockingDeques 提供阻塞和超时的 Deque

下表为 Deque BlockingDeque 对于具体方法的策略。

 

接口

First 或者 Last

策略

插入

移除

检测

Deque

Head

扔出异常

addFirst

removeFirst

getFirst

返回特定值

offerFisrt

pollFirst

peekFirst

Tail

扔出异常

addLast

RemoveLast

getLast

返回特定值

offerLast

PollLast

PeekLast

BlockingDeque

Head

永远阻塞

putFirst

takeFirst

N/A

在一定时间段内阻塞

offerFirst

pollFirst

N/A

Tail

永远阻塞

PutLast

takeLast

N/A

在一定时间段内阻塞

offerLast

pollLast

N/A

 

 

    对于 Deque 特殊的用法就是添加移除和检测发生在队列的末端。这种用法类似于栈 ( 先进后出顺序 ) 。事实上, Deque 也提供了类似的方法, push() pop() peek(). 这些方法被映射到 addFirst()

removeFirst() PeekFirst().

下表为 JDK 提供的实现类。

描述

LinkedList

JSE6 中重新设计实现了 Deque 接口。

实现了非同步了堆栈。

ArrayDeque

不支持并行 容量不限的 Deque

LinkedBlockingDeque

唯一支持并行的 Deque

 

 

线程

 

Java 中, java.lang.Thread 被用来表述一个应用或者 JVM 线程。在代码中,常用 Thread.currentThread() 来获得当前线程。

 

下表为线程的相关方法

 

线程方法

表述

Start

启动一个线程 执行 Run 方法。

Join

阻塞当前线程直到其他线程退出。

interrupt

中断其他线程。如果一个线程正在被阻塞,如果试图去 Interrupt 这个线程,这个线程会泡出 InterruptedException ,否则,置为 interrupt 状态。

Stop suspend resume destroy

这些方法已经不赞成使用。这些危险的操作依赖于线程的状态。可以使用 interrut volatile 标记来实现。

 

Uncaught exception handlers

 

如果一个线程添加一个 UncaughtExceptionHandlers, 如果该线程被非安全终止时会收到通知。 如下代码:

 

Thread t  =   new  Thread(runnable);

t.setUncaughtExceptionHandler(
new  Thread.

UncaughtExceptionHandler() 
{

void  uncaughtException(Thread t, Throwable e)  {

//  get Logger and log uncaught exception 

}


}
);

t.start();

 

 

死锁

    当线程相互等待资源,而这些资源又被相互持有是,死锁就发生了。最明显的资源就是对象的监控器。但是可以引起阻塞(比如 Wait notify )的任何资源也可以引起死锁。

最新的 JVM 能够检测死锁,并在线程的 Dump 中打印死锁信息。

 

另外的死锁情况是,饥饿线程和活锁。 饥饿线程是指一些线程长时间持有锁而是某些线程处于饥饿状态而不处理真的业务。活锁是指线程花费大量时间检测资源 避免死锁而不是真正处理业务逻辑。

 

线程交互

 

Wait/notify

 

Wait/nofify 是最合理的方式来处理一个线程子在一定条件下通过一个信号来通知另一线程,特别是代替在循环里通过 Sleep 来检测条件的方式。比如,一个线程可能等待一个需要处理的队列,当需要处理的内容添加到队里中时,而另外一个线程会通知等待的线程。

 

规范的用法如下:

 

public   class  Latch  {

private   final  Object lock  =   new  Object();

private   volatile   boolean  flag  =   false ;

public   void  waitTillChange()  {

synchronized (lock)  {

while ( !  flag)  {

try   {

lock.wait();

}
  catch (InterruptedException e)  {

}


}


}


}


public   void  change()  {

synchronized (lock)  {

flag 
=   true ;

lock.notifyAll();

}

 

关于上面的代码,需要着重说明的如下:

一定要在同步锁中调用 call notify notifyall.

Wait 一定要在循环中检测等待条件。

一定要在调用 notify 或者 notifyAll 之前改变条件,否则即使通知了其他线程也无法退出循环。

 

Condition

 

    在 JSE5 中,新添加了 java.util.concurrent.locks.Condition 类。该类在语义上实现了 wait notify 的功能,同时添加了额外的功能,比如每个锁可以有多个条件,中断的等待,访问统计等。 Conditon 通过锁的实例获得。

 

public   class  LatchCondition  {

private   final  Lock lock  =   new  ReentrantLock();

private   final  Condition condition  =  lock.newCondition();

private   volatile   boolean  flag  =   false ;

public   void  waitTillChange()  {

lock.lock();

try   {

while ( !  flag)  {

condition.await();

}


}
  finally   {

lock.unlock();

}


}


public   void  change()  {

lock.lock();

try   {

flag 
=   true ;

condition.signalAll();

}
  finally   {

lock.unlock();

}


}


}

 

 

 

Coordination

 

java.util.concurrent 包包含几个常用的多线程交互类,这些类基本覆盖了常用的情况。通常使用这些类比使用 wait/notice 更安全。

 

CyclicBarrier

 

CyclicBarrier 以特定的值对计数器初始化。参与的线程调用 await() 方法时,如果没有没有达到计数器的初始化的值时,该线程被阻塞。直到计数器达到特定的值时,所有阻塞的线程被释放。 CyclicBarrier 可以重复设置,来调整一组线程的启动与停止。

 

CountDownLatch

CountDownLatch 以特定的值初始化,线程调用 await() 时,如果计数器没有减少到 0 时,该线程被阻塞。其他线程可以调用 countDown() 方法减少计数器。当计数器减少为 0 时,该 CountDownLatch 不能重新设置计数器而重用。

 

Semaphore

Semaphore 管理一组许可证,线程通过调用 acquire() 方法来检测是否有许可,如果没有被阻塞,线程可以调用 release() 方法来释放许可证。 Semaphore 等价于互斥排他锁。

 

Exchanger

Exchanger 等待线程调用 exchange() 方法来交互数据,类似使用 SynchronousQueue ,通过它交互数据是双向的。

 

Task Execution

 

许多 java 多线程程序需要一组工作线程从队列中取出任务来执行。 Java.util.concurrent 包对这种工作线程提供了可靠的基础。

 

ExecutorService

 

  Exccutor 和更易扩展的 ExecutorService 接口定义了相关的方法来执行任务。通过使用这些接口可以得到众多各式各样的实现。

 

最基本的 Executor 接口只接受 Runnable 的任务。

void execute(Runnable command)

ExecutorService 继承了 Executor 并添加了方法支持 Runnable Callable 的任务。

 Future<?> submit(Runnable task)

Future<T> submit(Callable<T> task)

Future<T> submit(Runnable task, T result)

List<Future<T> invokeAll(Collection<? extends

Callable<T>> tasks)

List<Future<T> invokeAll(Collection<? extends

Callable<T>> tasks, long timeout, TimeUnit unit)

T invokeAny(Collection<? extends Callable<T>> tasks)

T invokeAny(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

 

Callable and Future

 

一个 Callalbe 类似于 Runnable ,但它可以有返回值或者扔出异常。

V call() throws Exception;

通用的执行框架提交一个 Callalbe 并受到一个 Future 一个 Future 被标记为返回结果值。 Future 有方法来轮询或者阻塞直到结果已经返回。你可以在执行之前或者执行时取消一个任务。

 

如果你想让 Runalbe 来支持 Future ,你可以使用 FutureTask 作为桥梁。 FutureTask 实现了 Future Runnable ,所以你可以提交一个 Runnalbe 的任务,并作为 Future 来取得结果。

 

ExecutorService 的实现

对于 ExecutorService 接口最主要的实现是 ThreadPoolExecutor 。这个实现类实现了各种可配置的如下功能。

  • 线程池 配置核心线程数量,预先启动,最大线程数。
  • 线程工程 生成特殊定制的线程 , 比如线程名等。
  • 工作队列 制定队列的实现,这个队列是阻塞的,但可以是有界性或者无界限的。
  • 拒绝的任务 可以指定策略来拒绝任务,比如 Queue 已经没有空余,或者没有有效的工作线程。
  • 生命周期的钩子 类似拦截器可以在 Task 的生命周期添加功能,比如在工作开始于完成之间插入现有的功能。
  • 关闭 停止接受提交的任务,直到所有的任务被完成。

 

ScheduledThreadPoolExecutor 扩展了 ThreadPoolExecutor, 提供了对任务调度的功能而不是先进先出。在这点上 Java.util.Timer 不能足够的,而 ScheduledThreadPoolExecutor 经常提供了足够的弹性。

 

Executors 类提供了许多静态方法来创建包装好的 ExecutoService ScheduledExccutorService 实例。

方法

描述

newSingleThreadExecutor

返回一个线程的 ExecutorService

newFixedTreadPoll

固定数量的线程池

newCachedThreadPoll

大小变化的线程池

newSingleThreadScheduledExecutor

单线程的 ScheduledExecutorService

newScheduledThreadPool

一组线程的 ScheduledExecutorService

 

下面是对固定大小线程池的使用,提交长时间运行的任务。

 

int processors = Runtime.getRuntime().availableProcessors();

ExecutorService executor = Executors.

newFixedThreadPool(processors);

Future<Integer> futureResult = executor.submit(

new Callable<Integer>() {

public Integer call() {

// long running computation that returns an integer

}

});

Integer result = futureResult.get(); // block for result

 

在上述列子中,调用者提交了向执行者提交了长时间运行的任务,并立即返回。在结果还没有返回之间,调用 get() 方法会被阻塞。

ExecutorService 基本覆盖了所有的情况。

 

CompletionService

 

除了通常我们将任务提交到线程池的 Queue 之外,我们还需要每一个任务生产结果,并为日后处理。

CompletionService 接口允许使用者提交 Callalbe Runnable 的任务,同时在结果队列中取出或者轮询结果

Future<V> take() – take if available

Future<V> poll() – block until available

Future<V> poll(long timeout, TimeUnit unit) – block

until timeout ends

ExecutorCompletionService CompletionService 接口的标准实现。构造方式类似于 Executor

提供输入队列和工作线程池。

重要提示:对于线程池大小的设定,一般采用逻辑上处理器数量。在 Java 中,通过 Runtime.getRuntime().avaiableProcessors() 来获取有效的处理器数量,这个数量可能在 JVM 运行时被修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章