《Java核心技术卷1》Chap12并发汇总
并发
-
多进程和多线程的区别:每个进程都拥有自己的一整套变量,而线程则共享数据;
-
线程的六种状态:
- 新建(
new Thread()
) - 可运行(线程可能正在运行,也可能没有运行)
- 阻塞
- 等待
- 计时等待
- 终止
- 新建(
-
阻塞:当一个线程试图获取一个内部的对象锁,而这个锁目前被其他线程占有,该线程就会被阻塞,当线程等待另一个线程通知调度器出现一个条件时,该线程就会进入等待状态;有几个方法有超时参数,调用这些方法会让线程进入计时等待状态;
-
终止线程:
run()
方法正常退出,线程自然终止;- 因为一个没有捕获的异常终止了
run()
方法,线程意外终止;
- 除了已经废弃的
stop()
方法,没有办法可以强制线程终止
//java.lang.Thread
void join() : 等待终止指定的线程
void interrupt() : 向线程发送中断请求
static boolean interrupted() : 当前线程是否中断,会改变中断状态
boolean isInterrupted() : 当前线程是否中断,不改变中断状态
void setDaemon(boolean isDaemon) : 将线程转换为守护进程,唯一用途是为其他线程提供服务
void setPriority(int newPriority) : 设置线程优先级,现在最好不用
while(!Thread.currentThread().isInterrupted() && ...)
{
...
}
- 可以调用
interrupt()
方法,设置线程中断状态(向进程发送中断请求),Thread.currentThread isInterrupted()
方法获取是否设置中断状态,调用这个方法不会改变中断状态; - 线程
run()
方法不能抛出任何检查型异常,在线程死亡前,异常会传递到一个用于处理未捕获异常的处理器;
同步
- 为了避免竞态条件破坏数据,必须学习如何同步存取: 使用锁对象;
- 有2种机制可以防止并发访问代码块:
- ReentrantLock类:Java 5中提供,可重入锁
- synchronized关键字
//ReentrantLock保护代码块结构:
mylock.lock(); //ReentrantLock : 可重复锁
try{
...
}finally{
mylock.unlock(); //必须释放,防止死锁
}
- 使用锁时,不能使用
try-with-resources
语句; ReentrantLock
: 重入锁,因为线程可以反复获得已拥有的锁,锁有一个持有计数器(hold count
)来跟踪对lock()
方法的嵌套调用,每次调用lock()
方法后必须使用unlock()
方法进行释放,被一个锁保护的代码可以调用另一个使用相同锁的方法;
// java.util.concurrent.locks.Lock
void lock() ; //获得这个锁,如果锁当前被另一个线程占有,则阻塞
void unlock(); //释放这个锁
// java.util.concurrent.locks.ReentrantLock
ReentrantLock() ; //构造重入锁,用来保护临界区
ReentrantLock(boolean fair); // 构造采用公平策略的锁,公平锁倾向于选择等待时间最长的线程,公平锁可能影响线程;
private class Bank{
//声明私有重入锁
private var bankLock = new ReentrantLock();
public void transfer(int from , int to , int amount)
{
bankLock.lock(); //加锁
try{
...
}finally{
bankLock.unlock();
}
}
}
- 条件对象 : 可以使用条件对象来管理哪些已经获得了一个锁却不能做有用工作的线程,Java的条件对象又称为条件变量(
conditional variable
):- 一个锁可以有一个或多个相关联的条件对象;
- 当不满足某条件时,可以调用条件对象的
await()
方法,此时,将进入该条件的等待集,当锁可用时,该线程并不会变为可运行状态,实际上,它仍保持非活动状态,直到在另一个线程上调用signAll()
方法; - 当一个线程调用await()方法时,它没有办法重新自行激活,必须通过其他线程来重新激活该线程,如果一直没有激活,则将导致死锁
signalAll()
:并不会立即激活线程,只是解除等待线程的阻塞,sign()
:随机选择一个等待集中的线程,并解除这个线程的阻塞状态,存在死锁危险- 只有线程拥有该锁时,才能调用其对应的
await(), sign(), signAll()
方法
class Bank{
private Condition sufficientFunds;
...
public Bank()
{
...
sufficientFunds = bandlock.newCondition();
}
}
//await一般调用方法
while(!(OK to proceed))
{
condition.await();
}
//java.util.concurrent.locks.Lock
//java.util.concurrent.locks.Lock
Condition newCondition(); //返回一个与锁相关联的条件对象
//java.util.concurrent.locks.Condition
void await(); //将该线程放在这个条件的等待集中
void signalAll(); //解除该条件等待集中所有线程阻塞态
void signal(); //从该条件的等待集中随机选择一个线程,解除其阻塞态
synchronized
-
锁和条件的作用:
- 锁用来保护代码片段,一次只能有一个线程执行被保护的代码;
- 锁可以管理试图进入被保护代码的线程;
- 一个锁可以有一个或多个相关联的条件对象;
- 每个条件对象管理那些已经进入被保护代码段但还不能运行的线程
-
Java中每个对象都有一个内部锁,如果一个方法声明为同步方法,那么对象的锁将保护整个方法,要调用这个对象,线程必须获得内部对象锁:
public synchronized void method()
{
}
//等价
public void method()
{
this.intrinsicLock.lock(); // 获得内部锁
try{
...
}finally{
this.intrinsicLock.unlock();
}
}
- 内部对象锁只有一个条件对象,调用
wait()
方法将一个线程增加到等待集中,notifyAll()/notify()
方法可以解除等待线程的阻塞; wait() , notifyAll() , notify()
是Object
的final 方法,Condition为避免重复:await()、signalAll()、signal()
;- 每个对象都有一个内部锁,并且这个内部锁有一个内部条件
- 最好既不使用
Lock\Condition
,也不使用synchronized
关键字,在许多情况下,可以使用java.util.concurrent包中的某些机制,处理所有锁定;
//java.lang.Object
void notifyAll(); //解除在这个对象上调用wait()方法的线程的阻塞状态,该方法只能在同步方法或同步块中调用
void notify(); //随机解除在这个对象上调用wait方法的线程的阻塞状态
void wait(); //导致一个线程进入等待状态,直到它得到通知
- 同步块,专用锁:
public Bank{
private var lock = new Object();
synchronized(lock) //
{
...
}
}
-
监视器:
- 监视器是只包含私有字段的类;
- 监视器每个对象有一个关联的锁;
- 所有方法由这个锁锁定;
- 锁可以有任意多个相关联的条件;
-
volatile
: 保证变量在各个线程之间可见,volatile
不能提供原子性 -
final
变量:线程安全 -
线程局部变量
ThreadLocal
:
//java.lang.ThreadLocal<T>
T get();
void set(T t);
void remove();
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier); //创建一个线程局部变量
//非线性安全
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
//ThreadLocal
public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));
-
生产者线程向队列插入元素,消费者线程则获取元素,使用队列,可以安全地从一个线程向另一个线程传递数据;
-
阻塞队列
Blocking queue
,put() , take()
方法,协调多线程之间的合作;//java.util.concurrent.ArrayBlockingQueue //java.util.concurrent.LinkedBlockingQueue //java.util.concurrent.LinkedBlockingDeque
-
java.util.concurrent
:提供了ConcurrentHashMap , ConcurrentSkipListMap , ConcurrentSkipListSet , ConcurrentLinkedQueue:- 这些集合具有弱一致性,这意味着迭代器不一定能反映出它们构造之后的所有更改;
-
构造一个新线程的开销很大,涉及到与操作系统的交互,如果程序中存在大量生命周期很短的线程,那么不应该将每个任务映射到一个单独的线程,而应该使用线程池(thread pool),线程池包含包含很多准备运行的线程,为线程池提供一个Runnable方法,就会有一个线程调用run方法,当run方法退出时,这个线程池不会死亡,而是留在池中为下一个请求提供服务;
-
Runnable
封装一个异步运行的任务,可以把他想象成没有参数和返回值的异步方法,Callable
与Runnabl
类似,但是其有返回值:public interface Runnable{ public void run(); } public interface Callable<V>{ V call() throws Exception; }
-
Future<V>
用来保存异步计算的结果,可以启动一个计算,然后将Future对象交给某个线程,Future<V>
接口有:V get(); void cancel(); //取消计算 boolean isCancelled(); boolean isDone();
-
执行器:
Executors
,执行器有许多静态工厂方法,用来构造线程池:newCachedThreadPool(); newFixedThreadPool(); newWorkStealingPool(); newSingleThreadExecutor(); newSingleThreadScheduledExecutor();
-
使用如下方法将Runnable或Callable对象提交给ExecutorService:
Future<T> submit(Callable<T> task); Future<?> submit(Runnable task); Future<T> submit(Runnable task, T result);
-
使用完一个线程池时,调用shutdown()方法,启动线程池关闭序列,被关闭的执行器不再接受新任务,所有任务完成时,线程死亡,调用
shutdownnow()
,线程池会取消苏搜友尚未开始的任务; -
Thread pooling
使用步骤:- 调用Executors类的静态方法构造线程池;
- 调用submit提交Runnable或Callable对象
- 保存好返回的Future对象,以便得到结果或取消任务
- 当不需要再提交任何任务时,调用
shutdown
.