看了一阵子java线程方面的知识,《thinking in java 3rd》,《effective java 2nd》,感觉还是雾里看花,难以得其精髓。
多线程编程本来就是一门很玄奥的学问,不是看一些基础的语法知识就能真正掌握的。在实践中去揣摩,我想才是最好的方法。奈何我现在没有这样的条件,离论文开题的时间不远了,我还没有摸到头绪,真不知道是该坚持还是放弃。
扯远了,还是回到线程来吧,虽然不得要领,但还是要把一些基础的东西总结一下,一旦以后需要用到的时候,也可以方便地回顾。
1. 线程的创建
java中创建一个线程有两种方式:
1.1. 扩展Thread类,并重载run()方法
public class ThreadName extends Thread {
public void run() {
// do something here
}
}
1.2. 实现runnable接口,并调用Thread提供的构造函数
public class ThreadName implements Runnable {
public void run() {
// TODO Auto-generated method stub
}
public void main(String args[]) {
Thread thread = new Thread(new ThreadName());
thread.start();
}
}
这两种方法各有利弊。简单来说,如果你确定当前的类就是作为一个单纯的线程来实现,不需要再继承其他任何类的时候,那么最好就用第一种方式,因为简单,而且可以直接就获得Thread所提供的各种方法,在类内部使用;反之,如果你需要继承其他类,或者将来可能会有这种需要,那么就用第二种方法。
第二种方法需要注意的地方是,当你实现了Runnable接口后,你仅仅是实现了该接口而已,你现在所有的只是一个run()方法,即使你生成一个对象来调用run()方法,和普通的方法调用也没什么两样,并不会创建一个新的线程。只有当你用Thread构造函数创建一个对象之后,这才是新创建了一个线程。
当你使用第二种方法的时候,可能你也想在类内部调用Thread所提供的一些方法,这时可以用Thread.currentThread()来获得当前线程的引用。
2. 线程的运行
当你获得一个线程实例thread后,使他开始运行的唯一方法就是thread.start(),由java虚拟机调用该线程的run方法。
注意不是调用run()方法来启动线程。当你调用run()方法时,如果该线程是使用独立的 Runnable
运行对象构造的,则调用该 Runnable
对象的 run
方法;否则,该方法不执行任何操作并返回。
另外要注意,多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
3. 结束线程运行
就我目前所知,有两种结束线程的方法(我是指通过直接操作线程对象的合法方法)。
3.1. 在线程内部设置一个标记为volatile的标志位
public class StopThread extends Thread {
public volatile boolean stop = false;
private static int i = 0;
public void run() {
while(!stop) {
i++;
}
System.out.println(i);
}
/** *//**
* @param args
*/
public static void main(String[] args) {
StopThread thread = new StopThread();
thread.start();
try {
sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
thread.stop = true;
}
}
关于volatile的说明可能需要先了解JAVA的内存模型,简单点说就是JVM有个主存,各个线程再有各自的工作内存,两个存放的地方带来的问题就是不一致。volatile就是为了解决这个不一致出现的。
使用volatile会每次修改后及时的将工作内存的内容同步回主存。这样,线程所看到的就是最新的值,而不是被缓存的值。
注,这里还牵涉到“原子操作”的概念。所谓原子操作,大体上就是指操作只由一个指令即可完成,不需要上下文切换。在java中,对除long和double之外的基本类型进行简单的赋值或者返回值操作的时候,才是原子操作。然而,只要给long或double加上volatile,就和其他基本类型一样了。但自增操作并不是原子操作,它牵涉到一次读一次写!
正因为对boolean的操作是原子操作,我们不用担心多个线程同时对boolean值进行修改而导致不一致的情况,所以在修改、读取boolean值的时候不需要加synchronized关键字。
总结一下volatile关键字的两个作用:
1) 保证声明为volatile的64位变量的加载或存储是个基本的单元操作
2) 保证在多处理器计算机中,即使是处理高速缓存中的数据,易失性变量的加载和存储也能够正确地进行
3.2. 调用interrrupt()方法
有的时候,线程可能会阻塞,比如在等待输入的时候,并且他也不能轮询结束标志。这个时候,可以用Thread.interrupt()方法来跳出阻塞代码。
public class Blocked extends Thread {
public Blocked() {
System.out.println("Starting");
}
public void run() {
try {
synchronized(this) {
wait();
}
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("Exiting run()");
}
public static void main(String args[]) {
Blocked thread = new Blocked();
thread.start();
try {
sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
thread.interrupt();
thread = null;
}
}
注意,我们在用interrupt终止线程后,最好再将该线程赋为null,这样垃圾回收器就可以回收该线程了。
另,使用interrupt()并不需要获得对象锁 - 这与wait()、notify()等不同
上面例子中有一个比较tricky的地方:当我们使用interrupt()方法中断线程的运行时,线程将抛出InterruptedException,但在抛出exception的同时,他的中断状态将被清除,所以如果我们在catch(InterrruptedException e) { }里调用isInterrupted(),返回的结果将会是false。
Thread类提供了两种方法来判断一共线程是否处于中断状态:
interrupted():
静态方法,用于检查
当前进程是否已经被中断,同时
清除线程的中断状态
isInterrupted():
实例方法,用于检查
任何一个线程是否已经被中断,
不会清除中断状态
在编程实践中尽量不要catch InterruptedException,因为这会清除中断标记。如果要catch,那么在catch块里重新设置中断标志:Thread.currentThread().interrupt(); 或者干脆不catch,声明throws InterruptedException
当你使用Timer类调度线程的时候,可以使用Timer类提供的cancel()方法来终止线程的运行。Timer类还是比较好用的,具体参见API doc。
4. wait(), notify(), notifyAll()
这三个方法是线程同步机制的基础,但这三种方法已经被Joshua Bloch视为“low-level”的,“汇编级别”的代码,应该尽量被JDK 1.5以来提供的高层次框架类取代。这正是java让人又爱又恨的地方 - 它总是提供各种方便易用的API供使用者调用,帮助编程人员提高效率,避免错误,但与此同时,它也在无形之间将底层机制与使用隔离,使相当一批编程者“沦为”API的“纯”调用者,只懂得用一堆API来堆起一个程序。很不幸,我就是其中之一。但我总算还保留着一点求知的欲望。
使用wait(),总是要最先想到,一定要用while循环来判断执行条件是否满足:
synchronized(obj) {
while(conditionIsNotMet) {
wait();
}
// Perform action approriate to condition
}
这样就可以保证在跳出等待循环之前条件将被满足,如果你被不相干的条件所通知(比如notifyAll()),或者在你完全退出循环之前条件已经改变,你被确保可以回来继续等待。
有两个wait方法带有超时参数:
void wait(long millis);
void wait(long millis, int nanos);
但wait方法返回时,无法确定返回的原因是因为超时还是得到了通知。有两种方式可以解决这个问题:
1)
long before = System.currentTimeMillis();
wait(delay);
long after = System.currentTimeMillis();
if(after - before > delay)
// timeout
2)可以让负责通知的线程设置一个标志
对于notify(), notifyAll(),Joshua Bloch的建议是尽量使用notifyAll(),以避免出现某些进程永远沉睡的现象。
5. synchronized
synchronized是将对象中所有加锁的方法(or代码块)锁定。由于同一时间只能有一个线程拥有对象的锁,这也就保证了互斥。
有两种加锁形式:
5.1. 给代码块加锁:
synchronized(obj) {
// some codes here
}
5.2. 给方法加锁:
public synchronized void method() {
// some codes here
}
注意,java中不允许在重载(重写?)的时候改变签名,但sychronized关键字
并不属于签名,因此,你可以继承一个类,然后重载(重写?)这个几类的方法,加上sychronized关键字来保证互斥
如果某个线程拥有一个对象的锁,并且它调用了同一个对象上的另一个synchronized方法,那么该线程将自动被赋予对该方法的访问权。只有当该线程退出上一个synchronized方法时,它才会释放该锁。
这是因为每个对象都有一个锁计数器,用于计算锁的所有者调用了多少次synchronized方法。当锁计数器的值达到0时,该线程便放弃对该锁的所有权。
对于锁,需要明确下面的关系:
你可以拥有同一个类的不同对象,每个对象被不同的线程锁定。这些线程甚至可以执行同一个synchronized方法。因为我们
锁定的是对象,而不是方法。当然,在某个时间点上,
一个对象的锁只能被一个线程拥有。不过,
一个线程可以同时拥有多个对象的锁,只需要在执行一个对象上的synchronized方法的同时,又执行另一个对象的synchronized方法即可。
synchronized一个较为特殊的应用是给静态方法加锁:
public class Singleton
{
private static Singleton instance;
private Singleton() { }
public static synchronized Singleton getInstance() {
if(instance == null)
instance = �31span style="color: #0000ff">few Singleton();
return instance;
}
}
当一个线程调用Synchronized方法时,它便会获取对象的锁。但是该方法是静态方法,当调用Singleton.getInstance()时,哪个对象负责执行线程锁的操作呢?
调用一个静态方法将会锁定类对象(比如Singleton.class)。因此,如果一个线程调用一个类的静态synchronized方法,那么该类的
所有静态synchronized方法均会被锁定,直到第一个调用返回为止。
以上介绍了java线程中语法上的一些基础的东西,下面要介绍的同样也是基础,但同上面而言还是有些差异,还是分开一段来介绍的好。
1. 一些方法
sleep():
sleep()方法能迫使线程休眠指定长的时间。在调用sleep()方法的时候,必须把它放在try块中,因为在休眠时间到期之前有可能被打断。如果某人持有对此线程的引用,并且在此线程上调用了interrupt()方法,就会发生这种情况。
daemon线程:
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。一个后台线程所创建的任何线程都将被自动设置成后台线程
join():
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive()返回为false)
你也可以在调用join()时带上一个超时参数(单位可以是毫秒或者毫秒+纳秒),这样如果目标线程在这段时间到期还没结束的话,join()方法总能返回。
对join()方法的调用可以被中断,做法是在调用线程上使用interrupt()方法,这时需要用到try-catch
isAlive():
如果该线程是可运行线程或被中断的线程,那么该方法返回true;如果该线程仍然是个新线程或尚未成为可运行线程,或者该线程是个死线程,那么该方法返回false
注:无法确定一个“活”线程究竟是处于可运行状态还是被中断状态,也无法确定一个运行线程释放正处在运行之中。另外,你也无法对尚未成为可运行的线程与已经死掉的线程进行区分。
2. 线程的四种状态:创建、就绪、死亡、阻塞。
线程进入阻塞状态可能有如下四种原因:
2.1. 通过调用sleep()使线程进入休眠状态。在这种情况下,线程在指定时间内不会运行
2.2. 通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息,线程才会进入就绪状态
2.3. 线程在等待输入/输出操作的完成
2.4. 线程试图在某个对象上调用其同步控制方法,但是对象锁不可用
3. 只有当下列四个条件同时满足时,才会发生死锁:
3.1. 互斥条件:线程使用的资源中至少又一个是不能共享的
3.2. 至少有一个进程持有一个资源,并且他在等待获取一个当前被别的进程持有的资源。
3.3. 资源不能被进程抢占。所有的进程必须把资源释放作为普通事件。
3.4. 必须有循环等待,即,一个线程等待其他线程持有的资源,后者又在等待另一个进程持有的资源,这样一直下去,直到又一个进程在等待第一个进程持有的资源,使得大家都被锁住。
要发生死锁,必须这四个条件同时满足,所以,只要破坏其中任意一个,就可以打破死锁。其中第四个条件是最容易被打破的。
4. 线程的优先级
JVM将线程的优先级映射为主机平台的优先级等级。
每当主机平台使用的优先级低于Java平台时,某个线程的运行就可能被另一个优先级明显低得多的线程线程抢先。这意味着你不能依靠多线程程序中的优先级等级。
另外,调用yield方法,只会让当前线程暂时放弃运行,而主机则始终准备对放弃运行的线程实施重新启动。如果当前线程优先级较高,则可能主机一直重启该线程,而其他低优先级线程将得不到运行。为此,yield也靠不住,sleep可能是更好的方式。
5. java中对以“管道”形式对线程的输入/输出提供了支持
PipedWriter类允许线程向管道写;PipedReader类允许不同线程从一个管道中读取。或是采用PipedInputStream和PipedOutputStream提供字节流支持
使用管道的主要原因是为了使每个线程始终能保持简单。可以将多个线程相互连接起来,而不必担心线程的同步问题。
但要注意,管道式数据流只适用于线程在低层次上的通信,在其他情况下,可以使用队列。
下面是对《effective java 2nd》中Concurrency一章的总结。感觉这一章并不如我所想象的那样,对java的线程机制有一个全面透彻的解说,反而是花了很大力气宣传一本书 - 《Java Concurrency in Practice》。好吧,想在一章的内容里对java线程的认识达到某种高度,怎么想也是不太现实的。但这本书究竟如何,我还没看过,不作评论,但我想肯定是很适合正在用java线程做项目的人的。对我而言,重要的不是学会怎么用java代码来写出多线程程序,而是搞清线程内部的机制。
下面对Concurrency一章的重点知识(我认为重要的)进行一下总结:
Item 66: Synchronize access to shared mutable data
- when multiple threads share mutable data, each thread that reads or writes the data must perform synchronization. (读写都要加锁,不能只加一个)
- If you need only inter-thread communication, and not mutual exclusion, the volatile modifier is an acceptable form of synchronization, but it can be tricky to use correctly.
Item 67: Avoid excessive synchronization
- Inside a synchronized region, do not invoke a method that is designed to be overridden, or one provided by a client in the form of a function object
- As a rule, you should do as little work as possible inside synchronized regions.
- In summary, to avoid deadlock and data corruption, never call an alien method from within a synchronized region. More generally, try to limit the amount of work that you do from within synchronized regions.
Item 68: Prefer executors and tasks to threads
- Executor Framework - refer to Java Concurrency in Practice
Item 69: Prefer concurrency utilities to wait and notify
- Given the difficulty of using wait and notify correctly, you should use the higher-level concurrency utilities instead.
- The higher-level utilities in java.util.concurrent fall into three categories: the Executor Framework; concurrent collections; and synchronizers.
- Synchronizers are objects that enable threads to wait for one another, allowing them to coordinate their activities. The most commonly used synchronizers are CountDownLatch and Semaphore. Less commonly used are CyclicBarrier and Exchanger.
- For interval timing, always use System.nanoTime in preference to System.currentTimeMillis.
- Always use the wait loop idiom to invoke the wait method; never invoke it outside of a loop.
- The notifyAll method should generally be used in preference to notify. If notify is used, great care must be taken to ensure liveness.
Item 70: Document thread safety
- Conditionallythread-safe classes must document which method invocation sequences require external synchronization, and which lock to acquire when executing these sequences.
- If you write an unconditionally thread-safe class, consider using a private lock object in place of synchronized methods. This protects you against synchronization interference by clients and subclasses and gives you the flexibility to adopt a more sophisticated approach to concurrency control in a later release.
Item 71: Use lazy initialization judiciously
- In summary, you should initialize most fields normally, not lazily. If you must initialize a field lazily in order to achieve your performance goals, or to break a harmful initialization circularity, then use the appropriate lazy initialization technique.
- For instance fields, it is the double-check idiom;
For static fields, the lazy initialization holder class idiom;
For instance fields that can tolerate repeated initialization, you may also consider the single-check idiom.
Item 72: Don’t depend on the thread scheduler
- When many threads are runnable, the thread scheduler determines which ones get to run, and for how long.
- In summary, do not depend on the thread scheduler for the correctness of your program. The resulting program will be neither robust nor portable. As a corollary, do not rely on Thread.yield or thread priorities.
- Thread priorities may be used sparingly to improve the quality of service of an already working program, but they should never be used to “fix” a program that barely works.
Item 73: Avoid thread groups
- Thread groups are best viewed as an unsuccessful experiment, and you should simply ignore their existence.
我个人最喜欢第73条。。。