一 共享资源
多线程经常会共享一些资源(内存、IO等)。这类资源被称为“临界资源”,要保证所有对这类资源访问的线程处于一种互斥的状态,当有一个线程在访问的时候,其他需要访问相同资源的线程应当处于阻塞状态。
这里需要说一下原子操作,原子操作是一个不能被打断的操作,也就是当CPU从一个线程切换到另一个线程的时候,失去时间片的线程中的原子操作只要开始,就必须会执行完毕,而不会处于一种未执行完毕的中间状态。
Java中实现共享资源互斥的方法有三种:synchronized、lock、原子类。
1 synchronized
synchronized为Java提供了一种“锁”的概念,当一个线程需要访问某个资源时,它首先需要获得这个资源的锁,如果其他线程拥有这个锁,那么这个线程就会进入阻塞状态,直到拥有锁的线程对资源访问结束并返回锁。如果当前线程获得了资源的锁,那么它就可以访问资源,同时,其他未获得锁的线程必须处于阻塞状态,直到当前线程访问资源结束,并把锁返回。synchronized按照锁属于的目标不同,分为类锁和对象锁。
说到synchronized就要提一下volatile关键字。volatile关键字用于修饰变量,线程在对共享变量进行修改时,其实是先把共享变量从主内存中读取到线程自身的内存中得到一个副本,然后对副本进行修改,修改完成后再将副本写会到主内存中。所以,为了保证主内存中的数据同步,线程的这种操作需要加锁。但是,如果使用volatile关键字修饰变量,那么线程在对该变量进行修改时,将直接修改主内存中的值,而没有把变量从主内存读取到自身内存以及把修改后的变量写回到主内存中的操作。
synchronized关键字有两种使用方式,
(1)一种是用来修饰函数,例如
public synchronized int next1() {
currentEvenValue ++;
currentEvenValue ++;
return currentEvenValue;
}
public static synchronized int next2() {
currentEvenValue ++;
currentEvenValue ++;
return currentEvenValue;
}
这种方式对整个函数上锁,next1()函数中的synchronized获得了对象锁,多个线程执行同一个对象的next1()函数时,必须获得对应对象的锁。next2()方法被static关键字修饰,所以synchronized关键字要求访问者必须拥有类锁,才能执行这个方法。
(2)synchronized的另外一种使用方法是代码块,例如
private Lock lock = new ReentrantLock();
public int next1() {
synchronized(this){
currentEvenValue ++;
currentEvenValue ++;
return currentEvenValue;
}
}
这种方法,synchronized 后面的参数,即为锁的对象,this表示当前对象,所以这是一个对象锁。
2 Lock
相比于synchronized,Lock提供了一种更加灵活的锁,但是Lock对象必须显式的被创建、锁定和释放。下面代码中的try-finally语句,是Lock的一种惯用方式。
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try{
currentEvenValue++;
currentEvenValue++;
return currentEvenValue;
}finally{
lock.unlock();
}
}
总体来说,synchronized需要的代码量少,并且用户出现错误的可能性低。所以只有在需要解决特殊问题时,才会使用Lock代替synchronized。例如,synchronized不能尝试获得锁,失败后继续干其他的事,而Lock可以。例如:
Lock lock = new ReentrantLock();
//尝试加锁,成功返回true,否则返回false
boolean b1 = lock.tryLock();
//尝试加锁,如果超过2秒没成功,返回false,否则返回true
boolean b2 = lock.tryLock(2, TimeUnit.SECONDS);
3 原子类
使用AtomicInteger,AtomicLong,AtomicReference等特殊的原子变量类实例化临界资源,可以避免共享资源被多个线程同时访问。
二 线程本地存储
防止任务在共享资源上发生冲突的第二种方式,是根除对变量的共享。线程本地存储是一种机制,可以为使用相同变量的每一个线程创建不同的存储。
实现线程本地存储的类是ThreadLocal类。下面的例子中,使用ThreadLocal类存储多个线程的一个Int值。不同线程的值的改变,对于程序员来说是完全透明的,例如下面的increment()方法和get()方法,程序员不需要指定ThreadLocal类返回哪个线程的值。
Accessor类中定义了任务的内容,只要线程没有被打断,则一直增加自己存储在本地的int值(ThreadLocalVariableHolder类中的value)。
public class Accessor implements Runnable {
private final int id;
public Accessor(int id){
this.id = id;
}
@Override
public void run() {
while(!Thread.interrupted()){
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString(){
return "#"+ id +":"+ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
protected synchronized Integer initialValue() {
return 0;
}
};
public static void increment(){
value.set(value.get()+1);
}
public static int get(){
return value.get();
}
public static void main(String[] args) throws InterruptedException{
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0; i<5; i++){
exec.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
exec.shutdownNow();
}
}
输出结果:
#3:86930
#2:78177
#0:68734
#0:68735
#0:68736
#0:68737
#0:68738
#0:68739
#0:68740
#0:68741
#0:68742
#0:68743
#2:78178
#2:78179
#2:78180
#2:78181
#2:78182
#2:78183
#3:86931
#4:67885
可以看出,对于每个线程,ThreadLocal对象都为其保存了一个int值。
三 终结任务
1 一个线程一定会处于以下四种状态之一:
(1)新建(new)
(2)就绪(Runnable)
(3)阻塞(Blocked)
(4)死亡(Dead)
2 一个线程进入阻塞状态有以下原因
(1)使用sleep()方法进入休眠
(2)使用wait()方法挂起
(3)线程在等待输入/输出的完成
(4)线程在等待某个锁的释放
3 中断线程的方法
(1)对于使用Executor执行的线程,可以通过执行Executor的shutdownNow()方法,对线程池中的每个线程发送一个Interrupt命令,从而终止线程。
(2)对于使用Thread.start()方法执行的线程,可以执行Thread.interrupt()方法,向对应线程发送一个Interrupt命令,终止线程。
(3)使用Executor的submit()方法提交的线程,方法将会返回一个Future<?>对象,通过调用Future<?>.cancel(true)方法,也可以终止线程。
4 线程处理interrupt信号
(1)这里需要注意的是,对于处于非阻塞状态的线程,当线程接收到Interrupt命令之后,线程的Thread.currentThread.interrupted()方法将返回true,但是线程不会马上终止。这就相当于Interrupt命令给线程打了一个标签,表示这个线程需要停止了,这个标签可以通过interrupt()方法得到,但是接下来执行什么操作,需要程序员自己设计。例如:
public void run() {
while(!Thread.currentThread.interrupted()){
doSomething();
}
}
这种方式中,每次循环之前判断线程的Interrupt状态,如果为true,则跳出循环,从而保证线程结束。需要注意的是,interrupt()方法会清除线程的中断状态,例如一个线程的状态被置为interrupt,这是你调用interrupt()方法,方法将会返回true,表示线程需要被中断,然后方法将interrupt状态清除,如果这个时候你再调用一下interrupt()方法,该方法将会返回false,因为之前的中断状态已经被清除了。Java这样设计的目的是防止同一个interrupt信号通知线程两次。
(2)如果线程处于阻塞状态(上面的四种状态之一),如果线程由于sleep()方法或者wait()方法处于阻塞状态,当线程接收到interrupt信号之后,会抛出InterruptException异常,所以,程序中可以通过截获该异常,判断是否需要终止线程,如:
public void run() {
try{
TimeUnit.SECONDS.sleep(200);
}catch(InterruptedException e){
System.out.println("Interrupt Exception");
}
System.out.println("Exiting SleepBlocked.run()");
}
当线程处于sleep状态时,interrupt信号会使线程抛出InterruptException异常,通过截获这个异常,线程也可以安全退出。
(3)如果线程由于等待IO输入而处于阻塞状态,interrupt信号并不会使线程抛出InterruptException异常。这种情况下,只有通过关闭任务在其上发生阻塞的资源,使线程抛出IOException异常,才能使线程结束。例如:
public void run() {
try{
System.out.println("waiting for read");
is.read();
}catch(IOException e){
if(Thread.currentThread().isInterrupted()){
System.out.println("Interrupt from IO blocked");
}else{
throw new RuntimeException();
}
}
System.out.println("Exiting from IOBlocked.run()");
}
(4)如果线程由于等待锁处于阻塞状态,interrupt信号并不会使线程抛出InterruptException异常。这种情况下,synchronized关键字是无法被中断的,只用通过使用Lock的锁,才能被interrupt信号中断(使线程抛出InterruptException异常)。例如:
public void run() {
Lock lock = new ReentrantLock();
try{
Lock.lockInterruptibly();
TimeUnit.SECONDS.sleep(200);
}catch(InterruptedException e){
System.out.println("Interrupt Exception");
}
System.out.println("Exiting .run()");
}