ConCurrent in Practice小记 (1)#

杂记,随书自己写的笔记:

综述问题##

  • 1、线程允许在同一个进程中的资源,包括共享内存,内存句柄,文件句柄。但是每个进程有自己的程序计数器,栈和局部变量。

  • 2、安全问题:线程不安全本质上是由于单线程在在从共享内存或者文件中取得自己的资源后,在寄存器处理器缓存等地方对于线程外部是不可见的;因此当变量再次读取或写入到内存或者是文件中后就会产生冲突。

  • 3、活跃性问题:即因为资源占用错误而是的程序无法得到正确的结果,典型如死锁、饥饿、活锁。

  • 4、性能问题:多线程的切换上下文,同步机制造成的阻塞等等。

进程和线程##

进程:###

有独立的地址空间,包含文本域,数据域,堆栈。其中:

  • 文本域存储执行器执行的代码;
  • 数据域存储变量和进程执行期间的动态分配的内存
  • 堆栈域存储活动过程中调用的指令和本地变量

线程:###

有自己的堆栈和变量,共享内存地址,即没有自己的地址空间。所以一个线程包含一下几个方面:

  • 一个指向当前被执行指令的指针;

  • 一个私有堆栈;

  • 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态(资源非共享);

  • 一个私有的数据域

    线程的状态####

线程共有六种状态

  • New 新创建,需要做初始化工作,并没有具体执行程序代码。

  • Runnable 可运行,在调用start方法后,就处于该种状态,由于线程是容易被(线程调度?)打断的,所以并非在runnable状态中就是一直在执行的。线程被打断通常是其他线程执行任务。

  • Blocked 阻塞,阻塞通常是由于同步的问题导致多个线程对共同的请求而导致阻塞。即当线程A试图得到一个内部的对象锁(synchronized)而该锁正被另一个线程持有时,线程处于阻塞状态,仅当该锁被释放且线程调度器允许线程A持有内部锁时,线程A转变为非阻塞状态,为runnable状态。
    (这里的锁是synchronized的内部锁,在java.util.concurrent中也有锁,两者意义不同,这里指前者)

    (14.5.3) (14.5.5)

  • Waiting 等待,

  • Timed Waiting 计时等待,有几个方法有一个超时参数,当这些方法被调用时会进入计时等待状态,一直到保持超时期满或者收到适当的通知。这种方法有Thread.sleep,Object.wait,Thread.join,Lock.tryLock,Condition.await等等。

  • Terminated 终止,线程因为如下两个原因被终止,

    • run方法正常结束退出,自然死亡
    • 由于一个未被捕获的异常终止了run方法而意外死亡

    线程属性####

线程优先级:分为十级,最低为一,最高为十,一般为五。不要将执行程序的正确性依赖于优先级,优先级过高会导致低优先级的任务饿死。

守护进程:setDaemon,为其他线程提供服务的线程,如计时线程。守护线程永远不该取访问固有资源,如文件和数据库等等,因为它会在任何时候,甚至在一个操作中发生中断。

线程安全性##

基本概念

  • 共享:多个线程可访问;
  • 可变:生命周期内可改变实例内容
  • 同步:主要是使用独占加锁synchronized,还包括volatile类型,Explicit Lock显式锁,原子类型(操作)等等。

线程安全性指每次运行结果即过程中的变量值及状态和单线程下执行的预期一致。

++而线程不安全都是由全局变量和静态变量引起的。若程序中对两者仅有读操作,而没有写操作,一般是线程安全的,如果同时执行写操作,则需要考虑线程同步。++

线程安全的例子:

  • 常量不会改变,只读,基本安全
  • 每次调用方法前都新建一个实例,线程安全,因为不会访问共享资源(存疑)
  • 局部变量是安全的,包括方法的参数变量和方法内变量
  • 无状态变量是线程安全的;无状态变量是指没有状态信息的变量,仅在一次操作中有效,因此对多线程是透明的,不能保存数据。

竞态条件(Race Condition):###

程序的正确执行结果需要依靠多个线程中的操作满足一定的时序,此时相互竞争资源的情况叫做竞态条件。最常见的类型就是“先检查后执行”(Check-Then-Act),在观测和执行之间的时间内可能会观测结果会改变。

先检查后执行的常见情况是延迟初始化,LasyInit。

Example:

public class LasyInitRace{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();
}
return instance;
}
}

此时,Thread A先判断instance是否初始化,而Thread B到达,就会出现竞态条件。容易产生数据丢失。需要将之前的复合操作加上原子特性才可以。java.util.concurrent.atomic提供了很多类。

** 要保证状态的一致性,就必须在原子操作中一次性更新所有有关的状态。同样的被锁的对象中有多个变量状态的也必须由同一个锁来保护.**

内置锁机制##

Java内部由关键词synchronized表示内部互斥锁,该机制使得由其修饰的语句块都成为原子特性。

同步代码块(Synchronized Block)包含两部分内容,一个作为锁的对象引用和一个由该锁保护的代码块。

synchronized关键词永远锁对象,而不是锁代码。静态的synchronized方法锁的是class对象。

互斥与重入:###

由于是互斥锁,会造成活跃性的问题,即同步代码块由当前线程访问时,会阻塞其他线程访问.因此,应当在活跃性和互斥中取得平衡,较好的方式为缩小synchronized的作用域,仅将变量赋值和改写等地方加锁而后释放,而在耗时操作中不做锁操作。

Java内部锁支持重入,即当前的线程获得锁之后在执行代码期间,该线程再次对该方法进行调用请求,仍是可以响应的。所以侧面反应出synchronized关键词是对于(线程)对象引用计数加锁,而不是对调用加锁。(这里同pthread不同POSIX线程)

** Example:**

public class Widget{
public synchronized void doSomething(){
//........
}
} public class LoggingWidget extends Widget{
public synchronized void doSomething(){
System.out.println(toString() + " : call doSomething ");
super.doSomething();
}
}

如果不能重入,那么当LoggingWidget中的synchronized方法得到Widget的锁之后,执行父类的方法就会死锁。(本质上是因为某些继承来的变量和方法需要锁,所以父类也加上了锁.)

ReentrantLock机制##

ReentrantLock机制的用途和设计目的和synchronized关键字一样,但是实现的时候是用的一个普通的类实现,和语言本身特性无关,因此可以被继承和重写以实现自己的效果。

ReentrantLock和synchronized不同在于

  • 1、后者在因为资源等待时刻,线程如果被唤醒就是打断(中止),但是在前者中,等待的线程可以在等待一段时间之后继续进行其他的工作。无需一直等待。

  • 2、ReentrantLock在使用的时候是直接构造一个ReentrantLock对象,而不是使用的对象的内部锁。所以在最终需要释放锁,而synchronized同步锁则是由JVM自动释放的。典型的应用如下:(记得必须释放锁)

Lock lock= new ReentrantLock();
lock.lock();
try{
//update object state
}catch{
//catch the exception
}finally{
lock.unlock();
}
  • 3、效率更高,且可以设计公平性。公平性是指在等待队列中,等待时间更长的线程更有可能得到锁(资源),synchronized本身是不公平的,考虑优先级的。在java.util.concurrent.locks包中ReentLock可以设计公平性,但是公平性的锁机制效率很低。

Condition###

Condition对象体现了Object中有关监听器的wait(),notify(),notifyAll()等同ReentrantLock对象的联合运用。即不仅可以绑定到一个锁对象上,还可以多个对象绑定一个锁。

包中的原文解释:

	条件(也称为条件队列或者是条件变量)为线程提供了一个含义,以便在某个状态现在可能为true的另一个线程通知它之前,一致挂起该线程(即让其“等待”)。因为访问次共享状态信息发生在不同的线程中,所以它必须受保护,因此将某种形式的锁与该条件关联。等待提供一个条件的属性是:以原子方式释放相关的锁,并挂起当前线程,类似Object.wait一样。

Condition对象是绑定到一个锁上的,因此需要为一个锁new一个新的condition对象。

实现一个Buffer,官方示例:

class BoundedBuffer{
final Lock lock = new ReentrantLock(); //final变量本身的声明和初始化是安全的
final Condition notFull = new Condition();
final Condition notEmpty = new Condition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterrupteException{
lock.lock();
try{
while(count == items.length){
notFull.await();
}
items[putptr] = x;
if(++putptr == item.length) putptr = 0;
++count;
not.Empty.signal();
}finally{
lock.unlock();
}
}
public Object take() throws InterrupteException{
lock.lock();
try{
while(count == items.length){
notEmpty.await();
}
items[takeptr] = x;
if(++takeptr == item.length) takeptr = 0;
--count;
notFull.signal();
return x;
}finally{
lock.unlock();
}
}
}//该功能在ArrayBlockingQueue中有体现。

== 注意:Condition对象也是普通类的对象,因此也可以有内部锁和wait().notify(),notifyAll()方法,两者一般不要混用。==

对象的共享##

同步的意义不仅在于对于变量的读写保护,也在于可见性,即当某个线程拿到变量并进行修改时,其他变量可以得到该信息,其他线程可以查看当前线程的处理结果。所有执行读操作和写操作都必须在同一个锁上同步。

最低安全性:当读写操作的时序出现问题没有同步时,则至少得到是某一个线程在执行过程中设置的值,而不是一个随机值,这种安全性保证称之为“最低安全性”。

volatile在Java中有两种意义:

  • 保证对线程的可见性
  • 保证编译器不进行优化

volatile变量:volatile关键字的意义就在于保持了变量对线程的可见性。volatile修饰的变量在各自线程中的缓存副本无效,但是并不保证同步。

** Example:**

public class ThreadPool {
private volatile int n = 0; public void increase(){
n++;
} public static void main(String[] args){
final ThreadPool test = new ThreadPool();
for (int i = 0; i < 10; i++){
new Thread(){
public void run(){
for (int i = 0; i < 1000; i++){
test.increase();
}
}
}.start();
} while (Thread.activeCount() > 1 ){
Thread.yield();
}
System.out.println(test.n);
}
}

结果在并不每次都是10000。volatile保证的是当线程A修改变量的值后立即写入内存,而且同时通知其余的线程变量以更新状态(本质上是置CPU中的缓存无效)。那么其余的线程在取变量值的时候必须去内存中取。

== 在线程A取得变量还未修改时,线程B也取得变量,那么两者取到的值是一样的,且操作是一样的,因此最终结果和只执行了一个线程是一样的,这里的变量数会少于10000.==

一般当且仅当满足以下条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者只有单个线程更行变量的值
  • 该变量不会与其他状态变量一起纳入到不变性条件中
  • 在访问时不需要加锁

阻塞队列##

阻塞队列是一种使用数组为基础的缓冲区队列,其基本功能和上面的Condition中的示例代码一致。由于先进先出的队列的性质,间接地实现了线程安全,而阻塞队列中增加了空队列取阻塞和满队列入阻塞。

理论上对于一个阻塞队列,一个线程将共享数据put进队列,另一个线程取出队列中的对象,因此同时仅有一个线程操作对象,也是一种同步安全的机制。

阻塞队列和线程池的大小关系极大,前者就是存放任务的,后者是执行任务的worker的多少,总结如下:

  • 如果运行的线程数小于corePoolSize,Executor始终首选新开线程,而不是排队。即任务根本不会存放,不会被添加到Queue中,直接开线程。
  • 而当当前运行线程数大于corePoolSize则不会优先排队,不会开新的线程。
  • 如果队列已满,任务无法被添加至队列中,则会创建新的线程,除非此时运行的线程数已经达到maximumPoolSize,这时任务会被拒绝。

三种排队策略:SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue###

三者都是对BlockingQueue的实现,但本身体现了三种不同的排队策略。

  1. 直接提交:SynchronousQueue,该队列没有容量(!是的,可以认为容量是1),典型的生产者-消费者模型。该种策略直接把任务提交给线程,而不进行缓存,当消费者没有将任务取走时,则加入队列失败,同上面第三条,开一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁,直接提交的策略通常要求无界maximumPoolSize以避免拒绝新的任务。因为队列平均处理长度是1,所以线程具有无限增长的可能性。
  2. 无界队列:LinkedBlockingQueue,该队列是链表实现的无界队列,认为具有无限的缓冲区。所以当所有的运行线程达到corePoolSize时,任务开始在队列中缓存,所以maximumPoolSize无效,线程数目不会大于corePoolSize。
  3. 有界队列:ArrayBlockingQueue,有助于防止资源耗尽,但是较难调整和控制。队列大小和线程池的大小需要折衷考虑:一般地,使用大型队列和小型池可以最大限度地降低CPU的使用率,系统资源开销和上下文切换,但是会导致人工降低吞吐量。

Callable和Future###

Runnable接口封装一个异步运行的任务,可以认为是一个没有返回值和参数的异步方法,而Callable封装同一个类型任务,具有返回值,内部仅有一个方法call().

public interface Callable<V>{
V call() throws Exception;
}

Future接口用来保存一个异步执行的计算结果,可以启动一个计算,将Future对象交给某个线程,Future对象的所有者在结果计算好后就可以获得它。

public interface Future<V>{
V get() throws ...;
V get(long timeout, TimeUnit unit) throws ...;
void cancel(boolean mayInterrupt);
boolean isCancelled();
boolean isDone();
}

控制任务组###

详细的看core java,暂时没弄懂。

基本上是有关ExecutorService的,主要是逻辑方面。

大部分是指在同一任务中,如果需要多次计算得到相同的结果,(并行计算?)无论谁先得到结果或者是等到多个线程执行完得到某一结果再比较得到最优,就使用ThreadGroup。

fork-join框架###

同步器###

给出了几种有可能用到的方法和类,来替代手工写的同步方法和条件集合。主要是解决线程之间的公用集结点模式(common rendezvous patterns)。

Core Java中总结了一个图表:

CyclicBarrier允许线程集中的预定数目的线程到达一个公共屏障(Barrier)时,执行一个处理栅障的动作当大量的线程需要在他们的结果可用之前完成时
CountDownLatch允许线程等待直到计数器减为零当一个或者多个线程需要等待直到制定数目的事件发生
Exchanger允许两个线程在要交换的对象准备好时交换对象当两个工作线程在同一数据结构的两个实例上的时候,一个向实例添加数据,一个向实例清除数据
Semaphore允许线程集直到被允许继续运行为止限制访问资源的线程总数, 如果许可数为1,常常阻塞线程直到另一个线程给出许可为止
SynchronousQueue允许一个线程把对象交给另一个线程在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时

具体的实现实现和内容到下一节可以看到。(Java7 concurrency cookbook)

05-01 04:45