1. Thread.yield方法声明把CPU让给其他具有相同优先级的线程去执行,不过这只是一个暗示,并没有保障机制
2. Executor
执行器,管理Thread对象。
语法demo:
ExecutorService exec=Executors.newCachedThreadPool();
for(int i=0;i<5;i++)
exec.execute(new XXX()); //XXX为实现Runnable接口的类
exec.shutdown();
三种类型及其区别:
CachedThreadPool:在程序执行过程中创建与所需数量相同的线程,在回收旧线程时停止创建新线程,是Executor首选。
FixedThreadPool: 一次性预先执行代价高昂的线程分配,可以限制线程的数量。
SingleThreadExecutor: 如果向其提交多个任务,那么这些任务排队,每个任务都在下个任务开始前结束,所有的任务使用相同的线程。
3.如何创建有返回值的线程
实现Callable接口,重写call()方法。
exec.submit(new XXX()) 会返回Future对象,用future.get()获取值,这个值是泛型的, 取决于实现接口时的声明,如 implements Callable
关于get: 可以先调用Future的isDone()方法来查询是否已经完成。任务完成时会具有一个结果,可以通过get来获取。如果不用isDone直接get,get会阻塞直到结果准备就绪。
4.通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(是否是后台,优先级,名称),
比如class MyThreadFactory implements ThreadFactory{
public Thread newThread(Runnable r) {
// TODO 自动生成的方法存根
Thread t=new Thread(r);
t.setDaemon(true); //设置为后台线程
return ..;
}
5. 守护线程中派生的子线程默认是守护线程,当所有的非守护线程结束时,后台线程终止,一旦main()退出,JVM会关闭所有的守护线程。
当所有的非后台线程结束时,程序就终止了。同时会杀死进程中所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
6.线程join方法:
若在A线程中调用B.join,则A被挂起,直到B结束。在调用时也可以带上一个超时参数。 对join方法的调用可以被打断(通过在调用线程上调用interrupt()方法),这时被打断的B需要用到try-catch子句。(在run方法中,catch InterruptedException)
7.捕获线程抛出的异常
线程抛出的异常不能被正常的try catch到,可以用Executor解决这个问题:
如4中所示自定义一个myfactory类,在该factory的newThread方法中t.setUncaughtExceptionHandler(new XXX) ; XXX是实现了Thread.UncaughtExceptionHandler接口的类。 然后ExecutorService exec=Executors.newCachedThreadPool(new myfactory);
8. synchronized和Lock对象
synchronized:如果某个任务处于一个对标记为synchronized的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。不过如果是synchronized(obj)方法块,只要obj不是相同的对象,两个方法不会因为另一个方法的同步而被阻塞
用synchronized时代码量更小,且不会出现忘了unlock这种情况,用Lock对象需要 lock(); try{ } finally{ unlock},避免在lock之后代码出现异常导致死锁。
显式使用lock对象可以解决更特殊的问题,比如尝试获取锁一段时间然后放弃、实现自己的调度算法等等。且在加锁释放锁方面有更细粒度的控制力。
ps:synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。
9. volatile
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
如果多个任务在同时访问某个域,这个域就应该时volatile的,否则只能由同步来访问。同步也会导致向主存中刷新。因此如果一个域完全由synchronized方法或语句块来防护,就不必将其设置为volatile。
long和double的读取写入可能不是原子的,因为long和double 64位,在32位机器上的读写操作会被当作两个分离的32位操作执行。如果使用volatile关键字,就会获得原子性。此关键字还确保了应用中的可视性。如果把一个域声明为volatile的,只要对这个域产生写操作,所有的读操作都可以看到这个修改。即便使用了缓存,volatile域会被立即写入到主存中,而读取操作发生在主存。如果域由synchronized方法或语句块防护,不必设置为volatile。使用volatile 而不是synchronized的唯一安全的情况是类中只有一个可变域。第一选择应该是synchronized。
10.同步控制块
亦称为临界区,在方法内部:
synchronized(syncObject){
}
11.ThreadLocal
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
12.线程的暂停、继续、终止
wait,notify,exec.shutdownNow()
在Executor上调用shutdownNow(),将发送一个interrupt()调用给它启动的所有线程。(相当于xx.interrupt())
如果想中断某个单一任务,使用Executor.submit()而不是execute()来启动任务,返回一个Future> f,可以通过调用f.cancel(true)来中断。
可以中断对sleep的调用(或者任何要求抛出InterruptedException的调用),不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程(办法是关闭任务在其上发生阻塞的底层资源)。
13.wait 和 notify
sleep和yield不释放锁,wait会释放锁
两种形式的wait:毫秒数作为参数或者不加参数
把wait、notify放在Object类中是因为这些方法操作的锁也是所有对象的一部分,所以可以把wait放进任何同步控制方法里,不需要考虑这个类是继承Thread还是实现Runnable接口。实际上,只能在同步控制方法或同步控制块里调用wait、notify。否则在运行时将得到IllegalMonitorStateException异常。调用这两个方法的任务在调用这些方法前必须拥有对象的锁。
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
可以用lock+condition来代替wait和notify,但是更复杂,只有在更加困难的多线程问题中才必需。
java.util.concurrent.lock
中的Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
区别:
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:
现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
对!读取线程不应该互斥!
我们可以用读写锁ReadWriteLock实现:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
测试结果:
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。
Condition可以替代传统的线程间通信,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。
——为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!
传统线程的通信方式,Condition都可以实现。
注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Condition的强大之处在于它可以为多个线程间建立不同的Condition
看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition
实例来做到这一点。
——其实就是java.util.concurrent.ArrayBlockingQueue的功能
优点:
假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。
那么假设只有一个Condition会有什么效果呢?缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。
14.同步队列
详见 http://wsmajunfeng.iteye.com/blog/1629354
15.管道
两个线程之间,一个拥有pepedwriter,一个拥有pipedreader(需要writer做参数)。
16.CountDownLatch和CyclicBarrier
CountDownLatch:
被用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。
new CountDownLatch(int size) , 被等待的线程在执行完操作后latch.countdown(),等待的线程调用latch.await(); 计数值为0时结束等待。
CyclicBarrier:
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。适用于:创建一组任务,并行执行,然后再下一个步骤之前等待。用于一组或几组线程,比如一组线程需要在一个时间点上达成一致,例如同时开始一个工作。
//当await的数量到达了设定的数量后,首先执行该Runnable对象。
CyclicBarrier(int,Runnable):
//通知barrier已完成线程
await():
Semaphore
Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
Test {
public
static
void
main(String[] args) {
int
N =
8
;
//工人数
Semaphore semaphore =
new
Semaphore(
5
);
//机器数目
for
(
int
i=
0
;i
new
Worker(i,semaphore).start();
}
static
class
Worker
extends
Thread{
private
int
num;
private
Semaphore semaphore;
public
Worker(
int
num,Semaphore semaphore){
this
.num = num;
this
.semaphore = semaphore;
}
@Override
public
void
run() {
try
{
semaphore.acquire();
System.out.println(
"工人"
+
this
.num+
"占用一个机器在生产..."
);
Thread.sleep(
2000
);
System.out.println(
"工人"
+
this
.num+
"释放出机器"
);
semaphore.release();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
|
下面对上面说的三个辅助类进行一个总结:
1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
17.concurrenthashmap(转自http://blog.csdn.net/liuzhengkang/article/details/2916620)
V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck } e = e.next; } } return null; } |
V readValueUnderLock(HashEntry e) { lock(); try { return e.value; } finally { unlock(); } } |
put操作一上来就锁定了整个segment,这当然是为了并发的安全,修改数据是不能并发进行的,必须得有个判断是否超限的语句以确保容量不足时能够rehash,而比较难懂的是这句int index = hash & (tab.length - 1),原来segment里面才是真正的hashtable,即每个segment是一个传统意义上的hashtable,如上图,从两者的结构就可以看出区别,这里就是找出需要的entry在table的哪一个位置,之后得到的entry就是这个链的第一个节点,如果e!=null,说明找到了,这是就要替换节点的值(onlyIfAbsent == false),否则,我们需要new一个entry,它的后继是first,而让tab[index]指向它,什么意思呢?实际上就是将这个新entry插入到链头,剩下的就非常容易理解了。
V put(K key, int hash, V value, boolean onlyIfAbsent) { V oldValue; |
remove操作非常类似put,但要注意一点区别,中间那个for循环是做什么用的呢?(*号标记)从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。
V remove(Object key, int hash, Object value) { V oldValue = null; |
static final class HashEntry { HashEntry(K key, int hash, HashEntry next, V value) { |