欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。
16. 读写锁ReentrantReadWriteLock
读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排它锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有Thread进行写操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。(lock.readlock.lock(), lock.readlock.unlock, lock.writelock.lock, lock.writelock.unlock)
17. Timer的使用
JDK中的Timer类主要负责计划任务的功能,也就是在指定时间开始执行某一任务。Timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类(public abstract class TimerTask extends Object implements Runnable)。可以通过new Timer(true)设置为后台线程。
有以下几个方法:
- void schedule(TimerTask task, Date time):在指定的日期执行某一次任务。如果执行任务的时间早于当前时间则立刻执行。
- void schedule(TimerTask task, Date firstTime, long period):在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。如果执行任务的时间早于当前时间则立刻执行。
- void schedule(TimerTask task, long delay):以当前时间为参考时间,在此基础上延迟指定的毫秒数后执行一次TimerTask任务。
- void schedule(TimerTask task, long delay, long period):以当前时间为参考时间,在此基础上延迟指定的毫秒数,再以某一间隔无限次数地执行某一任务。
- void scheduleAtFixedRate(TimerTask task, Date firstTime, long period):下次执行任务时间参考上次任务的结束时间,且具有“追赶性”。
TimerTask是以队列的方式一个一个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行时间也会被延迟。
TimerTask类中的cancel方法的作用是将自身从任务队列中清除。
Timer类中的cancel方法的作用是将任务队列中的全部任务清空,并且进程被销毁。
Timer的缺陷:Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExecutor只支持相对时间的调度。Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不波或异常,因此当TimerTask抛出为检测的异常时将终止定时线程。
JDK5或者更高的JDK中已经很少使用Timer.
18. 线程安全的单例模式
建议不要采用DCL的写法,建议使用下面这种写法:
public class LazyInitHolderSingleton {
private LazyInitHolderSingleton() {
}
private static class SingletonHolder {
private static final LazyInitHolderSingleton INSTANCE = new LazyInitHolderSingleton();
}
public static LazyInitHolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
或者这种:
public enum SingletonClass
{
INSTANCE;
}
19. 线程组ThreadGroup
为了有效地对一些线程进行组织管理,通常的情况下事创建一个线程组,然后再将部分线程归属到该组中,这样可以对零散的线程对象进行有效的组织和规划。参考以下案例:
ThreadGroup tgroup = new ThreadGroup("mavelous zzh");
new Thread(tgroup, new Runnable(){
@Override
public void run()
{
System.out.println("A: Begin: "+Thread.currentThread().getName());
while(!Thread.currentThread().isInterrupted())
{
}
System.out.println("A: DEAD: "+Thread.currentThread().getName());
}}).start();;
new Thread(tgroup, new Runnable(){
@Override
public void run()
{
System.out.println("B: Begin: "+Thread.currentThread().getName());
while(!Thread.currentThread().isInterrupted())
{
}
System.out.println("B: DEAD: "+Thread.currentThread().getName());
}}).start();;
System.out.println(tgroup.activeCount());
System.out.println(tgroup.getName());
System.out.println(tgroup.getMaxPriority());
System.out.println(tgroup.getParent());
TimeUnit.SECONDS.sleep(5);
tgroup.interrupt();
输出:
A: Begin: Thread-0
2
mavelous zzh
10
B: Begin: Thread-1
java.lang.ThreadGroup[name=main,maxpri=10]
B: DEAD: Thread-1
A: DEAD: Thread-0
20. 多线程的异常捕获UncaughtExceptionHandler
setUncaughtExceptionHandler()的作用是对指定线程对象设置默认的异常处理器。
Thread thread = new Thread(new Runnable(){
@Override
public void run()
{
int a=1/0;
}
});
thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){
@Override
public void uncaughtException(Thread t, Throwable e)
{
System.out.println("线程:"+t.getName()+" 出现了异常:"+e.getMessage());
}
});
thread.start();
输出:线程:Thread-0 出现了异常:/ by zero
setDefaultUncaughtExceptionHandler()方法对所有线程对象设置异常处理器。
Thread thread = new Thread(new Runnable(){
@Override
public void run()
{
int a=1/0;
}
});
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(){
@Override
public void uncaughtException(Thread t, Throwable e)
{
System.out.println("线程:"+t.getName()+" 出现了异常:"+e.getMessage());
}
});
thread.start();
输出同上,注意两者之间的区别。如果既包含setUncaughtExceptionHandler又包含setDefaultUncaughtExceptionHandler那么会被setUncaughtExceptionHandler处理,setDefaultUncaughtExceptionHandler则忽略。更多详细信息参考《JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止》
21.ReentrantLock与synchonized区别
- ReentrantLock可以中断地获取锁(void lockInterruptibly() throws InterruptedException)
- ReentrantLock可以尝试非阻塞地获取锁(boolean tryLock())
- ReentrantLock可以超时获取锁。通过tryLock(timeout, unit),可以尝试获得锁,并且指定等待的时间。
- ReentrantLock可以实现公平锁。通过new ReentrantLock(true)实现。
- ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的的wait(), notify(), notifyAll()方法可以实现一个隐含条件,如果要和多于一个的条件关联的对象,就不得不额外地添加一个锁,而ReentrantLock则无需这样做,只需要多次调用newCondition()方法即可。
22. 使用多线程的优势
更多的处理器核心;更快的响应时间;更好的编程模型。
23. 构造线程
一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent线程的:是否为Daemon、优先级、加载资源的contextClassLoader以及InheritableThreadLocal(参考第12条),同时还会分配一个唯一的ID来标志这个child线程。
24. 使用多线程的方式
extends Thread 或者implements Runnable
25. 读写锁
读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。Java中使用ReentrantReadWriteLock实现读写锁,读写锁的一般写法如下(修改自JDK7中的示例):
class RWDictionary {
private final Map m = new TreeMap();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Object get(String key)
{
r.lock();
try
{
return m.get(key);
}
finally
{
r.unlock();
}
}
public String[] allKeys()
{
r.lock();
try
{
return (String[]) m.keySet().toArray();
}
finally
{
r.unlock();
}
}
public Object put(String key, Object value)
{
w.lock();
try
{
return m.put(key, value);
}
finally
{
w.unlock();
}
}
public void clear()
{
w.lock();
try
{
m.clear();
}
finally
{
w.unlock();
}
}
}
26.锁降级
锁降级是指写锁降级成读锁。如果当前线程拥有写锁,然后将其释放,最后获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,最后释放(先前拥有的)写锁的过程。参考下面的示例:
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
private volatile static boolean update = false;
public void processData()
{
r.lock();
if(!update)
{
//必须先释放读锁
r.unlock();
//锁降级从写锁获取到开始
w.lock();
try
{
if(!update)
{
//准备数据的流程(略)
update = true;
}
r.lock();
}
finally
{
w.unlock();
}
//锁降级完成,写锁降级为读锁
}
try
{
//使用数据的流程(略)
}
finally
{
r.unlock();
}
}
锁降级中的读锁是否有必要呢?答案是必要。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。