Java 程序中总是会有的 main()
运行在这个程序的主线程上。但有的时候可以让 Java 程序同时多运行几个线程从而提高运行效率,也就是多线程编程。这一点在 GUI 编程中尤其必要,常见的 Swing 框架就是利用了多线程:在这个框架中,负责 GUI 的线程 EDT(Event Dispatcher Thread)和负责实际运算的线程是分离的,这样才能保证即使是程序繁忙运行时, GUI 也能随时响应用户的操作而不至于冻结。
这里说的是线程 而非 进程,JVM 一般都是单进程的。
最底层:Thread 子类和 Runnable 接口
Java 中创建一个新线程的基本思路是通过 Java.lang.Thread 或 Java.lang.Runnable。其中 Thread 是类,Runnable 是接口。
Thread 子类
Thread 子类的思路比较直观:
public class test{
public static void main(String[] args){
(new myThreadClass()).start();
}
}
//class that extends Thread
class myThreadClass extends Thread{
public void run(){
System.out.println("I'm running in a thread named " + this.getName());
}
}
这里定义了一个 myThreadClass
,它是 Thread
的 subclass,其中包含了一个 run()
方法。要运行的内容只要写在 run()
中即可。run()
本身并没有什么,真正在背后创建线程的是 Thread
自带的 start()
方法:
当主线程中创建一个
myThreadClass
实例并调用其start()
方法时,Java 就会自动创建一个新的线程并运行run()
。
p.s. 获取当前进程的方法是通过 Thread 类方法 Thread.currentThread()
Runnable 接口
Runnable 接口也能用来创建、运行新线程:
public class test{
public static void main(String[] args){
Thread t = new Thread(new Runnable(){
public void run(){
System.out.println("Another new thread!");
}
});
t.start();
}
}
这里并没有专门创造一个 Thread
的子类,而是直接新建了 Thread
的实例 t
。同时新建了一个匿名的 Runnable 然后将其作为 t
的 constructor 的参数。之后如果主进程调用 t.start()
跟上边例子里效果完全一样。
Runnable 往往是更好的选择
Runnable 接口比直接创建 Thread
子类更加灵活。因为一个类只可以继承一个父类,如果我们新建的类 myThreadClass
继承了 Thread
就意味着它不能继承其他类。而接口实现的数量则没有限制,所以下边的例子里显然我们能做的更多:
public class test{
public static void main(String[] args){
drawApple t = new drawApple();
(new Thread(t)).start();
}
}
class drawApple extends drawFruit implements Runnable{
public void drawMyFruit(){
//extends its parent class as usual
super.drawMyFruit();
System.out.println("Wait, I can draw a delicious apple!");
}
public void run(){
//implements run() method of Runnable interface to run on new thread
drawMyFruit();
System.out.println("What's more, I can draw it in a new thread!");
}
}
class drawFruit{
public void drawMyFruit(){
System.out.println("I can only draw a basic fruit...");
}
}
输出结果为:
I can only draw a basic fruit...
Wait, I can draw a delicious apple!
What's more, I can draw it in a new thread!
干预调度的几个基本方法
Runnable 这个词,同时是也是线程可能的状态之一:可运行。
顾名思义,当一个 Runnable 的线程被创造,它并不一定就开始 running了,而只是处在 runnable 的状态。 具体 JVM 会把哪个线程排在前排在后真正运行,取决于很多因素,基本可以理解为一个黑箱。
有以下几种基本方法可以强制停止正在运行中的线程,来给其他线程腾出资源。
Thread.sleep()
Thread
的类方法 sleep(long millis)
(参数即睡眠的长度,以 milli-second 为单位),可以让当前正在运行的线程进入睡眠状态,在参数给定时长后醒来恢复可运行状态。
睡眠状态中途可以被 interrupt 唤醒
Thread.yield()
这个类方法可以让当前正在运行的线程从 running 变回 runnable 状态,从而可能让其他线程开始运行—— 然而 JVM 也可能会立刻又把这个线程调度到最前边去。
###join()
这个相比前两个 Thread
类方法更好用。它的作用 doc 里说的简洁明了:
Waits for this thread to die.
也就是说, t.join()
的作用就是,让当前正在运行的线程进入等待,一直等到线程t
挂掉再继续。
这个方法也接受参数 join(long millis)
,millis 即最长等待的时间,超过这个时间就不再等待变为可运行。
等待状态中途可以被 interrupt 唤醒
InterruptedException
前边提到的 Thread.sleep()
和 join()
都可以被 interrupt 唤醒。interrupt 就是让某个线程停止当前工作, t.interrupt()
即让线程 t 停止,如果此时 t 正在睡眠或等待,则将被唤醒并收到一个 InterruptException
。因此常见的处理方式是:
try{
t.sleep(5000);
} catch(InterruptedException){
System.out.println("How dare you wake me up!");
return;
}
同步 Synchronized
多个线程「同时」运行会带来「Memory Consistency」问题。比如 2 个线程同时对 a = 0
做简单的 a += 1
运算,结果本应等于 2。
实际运行时,却可能出现这样的情况:
一个线程读取了 a 的值 (0),于是开始计算 0 + 1。 此时另一个线程来读取了 a 的值,会发现此时 a 的值还是 0,于是它也开始计算 0 + 1。 最终两个线程各自把计算结果 1 写回 a, a 的结果为 1,而不是 2。
为了避免这样的错误,就需要保证:读取 a, 计算 a + 1,写回 a, 这三件事同时只有一个线程在做。
Java 提供了 synchronized
同步来解决这个问题:
public synchronized int getAPlusOne(){
a += 1;
return a;
}
也可以把 synchonized 放在一个方法中,只有部分语句同步。
public int getAPlusOne(){
synchronized(this){
a += 1;
return a;
}
}
这里需要理解的是 synchronized 本质是利用了 Java 中每个对象都有的内在锁 (intrinsic lock),一旦一个线程获得了一个对象的内在锁,其他线程就不能再访问这个对象了(本线程的其他方法不受影响,依然可以访问,所以这种锁称为 reentrant lock),直到 synchronized 涵盖的代码块运行结束,释放对象的内在锁。
关于同步要注意以下几点:
- 变量和类是不能同步的,只能同步方法,且不能是 abstract 方法。
- 线程睡眠时也不会释放锁。因此应避免在 synchronized 的代码块中使用 sleep()。
- 同一个线程可以获得多个对象的锁。
- 如果要同步的是一个类,那么获得的锁也是这个类 class 的,而不是任何一个实例 instance 的。实例与类之间的锁互不相干。
死锁
如果一个线程 A 要访问的对象当前被另一个线程 B 获得了锁,那么 A 就会 block, 等待 B 释放锁。
这是一个直观的解决方案,但为同步带来了新的问题,比如不小心会出现的死锁(deadlock):两个线程互相都在等待对方释放自己的锁,从而僵持住了。上边的例子中,如果线程 B 接下来要执行的任务需要获得某个对象的锁,而这个锁恰好又在线程 A 中,就成了死锁。因为 A 此刻正握住这个对象的锁等待着 B 执行完成,好去获得 B 此刻正占有的锁。
Java doc 中的这个死锁程序就有意思,它模拟了两个朋友鞠躬,都在等待对方,无法继续。
lock,比同步更灵活的外部锁
前边说了,synchronized 同步的本质是获得一个对象的内在锁,内在锁是 Java 中每个对象都自带的。
同时 Java 还提供了外部锁 lock 接口,更为强大灵活。基本语法:
Lock lk = new ReentrantLock();
lk.lock();
...
lk.unlock();
用它改写前边的 a += 1 的例子:
private Lock lk = new ReentrantLock();
public int getAPlusOne(){
lk.lock();
try{
a += 1;
return a
} finally{
lk.unlock();
}
}
这里一个良好的习惯是把解锁放在 fianally
当中,这样即使前边的运算中出现 exception,也不会一直锁着对象不放。
lock 真正强大灵活的在于基本锁之外的几种锁。其完整的 API 如下:
public interface Lock{
void lock();
void lockInterruptibly(); //可以被 interrupt 的锁
boolean tryLock(); //尝试锁,返回是否成功
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //尝试锁,如果不马上成功则等待给定时间
void unlock();
Condition newCondition();
}
SwingWorker
前边说了,Swing 框架下的 GUI 程序天生都是多线程的。因此 Swing 也提供了更方便的管理多线程的方式,其中就包括 SwingWorker
。
在这里先插一点。Swing GUI 程序的一个基本准则就是: 只有 EDT 一个线程与 GUI 发生交互,实际运算不直接干涉 GUI。
因此经常看到以下代码来初始化 GUI:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
...// Code to be executed by the EDT
}
});
这段代码把绘制 GUI 的工作放到一个 Runnable 里再由 SwingUtilities.invokeLater
调用,保证让 EDT 绘制界面。
SwingWorker<T,V>
SwingWorker 是一个抽象类,它的功能总结起来就是:提供一个简便的方法来在 Swing GUI 程序中运用背景线程,保证 EDT 专注于 GUI 部分,从而保持程序本身响应的流畅。
其基本运用方法如下:
public mySwingWorker extends SwingWorker<T,V>{
@Override
protected T doInBackground(){
//编程者必须实现的方法
//真正负责运算的代码,框架会在背景线程上运行它
//T 即该方法最终返回的类型
//
//可以在此方法中调用 publich(V),其作用为在运行过程中返回类型为 V 的值给下边的 process() 处理
}
@Override
protected void process(List<V> chunks){
//会在EDT上运行此方法
//每次都会获得最近 publish() 返回的 V 值组成的 list,作为参数
}
@Override
protected void done(){
// EDT 在 doInBackground() 运行结束后运行
// 一般会调用 get() 获得 doInBackground() 返回的类型为 T 的值
}
}
本来是复习笔记,写到这里时候就考完试了,不想再往下写了…… 关于 Java 并发还需要了解的概念是原子操作(替代锁)、
Executor
、FutureTask
等等等。嘛, 总之挺麻烦就对了……
假期开始。