Thinking in Java:并发

来源:互联网 发布:神话刷号软件 编辑:程序博客网 时间:2024/06/03 14:00
PS:一个任务对象可以由多个线程执行

1.并发的意义:从性能角度看,如果没有任务会阻塞,那么单处理器机器上使用并发就没有任何意义

使用Executor
1.Java SE5de Executor将为你管理Thread对象,从而简化了并发变成。Executor允许你管理异步任务的执行,而无需显式地管理线程的生命周期,因此是启动任务的优选方法。通过Executors的静态方法创建封装好的线程池(封装了ThreadPoolExecutor)。

2.任何线程池中,现有线程在可能的情况下,都会被自动复用。

3.Runnable是执行工作的独立任务,但是它不返回任何值,返回值的需要实现Callable接口泛型,类型参数表示为call(),并且必须使用ExecutorService.submit()方法调用。submit方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。可以用isDone方法判断是否完成,使用get方法来获取结果(阻塞或超时设置)。


4.休眠:TimeUnit.MILLISECONDS.sleep封装了Thread.sleep方法,可以使用时间单位

5.优先级:可以用getPriority来读取现有线程的优先级,并且在任何时刻都可以通过setPriority来修改它

6.让步:yield方法暗示可以让别的线程使用CPU,但并没有任何机制保证一定会执行。

7.后台线程daemon:通过设置setDaemon方法可以设为后台线程,这种线程并不属于程序中不可或缺的部分,因此所有非后台线程结束时,程序也就终止了,会杀死进程中的所有后台线程(甚至不会执行finally子句,这样是正确的)。

8.使用Executor而不是显式创建Thread对象的原因:
    1.实现Runable接口可以另外继承需要的任务类,而Thread不行
    2.通过下图这样创建新任务,在构造器启动任务可能会变得有问题,因为另一个任务可能会在构造器结束之前开始执行,如下结果所示。



9.有时会通过内部类来将线程代码隐藏在类中将会很有用。

10.加入一个线程:一个线程可以再其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join()。此线程将被挂起,直到目标线程t结束才恢复(t.isAlive()返回为假)。

11.interrupt方法可以中断线程,调用时,将给该线程设定一个标志,表明线程已经被中断。在InterruptException补货时将清理这个标志,因此在catch子句中,异常捕获时这个标志总是为假(isinterrupt() false)。

异常捕获

1由于线程的本质特性,你不能捕获从线程中逃逸的异常。一旦异常掏出任务的run()方法,它就会向外传播到控制台

2.为了在线程中捕获异常,需要实现一个Thread.UncaughtExcetionHandler接口,并通过Thread对象的set方法将这个异常处理器附着上去。

3.如果要在代码出处使用相同的异常处理器,则使用Thread.setDefaultUncaughtExceptionHandler方法,系统会检查线程专有版本的处理器,然后检查线程组专有版本,最后在检查这个。

共享受限资源

1.Java提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。

2.所有对象都自动含有单一的锁(监视器)。当在对象上调用其任意synchronized方法的时候,这是此对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。如:
synchronized void f(){}
synchronized void g(){}
如果某个对象调用了f(),对于同一个对象而言,只能等到f()调用结束并释放了锁之后,其他任务才能调用f()和g()。因此对某个特定对象来说,其所有synchronized方法共享一个锁,这可以被用来防止多个任务同时访问被编码为对象的内存

3.一个任务可以多次获得对象的锁,一个方法在同一个对象上调用了第二个方法,后者又在同一个对象上调用了另一个方法。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,则计数为0.加一次锁则计数递增。每当任务离开一个synchronized方法,计数递减,为0时所释放。

4.针对每一个类,也有一个锁(作为类的Class对象的一部分)所以synchronize static方法可以在类的范围内放置对static数据的并发访问。

5.使用同步的准则:
如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。(RAW、WAR)

使用显式地Lock对象
1.当你在使用Lock对象时,紧接着对lock的调用,必须放置在finally子句中带有unlock的try-finally语句中。注意,return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将数据暴露给了第二个任务。

2.当你使用synchronized关键字时,需要些的代码量更少,并且用户错误出现的可能性也会降低,但是某些事物失败了,就会抛出一个异常并没有机会去做任何清理工作,因此通常只有在解决特殊问题时,才使用显式Lock对象,例如用synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它。

3.显式地Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了更细粒度的控制力。例如用于遍历链接列表中的节点的节节传递加锁机制(也称锁耦合),这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁。

原子性和易变性

1.原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,可以保证他们会被当做不可分的操作来操作内存。但是JVM可以将64位long和double变量的读取和写入当做两个分离的32位操作,产生了在一个读取和写入操作中间发生上下文切换,导致不同的任务可以看到不正确结果的可能性。定义long和double变量时,如果使用volatile关键字,就会获得原子性。

2.在多处理器系统上,可视性问题远比原子性问题多得多。一个任务做出的修改,及时在不中断的意义上讲师原子性的,对其他任务也可能是不可视的。

3.同步机制强制在处理器系统中,一个任务做出的修改必须在应用中是可视的。如果没有同步机制,那么修改时的可视将无法确定。

4.volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,所有的读操作都可以看到这个修改。volatile域会立即被写入到主存中,而读取操作就发生在主存中。

5.如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新。

6.当一个域的值依赖于它之前的值时(如计数器),volatile就无法工作了,如果某个域的值收到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界遵循lower<=upper限制。

7.Java递增操作不是原子性的,并且涉及一个读操作和一个写操作,可能为产生线程问题留下了空间,因此有必要添加synchronized

8.基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,就应该设置这个域为volatile。如果你讲一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入的操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步。

原子类

1 Java SE5 引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,他们提供compareAndSet方法进行原子性条件更新。这些类被调整为可以使用在某些现代处理器上的可获得的,并且在机器级别上的原子性,在涉及性能调优时(相比于synchroinzed)。

2.应该强调的是,Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊情况下菜在自己的代码中使用它们,即使使用了也需要确保不存在其他可能出现的问题,通常依赖于锁要更安全一些。

临界区

1.防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。这种方式分离出来的代码为临界区,synchronized(syncObject){},这里synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。这也被成为同步控制块;在进入此段代码钱,必须得到syncObject对象的锁。

2.相比于对象加锁进行同步,使用同步控制块进行同步,所以对象不加锁的时间更长。这也是宁愿使用同步控制块而不是对整个方法进行同步控制的典型原因:使得其他线程能更多的访问(在安全的情况下尽可能多)(也可以使用显式地Lock对象创建临界区)

在其他对象上同步

1.synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是使用被调用的当前对象:synchronized(this),这样改对象其他的synchronized方法和临界区就不能被调用了。
2.同理,也可以所有相关的任务不在同一个对象上同步,只要是对象上的方法是在不同的锁上同步的即可。

线程本地存储

1.线程本地存储是可以为使用相同变量的每个不同的线程都创建不同的存储,ThreadLocal对象通常当做静态域存储。

4.终结任务

线程状态
一个线程可以处于一下四种状态之一:
1)新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度其将把这个线程转变为可运行状态或阻塞状态。
2)就绪(Runnable):在这种状态下,只要调度器吧时间片分配给线程,线程就可以运行。即在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态
3)阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。知道线程重新进入了就绪状态,它才有可能执行操作。
4)死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

进入阻塞状态
原因可能如下:
1.调用sleep()
2.调用wait()使线程挂起,知道线程得到了notify()或notifyAll()消息(或在concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪状态。
3.任务等待某个输入/输出完成
4.任务视图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了锁。

中断
中断发生的唯一时刻是在任务要进入或已经在阻塞操作内部时

1.有时为了终止处于阻塞状态的任务,那么需要中断。这一点会很棘手,因为可能需要清理资源,因此中断更像是抛出异常。当你为了在以这种方式终止任务是,返回良好状态,你必须仔细编写catch子句以正确清楚所有事物。

2.Thread的interrupt方法可以终止被阻塞的任务,这个方法将设置线程中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。另外,当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位,这提供了离开run循环而不抛出异常的第二种方式。

3.如果你在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。

4如果只中断某个单一任务,并且使用submit()启动任务(这样可以获取任务上下文)时,可以调用返回Future的canel()来中断特定任务。

5需要注意的是,sleepBlock是可中断的阻塞,而IOBlock和SynchronizedBlocked是不可中断的阻塞,通过后两者不需要任何InterruptedException处理器能判断出来。因此I/O及同步具有锁住你多线程程序的潜在可能,关乎利害。解决I/O可能的阻塞问题就是关闭任务在I/O上阻塞的底层资源(in.close()等等)

6.各种NIO类已经提供了更人性化的I/O中断。被阻塞的nio通道会自动的相应中断。

被互斥所阻塞

1.如之前SynchronizedBlocked是不可中断的阻塞,在任何时刻只要任务以不可中断的方式被阻塞,都会有被锁住程序的可能。ReentrantLock上阻塞的任务具备可以被中断的能力(lockInterruptibly()方法会在阻塞时可被中断),这与在synchronized方法或临界区上阻塞的任务完全不同。

检查中断

1.interrupted()可以检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断这个问题通知你两次,因此你可以经由单一的InterruptedException或单一成功的Thread.interrupted()测试来得到这种通知。典型惯用法如下:
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (!Thread.interrupted()) {
try {
System.out.println("before");
Thread.sleep(1000);
try {
System.out.println("calculate");
for (int i = 0; i < 1000000; i++) ;
}finally {
System.out.println("clean 2");
}
System.out.println("after");
}finally {
System.out.println("clean 1");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(2000);
System.out.println("1");
t.interrupt();
}
这里提供了2种退出任务的方式,在Sleep之后中断,则清除所有资源后由interrupted检测到正常退出;在sleep与interrupted之间中断,则会清除1后异常退出。因此关键点是相应interrupt的类必须紧跟try-finally来很好的清理资源和优雅的退出


线程之间的协作

1.在互斥之上,我们为任务添加了一种途径,可以将其自身挂起,直至某些外部条件发生变化。这种协作握手可以通过Object的方法wait()和notify()来安全地实现。在Java SE5的并发类库还提供了具有await()和signal()方法的Condition对象。

wait()与notifyAll()

1.wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。wait会在等待是将任务挂起,在notify或notifyAll发生时唤醒。这就提供了一种任务间同步的方式。

2.调用sleep的时候锁并没有被释放,调用yield也属于这种情况。另一方面,调用wait(),线程被挂起,对象上的锁被释放。因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象中的其他synchronized方法(不能是显式锁)可以再wait()期间被调用。

3.实际上,只能在同步控制方法或同步控制块里调用wait()、notify()、和notifyAll()。如果在非同步控制方法里调用这些方法,程序虽然通过编译,但运行时会得到ILLegalMonitorStateException异常。即调用这些方法的任务在调用前必须获得对象的锁。

4.可以让另一个对象执行操作维护其自己的锁,若这么做则必须首先得到对象的锁。如:
synchronized(x){
    x.notifyAll();
}

5.关于同步的示例:
public class ConcurrentDemo {

private Lock testLock = new ReentrantLock();

private boolean waxOn = false;

public synchronized void waxed() {
waxOn = true;
System.out.println(Thread.currentThread().getName()+" waxOn");
notifyAll();
}

public synchronized void buffered() {
waxOn = false;
System.out.println(Thread.currentThread().getName()+" waxOff");
notifyAll();
}

public synchronized void waitForWaxed() throws InterruptedException {
while (!waxOn) {
wait();
}
}

public synchronized void waitForBuffered() throws InterruptedException {
while (waxOn) {
wait();
}
}

public class WaxOn implements Runnable {

public void run() {
try {
while (!Thread.interrupted()) {
waxed();
TimeUnit.MILLISECONDS.sleep(200);
waitForBuffered();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("Exiting Task WaxOn");
}
}
}

public class WaxOff implements Runnable {


public void run() {
try {
while (!Thread.interrupted()) {
waitForWaxed();
TimeUnit.MILLISECONDS.sleep(200);
buffered();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("Exiting Task WaxOff");
}
}
}
PS:这里使用while循环包围wait(),因为可能有多个任务出于相同原因等待同一个锁,当第一个任务响应时,其余任务应当再次挂起

6.使用notify而不是notifyAll是一种优化。使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务,并且必须等待相同的条件。这些限制对所有可能存在的子类都必须总是起作用的。

7.notifyAll将唤醒所有等待该锁(对象锁)的任务。

使用显式地Lock和Condition对象
ReentrantLock  lock.newCondition
1.使用互斥并允许任务挂起的基本类是Condition,可以通过在Condition上调用await()来挂起一个任务。并且可以通过调用signal唤醒一个任务或者用signalAll来唤醒所有在这个Condition上被其自身挂起的任务。(与notifyAll相比,signalAll是更安全的方式

2.Lock和Condition对象只有在更加困难的多线程问题中才是必需的。

生产者-消费者与队列

1.wait和notifyAll方法以一种非常低级的方式解决了任务互操作问题。在更高的抽象级别中,可以使用同步队列来解决任务协作问题,起保证了任何时刻只允许一个任务插入或移除元素,在BlockingQueue接口中提供了队列并有大量标准实现

2.如果消费者任务视图从队列中获取对象,而该队列此时为空,那么这些队列还可以挂起消费者任务,并且当有元素时恢复任务。可以解决大量问题,且比wait和notifyAll可靠的多。通过同步队列可以避免很多显式地同步(Lock对象或者synchronized),因为队列的阻塞,使得处理过程江北自动地挂起和恢复。

public class Meal {
public int count;

public Meal(int num) {
this.count = num;
}

@Override
public String toString() {
return "Meal{" +
"count=" + count +
'}';
}
}

public class WaitPerson implements Runnable {
public Restaurant restaurant;
public int waiterId;

public WaitPerson(Restaurant restaurant, int waiterId) {
this.restaurant = restaurant;
this.waiterId = waiterId;
}

public void run() {
try {
while (!Thread.interrupted()) {
synchronized (this) {
while (restaurant.meals.isEmpty()) {
wait();
}
System.out.println(this + "去拿到厨师的菜,准备上菜");
}
synchronized (restaurant.meals) {
if (!restaurant.meals.isEmpty()) {
Meal meal = restaurant.meals.get(0);
restaurant.meals.remove(0);
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(this + "上菜完毕+" + meal);
} else if (restaurant.meals.isEmpty() && restaurant.orderNum == 20) {
System.out.println("准备下班");
restaurant.waitExec.shutdownNow();
}
}
Thread.yield();
}
} catch (InterruptedException e) {

} finally {
System.out.println(this + "下班");
}
}

@Override
public String toString() {
return "waiter-" + waiterId;
}
}

public class Chef implements Runnable {
public Restaurant restaurant;
public int chefId;

public Chef(Restaurant restaurant, int chefId) {
this.restaurant = restaurant;
this.chefId = chefId;
}

public void run() {
try {
while (!Thread.interrupted()) {
synchronized (restaurant.meals) {
System.out.println(this + "厨师收到通知继续做饭");
TimeUnit.MILLISECONDS.sleep(200);
if (++restaurant.orderNum == 20) {
System.out.println("准备收工");
restaurant.chefExec.shutdownNow();
}
Meal meal = new Meal(restaurant.orderNum);
restaurant.meals.add(meal);
}
WaitPerson waitPerson = restaurant.getWaitPerson();
synchronized (waitPerson) {
waitPerson.notifyAll();
System.out.println(this + "做好饭,等待服务生来取");
}
Thread.yield();
}
} catch (InterruptedException e) {

} finally {
System.out.println(this + "下班");
}
}

@Override
public String toString() {
return "chef-" + chefId;
}
}

public class Restaurant {
private Random random = new Random();
public int orderNum;
public List<Meal> meals;
public List<Chef> chefs;
public List<WaitPerson> waitPersons;

public Restaurant(List<WaitPerson> waitPersons, List<Chef> chefs) {
this.waitPersons = waitPersons;
this.chefs = chefs;
this.meals = new ArrayList<Meal>();
}

ExecutorService waitExec = Executors.newCachedThreadPool(new ThreadFactory() {
private int i = 0;

public Thread newThread(Runnable r) {
return new Thread(r, r.toString() + i++);
}
});

ExecutorService chefExec = Executors.newCachedThreadPool(new ThreadFactory() {
private int i = 0;

public Thread newThread(Runnable r) {
return new Thread(r, r.toString() + i++);
}
});

public Chef getChef() {
if (!chefs.isEmpty()) {
return chefs.get(random.nextInt(chefs.size()));
}
return null;
}

public WaitPerson getWaitPerson() {
if (!waitPersons.isEmpty()) {
return waitPersons.get(random.nextInt(waitPersons.size()));
}
return null;
}
}


任务间使用管道进行输入/输出

1.通过输入/输出在线程间进行通信通常很有用。提供线程功能的类库以“管道”的形式对线程间的输入/输出提供了支持。Java中即PipedWriter类(允许任务向管道写)和PipedReader类(允许不同任务从同一个管道中读取)。管道基本上是一个阻塞队列,存在于多个引入BlockingQueue之前的Java版本中。

2.PipedReader与普通I/O之间最重要的差异——PipedReader是可中断的。如果你将in.read()调用修改为System.in.read(),那么interrupt()将不能打断read的调用。

死锁的四个充要条件
1.互斥,任务使用的资源中至少有一个是不能共享的。在哲学家问题中,一根Chop一次只能被一个Philosopher使用。
2.至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,知道有一个任务在等待第一个任务所持有的资源。也就是说,要发生死锁,在哲学家问题中Philosopher必须拿着一根Chop并且等待另一根。
3.资源不能被任务抢占,任务必须把资源释放当做普通事件。在哲学家问题中Philosopher很有礼貌,他们不会从其他Philosopher那里抢Chop
4.必须有循环等待,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。在哲学家问题中每个Philosopher都试图先得到右边的Chop,然后得到左边的,所以发生了循环等待。

新类库中的构件(简介)

1.CountDownLatch:
它被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。
对象可以设置一个初始计数值,调用其wait方法会阻塞直到计数值为0,其他任务调用该对象的countDown方法来减少计数值。

2.CyclicBarrier:
非常想CountDownLatch,但是CountDownLatch是只触发一次的时间,CyclicBarrier可以重用。
可以向CyclicBarrier提供一个“栅栏动作”,他是一个Runnable,当计数值达到9时自动执行,是匿名内部类创建的,它被提交给了CyclicBarrier构造器。

3.DelayQueue:
这是一个无界的BlockingQueue,放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。队列是有序的,即队头对象的延迟到期的时间最长。如果没有任何延迟到期,那么就不会有任何头元素,并且poll将返回null

4.PriorityBlockingQueue:
很基础的优先级队列,它具有可阻塞的读取操作。

5.ScheduledExecutor
ScheduledThreadPollExecutor提供在一个预定时间运行任务,使用schedule(运行一次任务)或者scheduleAtFixedRate(每隔规则的时间重复执行任务),可以将Runnable对象设置为在将来的某个时刻执行。

6.Semaphore
正常的锁(synchronized或者来自concurrent.locks的锁)在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。

7.Exchanger
Exchanger是在两个任务之间交换对象的栅栏。当这些任务进入栅栏时,它们各自拥有一个对象,当它们离开时,它们拥有之前由对象持有的对象。当你调用Exchanger.exchanger方法时,它将阻塞直至对方任务调用它自己的exchange方法,等到完成时,对象被互换

8.SynchronousQueue
这是一种没有内部容量的阻塞队列,因此每个put都必须等待一个take,反之亦然。

服务器仿真demo:
private static int serviceId;

public class Request {
private final int serviceTime;

public Request(int serviceTime) {
this.serviceTime = serviceTime;
}

public int getServiceTime() {
return serviceTime;
}

@Override
public String toString() {
return "Request{" +
"serviceTime=" + serviceTime +
'}';
}
}

public class RequestGenerator implements Runnable {
private Random rand = new Random(47);
private BlockingQueue<Request> requestQueue;
public RequestGenerator(BlockingQueue requestQueue) {
this.requestQueue = requestQueue;
}

public void run() {
try {
while (!Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(300);
requestQueue.put(new Request(rand.nextInt(1000)));
}
} catch (InterruptedException e) {
System.out.println("请求生成器中断");
} finally {
System.out.println("请求生成器关闭");
}
}
}

public class RequestHandler implements Runnable, Comparable<RequestHandler> {
private final int id = serviceId++;
private int serviceCount = 0;
private BlockingQueue<Request> requests;
private boolean serviceStatus = false;

public RequestHandler(BlockingQueue<Request> requests) {
this.requests = requests;
}

public synchronized void serveRequest() {
assert !serviceStatus : "已经处于服务状态" + this;
serviceStatus = true;
notify();
}

public synchronized void free() {
serviceStatus = false;
serviceCount = 0;
}

public synchronized int compareTo(RequestHandler o) {
return serviceCount - o.serviceCount;
}

public void run() {
try {
while (!Thread.interrupted()) {
synchronized (this) {
while (!serviceStatus) {
wait();
}
serviceCount++;
}
Request request = requests.take();
TimeUnit.MILLISECONDS.sleep(request.getServiceTime());
}
} catch (InterruptedException e) {
System.out.println("请求中断" + this);
} finally {
System.out.println("请求结束" + this);
}
}


@Override
public String toString() {
return "RequestHandler{" +
"serviceCount=" + serviceCount +
", id=" + id +
'}';
}
}

public class RequestHandlerManager implements Runnable {
private PriorityQueue<RequestHandler> workingHandlers = new PriorityQueue<RequestHandler>();
private Queue<RequestHandler> handlerPools = new LinkedList<RequestHandler>();
private BlockingQueue<Request> requests;
private ExecutorService exec = Executors.newCachedThreadPool();
private int adjustPeriod;
private Random rand = new Random(47);

public RequestHandlerManager(int adjustPeriod, BlockingQueue requests) {
this.adjustPeriod = adjustPeriod;
this.requests = requests;
}

public void adjustRequest() {
if (requests.size() / 2 > workingHandlers.size()) {
RequestHandler requestHandler;
if (!handlerPools.isEmpty()) {
requestHandler = handlerPools.remove();
requestHandler.serveRequest();
workingHandlers.offer(requestHandler);
return;
}
requestHandler = new RequestHandler(requests);
requestHandler.serveRequest();
workingHandlers.offer(requestHandler);
exec.execute(requestHandler);
return;
}
if ((workingHandlers.size() > 1 && requests.size() / 2 < workingHandlers.size())) {
freeHandler();
}
if (requests.size() == 0) {
while (!workingHandlers.isEmpty()) {
freeHandler();
}
}
}

public void freeHandler() {
RequestHandler requestHandler = workingHandlers.poll();
requestHandler.free();
handlerPools.offer(requestHandler);
}

public void run() {
try {
while (!Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(adjustPeriod);
adjustRequest();
System.out.println(this);
}
} catch (InterruptedException e) {
System.out.println("Tomcat服务中断");
}finally {
System.out.println("Tomcat服务关闭");
}
}

@Override
public String toString() {
return "当前状态:工作队列-"+workingHandlers+" , 线程池-"+handlerPools+" ,请求队列-"+requests;
}
}

性能调优

1.使用Lock通常会比使用synchronized要高效,因为synchronized越过一定门槛值的时候会变得非常低效,并且其开销变化范围太大,而Lock相对一致;然而在选择方式上需要考虑2点:一是被互斥部分的大小:如果被互斥部分所花费的时间明显大于进入和退出互斥的开销,那么提高互斥速度所带来的好处也会减少很多(当然尽量保持被互斥部分小是很好的习惯);二是很明显synchronized缠上的代码相比于显式“加锁-try/finally-解锁”可读性要提高很多。因此建议是:以synchronzed入手,只有在性能调优时才考虑替换为Lock对象。
2.对于Atomic类,Atomic对象只有在非常简单的情况下才有用,包括只有一个要呗修改的Atomic对象并且独立于其他所有对象。更安全的做法是:以更加传统的互斥方式入手,只有在性能方面的需求能够明确指示时,在替换为Atomic

免锁容器

JavaSE5 通过使用更灵巧的技术来消除加锁,提高线程安全的性能
1.免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者能看到完成修改的即可。修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。
2.CopyOnWriteArrayList中,写入将导致创建整个底层数组副本。好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException(),因此不必方法这种异常。
PS:CopyOnWriteArrayList的修改操作使用的是显式Lock来保证副本同步。
3.CopyOnWriteArraySet使用CopyOnWriteArrayList实现免锁行为。
4.ConcurrentHashMap和ConcurrentLinkedQueue使用类似技术,容器中只有部分内容可以被复制和修改,也不会抛出ConcurrentModificationException()。
5.由于其读操作是读取副本数据(乐观锁机制),因此其读操作性能由于同步容器。

乐观加锁

1.某些Atmoic类还允许你执行所谓的“乐观加锁”,如compareAndSet方法,表示当你执行某项计算时,实际上没有使用互斥,只是在计算后会将新旧值比较,问题的关键在于如果操作失败时,你不能执行某些恢复操作的话,那么你就不能使用这项技术,而是必须使用传统的互斥。

ReadWriteLock

1.ReadWriteLock对像数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock使得你可以同时又多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,知道写锁释放
2.ReadWriteLock性能是不确定的,实践是检验真理的唯一标准。
3.ReentrantReadWriteLock有其他大量方法可用,这时一个相当复杂的工具,适合提高性能优化时想用它。优先还是更直观的同步。

总结:
线程的一个额外好处是它们提供了轻量级的执行上下文切换,而不是重量级的进程上下文切换。因为一个给定进程内的所有线程共享相同的内存空间,轻量级的上下文切换只是改变了程序的执行序列和局部变量。进程切换必须改变所有内存空间。
线程缺陷:1.等待共享资源的时候性能降低;需要处理线程的额外CPU花费;程序设计的复杂度;不同平台的不一致性。
0 0
原创粉丝点击