Thinking in Java:并发

Posted Chouney

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Thinking in Java:并发相关的知识,希望对你有一定的参考价值。

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.waitP

以上是关于Thinking in Java:并发的主要内容,如果未能解决你的问题,请参考以下文章

java并发 使用ScheduledExecutor的温室控制器--thinking in java 21.7.5

《Thinking In Java》作者:不要使用并发!

java 并发原子性与易变性 来自thinking in java4 21.3.3

Thinking in Java---Concurrent包下的新构件学习+赛马游戏仿真

Thinking In Java 对象导论

Thinking in Java---多线程仿真:银行出纳员仿真+饭店仿真+汽车装配工厂仿真