笔记:多线程
多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务,通常每个任务称为一个线程(thread),他是线程控制的简称,可以同时运行一个以上线程的程序称为多线程程序(multithreaded);多线程和多进程有哪些区别呢,本质的区别在于每个进程拥有自己的一整套变量,而线程则是共享数据,Java中启动一个线程的代码如下:
// 线程任务的具体实现接口
public interface Runnable {
public abstract void run();
}
// 启用新线程执行任务
Runnable runnable = new MyRunnable();
Thread newThread = new Thread(runnable);
newThread.start();
- 中断线程
当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止;还有一种可以强制线程终止的方法,interrupt 方法可以用来请求终止线程,示例如下:
while(Thread.currentThread().isInterrupted == false && more work to do){
do more work
}
如果线程被阻塞,就无法检测中断状态,如果在一个阻塞的线程(sleep或wait)上调用interrupt方法时,阻塞调用将会抛出InterruptedException异常,因此标准的线程run方法应具有如下形式:
public void run(){
try{
...
while(Thread.currentThread().isInterrupted() == false && more work to do){
// do more work
}
// 返回结果
}
catch(InterruptedException e){
// 线程被阻塞,需要中断线程返回
}
finally{
// 释放资源
}
}
如果在每次工作迭代之后都调用sleep方法(或者其他可中断方法),isInterrupted检测将没有任何用处,如果在中断状态被置为时调用sleep方法,他不会休眠,相反他将清除这个状态,并抛出 InterruptedExcpetion。
- 线程状态
- 新创建(New)
当使用New操作符创建一个新线程时,该线程还没有开始运行,这时他的状态是新创建(New)。
- 可运行(Runnable)
一旦调用start方法,线程将处于可运行(Runnable)状态,一个可运行的线程可能正在运行也可能没有运行,这取决与操作系统给线程提供运行的时间。
- 被阻塞(Blocked)、等待(Waiting)和计时等待(Timed Waiting)
当线程处于被阻塞或等待状态时,线程暂时不活动,不运行任何代码且消耗最小的资源,知道线程调度器重新激活他。
- 被阻赛(Blocked):当一个线程试图获取一个内部的对象锁(不是 java.util.concurrent库的锁),而该锁被其他线程持有,则该线程进入阻塞状态,当所有线程释放该锁,并且线程调度器允许本线程持有该锁时,该线程将变成非阻塞状态。
- 等待(Waiting):当线程等待另一个线程通知调度器一个条件时,该线程将进入等待状态,例如,在调用 Object.wait 方法或者Thread.join方法,或者等待 java.util.concurrent库的Lock或Condition时,就会出现这个情况。
- 计时等待(Timed Waiting):有几个方法具有超时参数,调用他们导致线程进入计时等待(Timed Waiting),这一状态将一直保持到超时期满或者接收到适当通知,带有超时参数的方法有 Thread.sleep、Object.wait、Thread.join、Lock.tryLock以及 Condition.await方法。
- 被终止(Terminated)
当线程run方法正常退出而自然死亡;因为一个没有捕获的异常终止了run方法而意外死亡,此时线程状态为被终止。
- 线程优先级
在Java程序中,每个线程有一个优先级,默认情况下,一个线程继承他的父线程优先级,可以用 setPriority方法提高或降低任何一个线程优先级,可以设置为MIN_PRIORITY(Thread类中定义为1)与MAX_PRIORITY(Thread类中定义为10)之间的任何值,NORM_PRIORITY(默认优先级)被定义为5。每当线程调度器有机会选择新线程时,他首先选择具有高优先级的线程,但是线程优先级时依赖与系统的,Java的线程优先级被映射到宿主主机平台的优先级上,优先级个人数也许更多,也许更少。
- 守护线程
守护线程的唯一用途时为其他线程提供服务,如果只剩下守护线程,虚拟机都退出了,因此,守护线程应该永远不去访问固有资源,如文件、数据库,因为他任何时候甚至在一个操作的中间发生中断,可以通过调用Thread实例的 setDaemon(true) 来将线程转换为守护线程,必须在线程启动之前调用。
- 未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但是,不被检测的异常会导致线程终止,在这种情况下,线程就死亡了,不需要任何catch子句来处理可以被传播的异常,就在线程死亡之前,异常被传播到一个用于未捕获异常的处理器,该处理器必须实现 Thread.UncaughtExceptionHandler接口的类,该接口只有一个方法定义:
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
可以使用 setUncaughtExceptionHandler方法未任何线程安装一个处理器,也可以使用Thread类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认处理器。
- 同步锁对象
有两种机制防止代码块受并发访问的干扰,Java语言提供一个synchronized 关键字达到这目的,并且在 Java SE5.0 引入了 ReentrantLock类(重入锁),synchronized关键字自动提供一个锁以及相关条件,使用 ReentrantLock类的代码示例如下:
package org.drsoft.mybatisExamples.example_thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private Lock bankLock = new ReentrantLock();
public void transfer(int form, int to, int amount) throws InterruptedException {
bankLock.lock();
try {
System.out.println("ThreadID=" + Thread.currentThread().getId() + "\tForm=" + form + "\tTo=" + to+ "\tAmount=" + amount);
Thread.sleep((int) (10 * Math.random()));
} finally {
bankLock.unlock();
}
}
}
- 条件锁对象
通常,线程进入临界区,却发现在某一条件满足之后才能执行,要使用一个条件对象来管理哪些已经获得了一个锁,当是却不能做有用工作的线程,一个锁对象可以有一个或者多个相关的条件对象,可以使用 newCondition方法获得一个条件,可以调用条件的 await方法阻塞线程,直到另一个线程的调用同一个条件上的 signalAll方法重新激活因为这个条件等待的所有线程,示例代码如下:
package org.drsoft.mybatisExamples.example_thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private Lock bankLock = new ReentrantLock();
private Condition toCondition;
public Bank() {
toCondition = bankLock.newCondition();
}
public void transfer(int form, int to, int amount) throws InterruptedException {
bankLock.lock();
try {
if (to == 2) {
System.out.println("ThreadID=" + Thread.currentThread().getId() + "\tawait");
toCondition.await();
}
System.out.println("ThreadID=" + Thread.currentThread().getId() + "\tForm=" + form + "\tTo=" + to+ "\tAmount=" + amount);
toCondition.signalAll();
Thread.sleep((int) (10 * Math.random()));
} finally {
bankLock.unlock();
}
}
}
- synchronized关键字
使用 synchronized 关键字声明在方法中,将保护整个方法,也就是说,要调用该方法,线程必须获得内部得对象锁,代码示例如下:
public synchronized void method(){
//method body
}
等价与如下代码:
public void method(){
this.intrinsicLock.lock();
try{
// method body
}
finally{
this.intrinsicLock.unlock();
}
}
内部锁只有一个相关条件,wait方法添加一个线程到等待集,notifyAll或notify方法解除等待线程得阻塞状态,可以使用 synchronized关键字实现前面得代码:
package org.drsoft.mybatisExamples.example_thread;
public class Bank {
public synchronized void transfer(int form, int to, int amount) throws InterruptedException {
if (to == 2) {
System.out.println("ThreadID=" + Thread.currentThread().getId() + "\tawait");
wait();
}
System.out.println(
"ThreadID=" + Thread.currentThread().getId() + "\tForm=" + form + "\tTo=" + to +
"\tAmount=" + amount);
notifyAll();
Thread.sleep((int) (10 * Math.random()));
}
}
将静态方法声明为 synchronized也是合法的,如果调用这种方法,该方法获得相关的类对象的内部锁,因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
- 同步阻塞
每一个Java对象有一个锁,线程可以通过调用同步方法获得锁,还有另一种机制可以获得锁,通过进入一个同步阻塞,当线程进入如下形式的阻塞:
synchronized(obj){
//critical section
}
有时候程序员使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定。
- volatile关键字
如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用 volatile关键字标识,volatile关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为 volatile,那么编译器和虚拟机就知道这该域是可能被另一个线程并发更新的,注意:volatile关键字不能提供原子性操作。
- final变量
还有一种方法可以安全的访问一个共享域,即这个域声明为 final时,例如如下代码:
final Map<String,Double> accountMap = new HashMap<String,Double>();
如果不声明为 final,就不能保证其他线程看到的是实例化的值,有可能看到的只是 null,而不是新构造的HashMap。
- 原子性
java.util.concurrent.atomic包中有很多类使用了高效的机器指令来保证其他操作的原子性,例如,AtomicInteger类,提供了方法 incrementAndGet 和 decrementAndGet方法,他们分别以原子方式将一个整数自增或自减,另外这个包中还提供了 AtomicBoolean、AtomicLong和AtomicReference等相关类。
- 线程局部变量
有时候可能避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例,示例代码如下:
package org.drsoft.mybatisExamples.example_thread;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadLocalExample {
public static final ThreadLocal<SimpleDateFormat> dataFormat = new ThreadLocal<SimpleDateFormat>(){
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) {
String dateStamp = dataFormat.get().format(new Date());
System.out.println("dateStamp="+dateStamp);
}
}
在一个指定的线程首次调用 get 方法时,会调用initialValue方法进行初始化,在此之后,get方法会返回属于当前线程的哪个实例。
- 读写锁(ReentrantReadWriteLock)
如果很多线程从一个数据结构读取数据,而很少的线程修改其中的数据的话,读写锁时十分有用的,在这种情况下,允许读数据线程访问共享时合适的,写线程依然必须时互斥访问的,下面是使用读写锁的必要步骤:
- 构造一个ReentrantReadWriteLock对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
- 抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
- 对所有的获取方法加读锁:
public double getTotalBalance(){
realLock.lock();
try{
// 读取数据代码
}
finally{realLock.unlock();}
}
- 对所有修改方法加写锁:
public void transfer(…){
writeLock.lock();
try{
// 写入数据代码
}
finally{writeLock.unlock();}
}
- Callable 与 Future
前面创建线程对象时,使用的是 Runnable接口,该接口封装异步运行的任务,是没有参数和返回类型的异步返回;Callable 和 Runnable类似,但Callable是有返回值,该接口只有一个方法 call,接口定义如下:
public interface Callable<V> {
V call() throws Exception;
}
其类型参数是call方法的返回值的类型;Futrue保持异步计算的结果,可以启动一个计算,将Futrue对象交给线程,Futrue 对象的所有者在结果计算好之后就可以获取他,Futrue接口定义如下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
get方法的调用被阻塞,可以设置一个超时时间,如果计算完成则会获取计算结果,如果在超时时间到达后,则会抛出 TimeoutException 异常,如果调用方法被中断,则会抛出 InterruptedException 异常,调用实例如下:
LogCallable callable = new LogCallable("高优先级线程");
FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
Thread newMaxPriorityThread = new Thread(futureTask);
newMaxPriorityThread.setPriority(Thread.MAX_PRIORITY);
newMaxPriorityThread.setDaemon(false);
newMaxPriorityThread.start();
int val = futureTask.get();
System.out.println("Thread value is " + val);