您的位置:首页 > 编程语言 > Java开发

《Java并发编程实战》读书笔记

2015-10-11 23:20 525 查看
自从写了大半点hibernate读书笔记被csdn的渣渣编辑器吞了之后,已经很多天没有再用博客来记录自己的学习了。这段时间深入学习了java并发这一块,收获良多,再次记录。

第二章 线程安全性

1.无状态的一定是线程安全的

无状态的对象: 不包含任何域,也不包含任何对其他类中域的引用。计算过程的临时状态仅存在于线程栈上的局部变量中,并只能由正在执行的线程访问。

2.原子性

++count不是原子的, 包含3个独立的操作,读取count的值,把值+1,把计算结果写入count。

3.竞态条件

当某个计算的正确性取决于多个线程到交替执行顺序时,就会发生竞态条件。

最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步的动作。

在计数器这个问题中可以使用原子类如AtomicLong或者AtomicReference<BigInteger>来统计已处理请求的数量。

4.内置锁与可重入

每个java对象都可以作为一个实现同步的锁,成为内置锁活着监视器锁。在进入同步代码块时自动获得锁,推出时自动释放锁。

内置锁是可重入的,意味着获取锁的操作的粒度是线程,而不是调用。 重入的一种实现方法为:为每个锁关联一个获取计数值和一个所有者线程。

之所以每个对象都有一个内置锁,只是为了免去显示地创建锁对象。

第三章 对象的共享

1.synchronized的作用

synchronized可以用来实现原子性、临界区、内存可见性。

关于可见性,可以看 http://blog.csdn.net/u012422829/article/details/46127827

2.重排序

在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些调整。
volatile和同步将限制编译器和硬件运行时对内存操作重排序的方式。

3.非原子的64位操作

非volatile的64位数值变量如double和long,jvm允许将64位的读操作和写操作分解为两个32位的操作。当读取一个非volatile的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么可能会读取到某个值的高32位和另一个值大低32位。

4.volatile

除了不会重排序,volatile变量不会被缓存在寄存器或者其他对处理器不可见的地方,因此读取volatile类型的变量时总会返回最新写入的值。
volatile不加锁,所以不会阻塞线程。

volatile变量正确使用方式:
确保它们自身状态的可见性,确保它们所引用对象状态的可见性,标识一些重要的程序生命周期事件的发生(例如初始化或关闭)。

当且仅当满足以下所有条件时,才应该使用:
对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值。
该变量不会与其他变量一起纳入不变性条件中。
在访问变量时不需要加锁。

5.this引用逸出

在构造函数中使用内部类,并且代码可能会出现并行。也就是说,当内部类代码执行的时候,外部类对象的创建过程很有可能还没结束,这个时候如果内部类访问外部类中的数据(或者其他线程访问)很有可能得到还没有正确初始化的数据。
所以:

不要在构造函数函数随便创建匿名类然后发布它们。

不在构造函数中随便起线程, 如果起要看有没有发布匿名类对象,不在构造函数内启动。


6.ThreadLocal

这个类使线程中的某个值与保存值的对象关联起来,ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

7.不变性

当满足以下条件时,对象才是不可变的:
对象创建以后其状态不能修改。
对象的所有域都是final类型。(虽然String类不是,但是我们自己定义的话还是都弄成final)
对象是正确创建的(对象创建期间,this引用没有逸出)。

8.安全发布的常用模式

分析以下代码:

public class Holder{

private int n;

public Holder(int n){ this.n=n;}

public void test(){
if(n!=n){ throw new Exception("bug??");
}
}


当调用test的时候,可能是会抛出异常的!
因为在构造函数中,n被赋值,然而别的线程可能获取了还未构造完成的holder对象并调用test方法,第一个n默认值为0,第二个n被读取时,可能已经被执行构造函数的线程修改为正确的n值,所以两个n的值可能是不一样的!

一个正确构造的对象可以通过以下方式来安全发布:
在静态代码块(活着静态语句)中初始化一个对象引用;(JVM内部存在同步机制,在类初始化时加锁)
将对象对引用保存到volatile类型的域活着AtomicReferance对象中;
将对象的引用保存到某个正确构造函数的final类型域中;
将对象的引用保存到一个由锁保护到域中。

第四章 对象的组合

没什么内容要记

第五章 基础构建模块

1.快速失败和安全失败

Fail-Fast机制:

我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛ConcurrentModificationException,这就是所谓fail-fast策略。

这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。

Java代码


HashIterator() {

expectedModCount = modCount;

if (size > 0) { // advance to first entry

Entry[] t = table;

while (index < t.length && (next = t[index++]) == null)

;

}

}

在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:

注意到modCount声明为volatile,保证线程之间修改的可见性。

Java代码


final Entry<K,V> nextEntry() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

在HashMap的API中指出:

由所有HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
Fail-Safe机制:

Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败(一般的集合类)的,而java.util.concurrent包下面的所有的类(比如CopyOnWriteArrayList,ConcurrentHashMap
)都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

CopyOnWrite:
以CopyOnWriteArrayList为例,在迭代期间不需要对容器进行加锁或复制。只有在修改时,才会创建并重新发布一个新的容器副本,从而实现可变性。
显然,每当修改容器时都会复制底层数组,需要一定的开销。仅当迭代操作远远多于修改操作时,才应该使用写入时复制容器。

2.双端队列与工作密取

正如阻塞队列适用于生产者消费者问题,双端队列同样适用于另外一种模式:工作密取。
工作密取设计中,每个消费者有各自的双端队列。如果一个消费者完成自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。
在大多数时候,只是访问自己的双端队列,从而极大减少了竞争。当工作者线程需要访问另一个队列时,会从队列到尾部而不是头部获取工作(而那个队列自己的线程会从头部取)。因此进一步降低队列到竞争程度。
工作密取非常适用于既是消费者也是生产者问题:当执行某个工作时可能产生更多的工作。比如网页爬虫、搜索图的算法、垃圾回收阶段对堆进行标记。

3. 闭锁

闭锁可以用来确保某些活动直到其他活动完成后才继续执行。

CountDownLatch是一种灵活的闭锁实现,可以使一个或多个线程等待一组时间发生。闭锁状态包括一个计数器,这个计数器被初始化为一个正数,表示需要等待的事件数目。

countDown方法递减计数器,表示一个事件已经发生了。await方法等待计数器达到0,这表示所有需要等待的时间都已经发生。如果计数器的值非0,那么await会一直阻塞直到计数器为0,或者等待中的线程中断或等待超时。

FutureTask(它表示的计算是通过callable实现的)也可以用做闭锁。

4.信号量

Semaphore 可以用于实现资源池,比如数据库连接池。

Semaphore sem = new Semaphore();

sem.acquire()

...

sem.release()

5.栅栏

类似于闭锁。栅栏能阻塞一组线程直到某个事件发生。他们的关键区别在于: 所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

CountDownLatchCyclicBarrier
减计数方式加计数方式
计算为0时释放所有等待的线程计数达到指定值时释放所有等待线程
计数为0时,无法重置计数达到指定值时,计数置为0重新开始
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
不可重复利用可重复利用
可以参考:
http://blog.csdn.net/tolcf/article/details/50925145

6.构建高效的结果缓存

public class Memoizer <A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
= new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;

public Memoizer(Computable<A, V> c) {
this.c = c;
}

public V compute(final A arg) throws InterruptedException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
} catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
}
}


第六章 任务执行

1.线程池的好处以及默认配置线程池

相比为每个任务分配一个线程的好处:
1.通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。
2.当请求到达时,工作线程通常已经存在,不用等待线程创建,提高了响应性。
3.通过调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。

默认:
newFixedThreadPool:达到线程池最大数量时保持固定大小
newCachedThreadPool:回收空闲的,不够时增加。没有大小限制。
newSingleThreadExecutor:单线程
newScheduledThreadPool: 创建一个固定大小的线程池,而且以延迟或定时方式来执行任务。(现在定时任务不用Timer)

ps:创建了固定大小的线程池之后,虽然不会因为创建过多线程而导致耗尽内存,但是仍然可能会因为有过多的Runnable队列而导致这个问题。

2.延迟任务与定时任务

Timer的缺陷:
在执行所有定时任务时只会创建一个线程;如果抛出一个未检查的异常,Timer线程并不捕获,直接终止。已经被调度但尚未执行的TimerTask不会再执行,新的任务也不能被调度。这个问题被称为线程泄漏。

转自:http://blog.csdn.net/tsyj810883979/article/details/8481621

1.按指定频率周期执行某个任务。
初始化延迟0ms开始执行,每隔100ms重新执行一次任务。

/**
* 以固定周期频率执行任务
*/
public static void executeFixedRate() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(
new EchoServer(),
0,
100,
TimeUnit.MILLISECONDS);
}
间隔指的是连续两次任务开始执行的间隔。

2.按指定频率间隔执行某个任务。
初始化时延时0ms开始执行,本次执行结束后延迟100ms开始下次执行。
/**
* 以固定延迟时间进行执行
* 本次任务执行完成后,需要延迟设定的延迟时间,才会执行新的任务
*/
public static void executeFixedDelay() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleWithFixedDelay(
new EchoServer(),
0,
100,
TimeUnit.MILLISECONDS);
}

3.周期定时执行某个任务。
有时候我们希望一个任务被安排在凌晨3点(访问较少时)周期性的执行一个比较耗费资源的任务,可以使用下面方法设定每天在固定时间执行一次任务。
/**
* 每天晚上8点执行一次
* 每天定时安排任务进行执行
*/
public static void executeEightAtNightPerDay() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
long oneDay = 24 * 60 * 60 * 1000;
long initDelay  = getTimeMillis("20:00:00") - System.currentTimeMillis();
initDelay = initDelay > 0 ? initDelay : oneDay + initDelay;

executor.scheduleAtFixedRate(
new EchoServer(),
initDelay,
oneDay,
TimeUnit.MILLISECONDS);
}
/**
* 获取指定时间对应的毫秒数
* @param time "HH:mm:ss"
* @return
*/
private static long getTimeMillis(String time) {
try {
DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
DateFormat dayFormat = new SimpleDateFormat("yy-MM-dd");
Date curDate = dateFormat.parse(dayFormat.format(new Date()) + " " + time);
return curDate.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return 0;
}


当执行任务的时间大于我们指定的间隔时间时,它并不会在指定间隔时开辟一个新的线程并发执行这个任务。而是等待该线程执行完毕。

3.Future对象

get方法的返回值:
如果任务已经完成,返回一个值或者抛出一个Exception,如果没有完成,get将阻塞并直到任务完成。如果抛出了异常,那么get将该异常封装为ExecutionException并重新抛出。

4.为任务设置时限

try{
int a=f.get(time,timeunit) ; //尝试获取计算结果
}
catch(ExecutionException e){

}
catch(TimeoutException e){
f.cancel();
a=0;//使用默认值..
}

或者使用invokeAll来批处理:



<T> List<Future<T>>
invokeAll(Collection<Callable<T>> tasks)


执行给定的任务,当所有任务完成时,返回保持任务状态和结果的 Future 列表。

<T> List<Future<T>>
invokeAll(Collection<Callable<T>> tasks,
long timeout, TimeUnit unit)


执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。

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


执行给定的任务,如果某个任务已成功完成(也就是未抛出异常),则返回其结果。

5.使用CompletionService批处理任务

如果你向Executor提交了一个批处理任务,并且希望在它们完成后获得结果。为此你可以保存与每个任务相关联的Future,然后不断地调用timeout为零的get,来检验Future是否完成。这样做固然可以,但却相当乏味。幸运的是,还有一个更好的方法:完成服务(Completion service)。

CompletionService整合了Executor和BlockingQueue的功能。你可以将Callable任务提交给它去执行,然后使用类似于队列中的take和poll方法,在结果完整可用时获得这个结果,像一个打包的Future。ExecutorCompletionService是实现CompletionService接口的一个类,并将计算任务委托给一个Executor。

ExecutorCompletionService的实现相当直观。它在构造函数中创建一个BlockingQueue,用它去保持完成的结果。计算完成时会调用FutureTask中的done方法。当提交一个任务后,首先把这个任务包装为一个QueueingFuture,它是FutureTask的一个子类,然后覆写done方法,将结果置入BlockingQueue中,take和poll方法委托给了BlockingQueue,它会在结果不可用时阻塞。

//使用CompletionService(完成服务)保持Executor处理的结果

public void count2() throws InterruptedException, ExecutionException{

ExecutorService exec = Executors.newCachedThreadPool();

CompletionService<Integer> execcomp = new ExecutorCompletionService<Integer>(exec);

for(int i=0; i<10; i++){

execcomp.submit(getTask());

}

int sum = 0;

for(int i=0; i<10; i++){

//检索并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。

Future<Integer> future = execcomp.take();

sum += future.get();

}

System.out.println("总数为:"+sum);

exec.shutdown();

}

第七章 取消与关闭

1.interrupt机制的优点

提供更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清楚工作。

2.对中断的理解

并不会真正地中断一个正在运行对线程,而只是发出中断请求,由线程在下一个合适的时刻中断自己。有些方法如wait、sleep、join等,将严格地处理这种请求,当它们受到中断请求或者在开始执行时发现某个已经被设置好的中断状态时,将抛出一个异常。

3.shutdown()和shutdownNow()的区别

shutdown()新的任务不会再被提交到线程池,但之前的都会依旧执行,通过中断方式停止空闲的(根据没有获取锁来确定)线程。

shutdownNow()则向所有正在执行的线程发出中断信号以尝试终止线程,并将工作队列中的任务以列表方式的结果返回。

两者区别:

是一个要将线程池推到SHUTDOWN状态,一个将推到STOP状态
并且对运行的线程处理方式不同,shutdown()只中断空闲线程,而shutdownNow()会尝试中断所有活动线程
还有就是对队列中的任务处理,shutdown()队列中已有任务会继续执行,而shutdownNow()会直接取出不被执行

4.守护线程

JVM启动时,除了主线程以外,都是守护线程。
普通线程与守护线程的差异仅仅在于线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在对守护线程都将被抛弃,既不会执行finally代码块,也不会执行回卷栈,只是直接退出。

第八章 线程池的使用

1.线程池

大小不能太大,也不能太小。最优大小有一个公式可求,与cpu数目、目标cpu使用率有关。

2.固定大小的线程池仍能耗尽资源

前文说过,runnable队列

3.SynchronousQueue的适用场合

只有当线程池是无界的(否则无空闲线程且线程数达到最大值时,将拒绝任务),或者可以拒绝任务时,才有实际价值。

4.饱和策略

ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改(如果一个任务被提交到已经关闭对Executor时,也会用到饱和策略)

以下转自http://www.molotang.com/articles/553.html

RejectedExecutionHandler接口

当ThreadPoolExecutor执行任务的时候,如果线程池的线程已经饱和,并且任务队列也已满。那么就会做丢弃处理,这也是execute()方法实现中的操作,源码如下:

这个reject()方法很简单,直接调用丢弃处理的handler方法的rejectedExecution()。

在java.util.concurrent中,专门为此定义了一个接口,是RejectedExecutionHandler:

其中只有rejectedExecution()一个方法。返回为void,而参数一个是具体的Runnable任务,另一个则是被提交任务的ThreadPoolExecutor。

凡是实现了这个方法的类都可以作为丢弃处理器在ThreadPoolExecutor对象构造的时候作为参数传入,这个前面的文章已经提到过了。其中ThreadPoolExecutor给出了4种基本策略的实现。分别是:

CallerRunsPolicy

AbortPolicy

DiscardPolicy

DiscardOldestPolicy

下面分别详细说明。

1. 直接丢弃

这个也是实现最简单的类,其中的rejectedExecution()方法是空实现,即什么也不做,那么提交的任务将会被丢弃,而不做任何处理。

这个策略使用的时候要小心,要明确需求。不然不知不觉的任务就丢了。

2. 丢弃最老

和上面的有些类似,也是会丢弃掉一个任务,但是是队列中最早的。

实现如下:

注意,会先判断ThreadPoolExecutor对象是否已经进入SHUTDOWN以后的状态。之后取出队列头的任务并不做任何处理,即丢弃,再重新调用execute()方法提交新任务。

3. 废弃终止

这个RejectedExecutionHandler类和直接丢弃不同的是,不是默默地处理,而是抛出java.util.concurrent.RejectedExecutionException异常,这个异常是RuntimeException的子类。这个策略实现如下:

注意,处理这个异常的线程是执行execute()的调用者线程。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//扩展线程池以提供日志和计时功能
public class TimingThreadPool extends ThreadPoolExecutor{
//需要重写配置型的构造方法
public TimingThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public static void main(String[] args) {
//默认使用Executors.newCachedThreadPool()的配置方法,过期时间为60秒
TimingThreadPool pool = new TimingThreadPool(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
pool.runTask();
pool.shutdown();
}
//执行任务
public void runTask(){
this.execute(new Runnable(){
@Override
public void run() {
System.out.println("我执行了一个任务~");
}}
);
}
//执行任务之前
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
System.out.println("执行任务之前~");
}
//执行任务之后
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("执行任务之后~");
}
//执行任务完成,需要执行关闭操作才会调用这个方法
@Override
protected void terminated() {
super.terminated();
System.out.println("执行任务完成~");
}
/**
* 运行结果:
*  执行任务之前~
我执行了一个任务~
执行任务之后~
执行任务完成~
*/
}


4. 调用者执行策略

在这个策略实现中,任务还是会被执行,但线程池中不会开辟新线程,而是提交任务的线程来负责维护任务。

注意,和DiscardOldestPolicy同样,也会先判断ThreadPoolExecutor对象的状态,之后执行任务。这样处理的一个好处,是让caller线程运行任务,以推迟该线程进一步提交新任务,有效的缓解了线程池对象饱和的情况。

上面只是SunJDK中提供的4种最基本策略,开发者可以根据具体需求定制。

5.拓展executor

例子:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//扩展线程池以提供日志和计时功能
public class TimingThreadPool extends ThreadPoolExecutor{
//需要重写配置型的构造方法
public TimingThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public static void main(String[] args) {
//默认使用Executors.newCachedThreadPool()的配置方法,过期时间为60秒
TimingThreadPool pool = new TimingThreadPool(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
pool.runTask();
pool.shutdown();
}
//执行任务
public void runTask(){
this.execute(new Runnable(){
@Override
public void run() {
System.out.println("我执行了一个任务~");
}}
);
}
//执行任务之前
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
System.out.println("执行任务之前~");
}
//执行任务之后
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("执行任务之后~");
}
//执行任务完成,需要执行关闭操作才会调用这个方法
@Override
protected void terminated() {
super.terminated();
System.out.println("执行任务完成~");
}
/**
* 运行结果:
*  执行任务之前~
我执行了一个任务~
执行任务之后~
执行任务完成~
*/
}


无论任务是从run中正常返回,还是抛出一个异常返回,afterExecute都会被调用。(如果任务在完成后带有一个error,那么久不会调用afterExecute)如果beforeExecute抛出RuntimeException,那么任务不会被执行,afterExecution也不会被调用。
在线程池完成关闭操作时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后。

第十章 避免活跃性危险

1.转账问题中避免死锁

两种方法:
1.利用hashcode求锁顺序: 无论从哪个账号转到哪个账号,先获取hashcode值比较小的那个账号
2.加时赛锁:在获取两个Account锁之前,先获得这个加时赛锁,从而保证每次只有一个线程以未知对顺序获得这两个锁

2.开放调用

如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用

3.死锁的避免和诊断

避免: 破坏死锁的四个条件之一

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

诊断:
通过线程转储来分析,线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息,线程转储还包含加锁信息,例如每个线程持有哪些锁,在那些栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁。在生成线程转储之前,jvm将在等待关系图中通过搜索循环来找出死锁,如果发现了一个死锁,则获取相应的死锁信息,例如在死锁中设计哪些锁和线程,以及这个锁的获取操作位于程序的哪些位置。

4.饥饿、活锁

当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿。引发饥饿的最常见资源就是cpu时钟。
通常,我们尽量不要改变线程的优先级。只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。

活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。
当多个相互协作的线程都对彼此进行相应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。就像两个过于礼貌的人,在路上面对面相遇,彼此都让出对方的路,然后在另一边相遇,循环往复。

解决活锁问题,需要在重试机制中引入随机性。

第十一章 性能与可伸缩性

1.可伸缩性

可伸缩性指的是,当增加计算资源时(例如cpu、内存、存储容量或io带宽),程序的吞吐量或者处理能力能相应地增加。

2.线程引入的开销

1)上下文切换
2)内存同步
3)阻塞
jvm在实现阻塞行为时,可以适用自旋等待(通过循环不断地尝试获取锁,直到成功)或者通过操作系统挂起被阻塞的线程。
如果等待时间较短,适合使用自旋等待,等待时间较长,适合使用线程挂起方式。

3.减少锁的竞争

串行操作会降低可伸缩性,上下文切换也会降低性能。在锁上发生竞争将同时导致这两个问题。
有三种方式可以降低锁的竞争程度:
减少锁的持有时间
减少锁的请求频率
使用带有协调机制的独占锁,这些机制允许更高的并发性

缩小锁的范围(加锁执行的代码少一点)
减小锁的粒度(通过锁分解和锁分段等技术),然而使用的锁越多,发生死锁的风险也越高
使用独占锁的替代机制:比如ReadWriteLock,原子变量

4.不要使用对象池来优化性能

如果使用对象池,那么就要加同步机制。阻塞的开销比内存分配开销大的多。不合算。

第十三章 显式锁

1.轮询锁 定时锁

可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。
可以利用tryLock来处理第十章钟动态顺序死锁的问题,用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试。

定时锁:
lock.tryLock(time,timeunit),如果不能在指定时间内完成,就返回false

2.可中断的锁获取操作

public boolean sendOnSharedLine(Stringmessage)
throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message)
throwsInterruptedException{...}
}


3.lock是否公平

ReenTrantLock的构造函数提供两种公平性选择,创建一个公平的锁或非公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁。在非公平的锁上,允许插队:当一个线程请求非公平的锁时,如果在发出请求的同时锁的状态变为可用,那么将跳过队列中所有的等待线程并获得这个锁。

公平性将由于在挂起线程和恢复线程时存在的开销而极大地降低性能。

默认的ReentrantLock和内置锁都是非公平的。

4.何时使用Reetrantlock而不是synchronized

仅当内置锁不能满足需求时,才可以考虑使用ReentrantLock:比如可定时的、可轮询的、可中断的锁获取操作、公平队列、非块结构的锁。

在java5中,内置锁还有另一个优点:在线程转储中能给出在哪些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。(java6中解决了这个问题)

第十五章 原子变量与非阻塞同步

1.CAS

compare and swap:
CAS包括3个操作数,需要读写的内存位置V,进行比较的值A,和拟写入的新值B。当且仅当V的值=A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。

如果需要一个计数器,可以直接用AtomicInterger或AtomicLong。

2.非阻塞算法

如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无锁算法。(Lock-Free)

非阻塞算法通过底层的并发原语来维持线程的安全性。这些底层的原语通过原子变量类向外公开。

3.ABA问题

CAS判断的是V的值是否还=A,但是可能存在的情况是这个值已经变化过,又回到A,所以这时候要加个版本号一起判断。

第十六章 Java内存模型

1.重排序

在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些调整。
volatile和同步将限制编译器和硬件运行时对内存操作重排序的方式。

2.Java内存模型,Happens-Before规则

Java内存模型是根据各种操作来定义的,包括对变量的读写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。要想保证执行操作b的线程看到操作a的结果(无论a和b是否在一个线程中执行),那么在a和b之间必须满足Happens-Before关系。如果两个操作之间缺乏这个关系,JVM可以对它们任意重排序。



Happens-before法则


Java的内存结构如下


如果多线程之间不共享数据,这也表现得很好,但是如果多线程之间要共享数据,那么这些乱序执行,数据在寄存器中这些行为将导致程序行为的不确定性,现在处理器已经是多核时代了,这些问题将会更加严重,每个线程都有自己的工作内存,多个线程共享主内存,如图



如果共享数据,什么时候同步到主内存让别人的线程读取数据呢?这又是不确定的,如果非要一致,那么代价高昂,这将牺牲处理器的性能,所以现在的处理器会牺牲存储一致性来换取性能,如果程序要确保共享数据的时候获得一致性,处理器通常了提供了一些关卡指令,这个可以帮助程序员来实现,但是各种处理器都不一样,如果要使程序能够跨平台是不可能的,怎么办?

使用Java,由JMM(Java Memeory Model Action)来屏蔽,我们只要和JMM的规定来使用一致性保证就搞定了,那么JMM又提供了什么保证呢?JMM的定义是通过动作的形式来描述的,所谓动作,包括变量的读和写,监视器加锁和释放锁,线程的启动和拼接,这就是传说中的happen
before,要想A动作看到B动作的结果,B和A必须满足happen before关系,happen before法则如下:

程序次序规则:在一个线程内,按照程序代码的顺序,书写在前面的操作先行发生与书写在后面的操作。

冠程锁定规则:一个锁的unlock操作先行发生于“后面”对同一个锁的lock操作。这里的“后面”是指时间上的先后顺序。

volatile变量规则:对一个volatile变量的写操作先行发生于“后面”对这个变量的读操作。这里的“后面”同样是指时间上的先后顺序。

线程的启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

线程中断规则:对线程的interrupt()方法的调用先行发生于被中断线程的代码检测

对象终结原则:一个对象的初始化完成(构造函数执行完成)先行发生与它的finalize()方法的开始。

传递性:A发生在B之前,B发生在C之前,A一定发生在C之前。

3.安全初始化,不要用双检锁

提前初始化:

private static Resource resource=new
Resource();

public static Resource getInstance() {
return resource;
}

延长初始化占位类模式:

private static class
ResourceHolder{
public static Resource resource=new Resource();
}

public static Resource getInstance() {
return ResourceHolder.resource;

}

关于双检锁的安全性问题: http://www.cnblogs.com/redcreen/archive/2011/03/29/1998802.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: