Java线程并发
打算细嚼慢咽Thinking in Java这本书。从自己觉得最难的多线程并发开始写起。
上下文切换
阻塞,如果没有并发,任务/线程阻塞时整个程序都将停止下来,直至外部条件发生变化。
实现并发最直接的方式时在OS级别使用进程,进程是运行在它自己的地址空间内的自包容的程序。而Java的并发系统回共享诸如内存和IO的资源,因此并发的困难在于协调不同线程驱动的任务之间对这些资源的使用。
Runnable类,并无特殊之处,不会产生任何内在的线程能力,要实现线程行为,必需显式地将一个任务附着到线程上。
疑问:yield()有什么作用吗?
如何管理Thread对象?
使用Executor类。
FixedThreadPool,CachedThreadPool,SingleThreadExecutor
SingleThreadExecutor可以确保在任意时刻在任何线程中都只有唯一的任务在运行,在这种方式中,你不需要在共享资源上处理同步。有时更好的解决方案是在资源上同步。
从任务中产生返回值
Runnable是执行工作的独立任务,但是不返回任何值。如果希望任务在完成时能够返回一个值,那么可以实现Callable范型接口,但必需使用ExecutorService.submit()方法。
Thread or Executor
加入一个线程join()方法
如何捕获从线程中逃逸的异常?
如果在代码中处处使用相同的异常处理器,那么简单的处理方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器。
重点来了:
锁语句产生一种互相排斥的效果,这种机制被称为互斥量(mutex)。
可以通过yield()和setPriority()来给线程调度器提供建议,但这些建议未必回有多大效果,这取决于具体平台和JVM实现。
对于某个特定对象来说,其所有synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。
在使用并发时,将域设置为private很重要,否则,synchronized关键字就不能放置其他任务直接访问域,这样就会产生冲突。
一个任务可以多次获得对象的锁。每当任务离开一个synchronized方法,计数递减,当计数为0的时候,锁被完全释放,此时别的任务就可以使用此资源。
针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内放置对static数据的并发访问。
使用显式的Lock对象
Java SE5的java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必需被显式地创建,锁定和释放。它与内建的锁形式相比,代码缺乏优雅性。
Lock or Synchronized
如果在使用synchronized关键字时,某些事物失败了,就会抛出一个异常。但是你没有机会去做任何清理工作,以维护系统使其处于良好状态。有了显式的Lock对象,你就可以使用finally子句将系统维护在正确的状态了。
显式的Lock对象在枷锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了你更细粒度的控制力。
只有非常精通了才可以使用原子性来代替同步。原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,但是JVM可以将64位的读取和写入当作两个分离的32位操作来执行,这久产生了在一个读取和写入操作中间发生上下文切换(字撕裂)。但是,当你定义long或double变量时,如果使用volatile关键字,就会活的原子性。
因此,原子操作可由线程机制来保证其不可中断,专家级的程序员可以利用这一点来编写无锁的代码,这些代码不需要被同步。
如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为volatile的。如果你将一个域定义为volatile,那么它就回告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步。
临界区:有时你只是希望防治多个线程同时访问方法内部的部分代码而不是防止访问整个方法,通过这种方式分离出来的代码段被称为临界区。
在其他对象上的同步:
synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this)。这种方式中,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用了。因此,如果在this上同步,临界区的效果就会缩小在同步的范围内。
线程本地存储:
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储室一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
一个任务进入阻塞状态,可能原因:
1)通过调用sleep使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
2)通过调用wait使线程挂起。直到线程的到了notify或notifyAll消息,线程才会进入就绪状态。
3)任务在等待某个输入/输出完成。
4)任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。
当任务协作时,关键问题是这些任务之间的握手。为了实现这种握手,我们使用了相同的基础特性:互斥。这种握手可以通过Object的方法wait和notify来安全地实现。Java SE5的并发类库还提供了具有await和signal方法的Condition对象。
wait会在等待外部世界产生变化的时候将任务挂起,并且只有在notify发生时,这个任务才会被唤醒并去检查锁产生的变化。因此,wait提供了一种在任务之间对活动同步的方式。
调用sleep的时候锁并没有被释放,调用yield也同样。当一个任务在方法里遇到对wait的调用的时候,线程的执行被挂起,对象上的锁被释放,这意味着另一个任务可以获得这个锁,因此在该对象中的其它方法通常将会产生改变。
当你调用wait时,就是在声明:我已经刚刚做完能做的所有事情,因此我要在这里等待,但是我希望其它的synchronized操作在条件适合的情况下能够执行。
调用wait notify的任务在调用这些方法前必须拥有对象的锁。
感觉这本书的这一章不是很好懂,先粗略读一遍,后面需要多次阅读。下午学习集合类。