3. 创建新线程
创建一个线程有两种方法:
- 从Thread类继承并重写run()方法。创建一个实例,调用start()方法,这会在新线程上调用run()。如下:
Thread t = new Thread() { // Create an instance of an anonymous inner class that extends Thread
@Override
public void run() { // Override run() to specify the running behaviors
for (int i = 0; i < 100000; ++i) {
if (stop) break;
tfCount.setText(count + "");
++count;
// Suspend itself and yield control to other threads for the specified milliseconds
// Also provide the necessary delay
try {
sleep(10); // milliseconds
} catch (InterruptedException ex) {}
}
}
};
t.start(); // Start the thread. Call back run() in a new thread
- 创建一个实现Runnable 接口的类并为抽象方法 run()提供具体实现。用带Runnable对象的构造函数构造一个新线程实例并调用 start()方法,这会在新线程上调用run()。
// Create an anonymous instance of an anonymous inner class that implements Runnable
// and use the instance as the argument of Thread's constructor.
Thread t = new Thread(new Runnable() {
// Provide implementation to abstract method run() to specify the running behavior
@Override
public void run() {
for (int i = 0; i < 100000; ++i) {
if (stop) break;
tfCount.setText(count + "");
++count;
// Suspend itself and yield control to other threads
// Also provide the necessary delay
try {
Thread.sleep(10); // milliseconds
} catch (InterruptedException ex) {}
}
}
});
t.start(); // call back run() in new thread
因为Java不支持多重继承,一般选择第二种方法。如果类已经继承了某一超类,它便不能继承Thread,必须实现 Runnable 接口。第二种方法也能用来提供针对JDK 1.1的兼容。应该注意的是Thread类本身实现Runnable接口。
run()方法制定线程的任务。不能直接从程序中调用run()方法,而是先要创建线程实例并调用start()方法,之后start()方法在新线程上调用run()。
3.1 Runnable接口
接口java.lang.Runnable 声明一个抽象方法run()用来制定线程的执行任务:public void run();
3.2 Thread类
类 java.lang.Thread 有以下构造函数:
public Thread();
public Thread(String threadName);
public Thread(Runnable target);
public Thread(Runnable target, String threadName);
前面两个构造函数用来通过子类化Thread 类创建线程,下面两个用来创建带实现Runnable 接口类实例的线程。
Thread类实现Runnable接口,如图所示:
如上面所提到的,run()方法制定线程的执行任务。不能直接调用run()方法而应该调用Thread类的 start()方法。如果线程是通过继承Thread 类创建的,方法start()将调用继承类中重写的run()方法;如果线程是通过给线程构造函数提供Runnable 对象创建的,方法start()将调用Runnable对象的run()方法 (而非Thread的run())。
3.3 通过继承 Thread 并重写run()创建新线程
通过继承Thread类来创建并运行一个新线程:
- 定义一个继承父类Thread的子类(命名或者匿名);
- 在子类中重写run()方法来制定线程的操作,(并提供其他如构造函数,变量和方法的实现);
- Client类创建了一个该新类的实例, 该实例为Runnable对象(因为 Thread类本身实现Runnable接口);
- Client类调用Runnable对象的start()方法。结果是两个线程并发执行-调用start()之后的当前线程和执行Runnable对象 run()方法的新线程。
class MyThread extends Thread {
// override the run() method
@Override
public void run() {
// Thread's running behavior
}
// constructors, other variables and methods
......
}
public class Client {
public static void main(String[] args) {
......
// Start a new thread
MyThread t1 = new MyThread();
t1.start(); // Called back run()
......
// Start another thread
new MyThread().start();
......
}
}
通常使用内部类 (命名或匿名)而非普通子类,这是为了可读性并获取对外部类私有变量和方法的访问。例如:
public class Client {
......
public Client() {
Thread t = new Thread() { // Create an anonymous inner class extends Thread
@Override
public void run() {
// Thread's running behavior
// Can access the private variables and methods of the outer class
}
};
t.start();
...
// You can also used a named inner class defined below
new MyThread().start();
}
// Define a named inner class extends Thread
class MyThread extends Thread {
public void run() {
// Thread's running behavior
// Can access the private variables and methods of the outer class
}
}
}
public class MyThread extends Thread {
private String name;
public MyThread(String name) { // constructor
this.name = name;
}
// Override the run() method to specify the thread's running behavior
@Override
public void run() {
for (int i = 1; i <= 5; ++i) {
System.out.println(name + ": " + i);
yield();
}
}
}
通过继承Thread类创建一个MyThead类并重写run()方法。定义一个带字符串参数的构造函数,该字符串用来标识此线程的名字。run()方法打印数字1 到5, 调用yield()让线程在打印出每个数字之后自动将资源控制权交与其他线程。
public class TestMyThread {
public static void main(String[] args) {
Thread[] threads = {
new MyThread("Thread 1"),
new MyThread("Thread 2"),
new MyThread("Thread 3")
};
for (Thread t : threads) {
t.start();
}
}
}
测试类分配并启动三个线程。输出如下:
Thread 1: 1
Thread 3: 1
Thread 1: 2
Thread 2: 1
Thread 1: 3
Thread 3: 2
Thread 2: 2
Thread 3: 3
Thread 1: 4
Thread 1: 5
Thread 3: 4
Thread 3: 5
Thread 2: 3
Thread 2: 4
Thread 2: 5
注意输出是不确定的(每次运行都可能产生不同输出), 因为没有完全控制线程的执行方式。
3.4 通过实现Runnable接口创建新线程
通过实现Runnable接口创建并运行新线程:
1.定义一个实现Runnable接口的类;
2.在类中为抽象方法run()提供实现来制定线程的操作,(并提供其他如构造函数,变量和方法的实现);
3.Client类创建了一个该新类的实例,该实例称作Runnable对象;
4.Client类接着创建一个新线程对象,构造函数以Runnable对象为参数,然后调用start()方法。Start()调用Runnable对象(而不是Thread类)中的 run()方法。
class MyRunnable extends SomeClass implements Runnable {
// provide implementation to abstract method run()
@Override
public void run() {
// Thread's running behavior
}
......
// constructors, other variables and methods
}
public class Client {
......
Thread t = new Thread(new MyRunnable());
t.start();
...
}
同样地,内部类 (命名或匿名) 通常是为了可读性并能获取对外部类私有变量和方法的访问。例如:
Thread t = new Thread(new Runnable() { // Create an anonymous inner class that implements Runnable interface
public void run() {
// Thread's running behavior
// Can access the private variables and methods of the outer class
}
});
t.start();
3.5 Thread类中的方法
Thread类中的方法包括:
-
public void start()
: 开始一个新线程。JRE 调用该类run()方 法,当前线程继续运行; -
public void run()
: 制定新线程的执行任务。当run()完成时,线程终止; - 中断函数
public static sleep(long millis)throws InterruptedException
public static sleep(long millis, int nanos)throws InterruptedException
public void interrupt()
暂停当前线程并将资源控制权交与其他线程以给定的时间。Sleep()方法是线程安全的因为它不会释放自己的监视器。通过调用 interrupt()方法能在给定时间到达之前唤醒阻塞线程。被唤醒线程会抛出InterruptedException并在继续操作之前执行其InterruptedException 处理函数。这是个静态方法且广泛用来暂停当前线程(通过Thread.sleep()),这样其他线程就有机会执行。 这也在许多应用中提供了必要的延迟。例如:
try {
// Suspend the current thread and give other threads a chance to run
// Also provide the necessary delay
Thread.sleep(100); // milliseconds
} catch (InterruptedException ex) {}
-
public static void yield()
: 暗示调度程序当前线程愿意将当前处理器的使用权交与其他线程,但是该调度程序可以随意忽略这条暗示,因此很少使用; -
public boolean isAlive()
: 如果线程是新建的或死亡返回false;如果线程是"就绪" 或 "阻塞"返回true; -
public void setPriority(sint p)
: 设置线程的优先级,依赖于运行情况。
stop(),suspend(),和resume()方法在JDK 1.4中已经被废弃了,它们因monitor的释放而非线程安全,详见JDK API文档。
3.6 守护线程
有两种线程,分别是守护线程和用户线程。守护线程能通过方法setDaemon(boolean on)设置。守护线程是基础线程,如:垃圾回收线程及 GUI的事件分发线程。JVM在运行线程都是守护线程的时候退出。换言之,当不再有用户线程且所有存留线程都是基础线程时,JVM 认为其任务已完成。
3.7 线程的生命周期
线程刚创建的时候处于"新建"(NEW)状态。在该状态的时候,它只是堆栈中的一个对象,而没有分配任何系统资源来执行。从"新建"状态开始,能做的唯一事情就是调用start()方法,这将线程置于"就绪"(RUNNABLE)状态。调用除start()之外的任何方法会导致IllegalThreadStateException。
start()方法分配必需的系统资源来执行线程,安排线程运行并调用run()。 这将线程置于"就绪"状态。由于大多数计算机只有一个CPU且通过CPU时间片来支持多线程,因此在"就绪"状态下,线程可能运行或等待CPU时间片。
线程不能被启动两次,否则会造成 IllegalThreadStateException。
当以下事件出现时线程会进入"阻塞"状态:
- 调用sleep()挂起线程一段时间以将控制权交给其他线程,也可以调用 yield()来示意调度函数当前线程愿意交出当前处理器的使用权。只是调度函数会随意忽略该暗示;
- 调用wait()方法等待满足特定条件;
- 线程阻塞,并等待I/O操作的完成。
线程再次从"阻塞"状态变成 "就绪":
- 如果线程休眠,特定的休眠时间结束或休眠通过interrupt()方法中断;
- 如果线程通过wait()处于等待状态,调用notify()或者notifyAll()方法通知等待线程条件已经满足并结束等待;
- 阻塞线程的I/O操作完成。
只有当run()方法自然终止并退出时线程才处于“终止”状态。
方法isAlive()能被用来检测线程是否存活。如果线程是"新建" 或 "终止",isAlive()返回false;如果线程是"就绪"或"阻塞"则返回true。
JDK 1.5引入了一新方法getState(),该方法返回 (封装) Thread.State枚举类型,为五种状态中一种 - {NEW, BLOCKED, RUNNABLE, TERMINATED, WAITING}。
- NEW: 线程尚未启动;
- RUNNABLE:
- WAITING:
- BLOCKED: 线程阻塞等待监视器锁;
- TIMED_WAITING: 线程等待一定时间;
- TERMINATED:
4. 线程调度及优先级
JVM 实现固定的线程调度机制。每个线程都会设置一个优先级(在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间)。数字越大,线程优先级越高。当创建新线程,它会继承创建线程的优先级。可以调用方法setPriority()来改变线程的优先级:public void setPriority(int priority);
参数priority取值范围为1(优先级最低)to 10。
JVM选择执行优先级最高的线程。如果有一个以上线程都有最高优先级,JVM会遵循轮询调度。
JVM实行优先权调度机制,在这种环境下如果任何时候有一更高优先级的线程状态转为"就绪",当前低优先级线程将立即让步与更高优先级线程。
如果有两个以上线程处于同一优先级且均处于就绪状态,线程可能会一直运行直至完成而不让步与其他同优先级线程,这样会造成线程饥饿。因此通过sleep()或 yield()让步与其他同级线程是个不错的做法。但是,永远都不可能将控制权交与低优先级线程。
在一些操作系统如Windows中,每一个运行线程都被分配一定量的CPU时间片,该时间片用以阻止其他同优先级线程出现饥饿。但不要依赖时间片,因为它也依赖于运行情况。
因此,正在运行的线程在出现以下情况时会停止执行:
- 更高优先级线程状态为"就绪";
- 正在运行线程通过调用如:sleep(),yield()和wait()等方法主动放弃控制;
- 正在运行线程终止,即其run()方法退出;
- 在执行时间片的系统上,运行线程消耗完自己的CPU时间片;
需要注意的一点是线程调度和优先级都依赖于JVM,因为JVM是虚拟机并需要原生操作系统资源来支持多线程。大部分JVM不保证优先级最高的线程一直在运行,有时为了避免饥饿会选择执行较低优先级线程,因此不应该依赖算法中的优先次序。
5. 监视器锁 & 同步
监视器(monitor)是用来阻塞并唤醒线程的对象,在 java.lang.Object中通过以下机制支持:
- 每个对象都有一个锁;
- 关键字synchronized用来获取对象锁;
- java.lang.Object中的方法wait(),notify()和notifyAll()方法控制线程。
每个java对象都有锁。任何时候,锁最多被一单线程控制。可以对方法或某段代码使用关键字synchronized。要执行对象同步代码的线程必须先要获取该对象的锁。如果该锁为其他线程所控制,那么尝试的线程会进入“找锁”状态,且仅当锁可以访问的时候才准备完成。占用锁的线程执行完同步代码之后便会放弃锁。
5.1 关键词"synchronized"
如下:
public synchronized void methodA() { ...... } // synchronized a method based on this object
public void methodB() {
synchronized(this) { // synchronized a block of codes based on this object
......
}
synchronized(anObject) { // synchronized a block of codes based on another object
......
}
......
}
同步能在方法层面或区块层面进行控制。变量不能同步,需要同步所有访问那个变量的方法。
private static int counter = 0;
public static synchronized void increment() {
++counter;
}
public static synchronized void decrement() {
--counter;
}
也可以对静态方法使用synchronized
。这情况下,需要获取类锁 (而不是实例锁) 来执行方法。
public class SynchronizedCounter {
private static int count = 0;
public synchronized static void increment() {
++count;
System.out.println("Count is " + count + " @ " + System.nanoTime());
}
public synchronized static void decrement() {
--count;
System.out.println("Count is " + count + " @ " + System.nanoTime());
}
}
public class TestSynchronizedCounter {
public static void main(String[] args) {
Thread threadIncrement = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; ++i) {
SynchronizedCounter.increment();
try {
sleep(1);
} catch (InterruptedException e) {}
}
}
};
Thread threadDecrement = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; ++i) {
SynchronizedCounter.decrement();
try {
sleep(1);
} catch (InterruptedException e) {}
}
}
};
threadIncrement.start();
threadDecrement.start();
}
}
Count is -1 @ 71585106672577
Count is 0 @ 71585107040916
Count is -1 @ 71585107580661
Count is 0 @ 71585107720865
Count is 1 @ 71585108577488
Count is 0 @ 71585108715261
Count is 1 @ 71585109590928
Count is 0 @ 71585111400613
Count is 1 @ 71585111640095
Count is 0 @ 71585112581002
Count is 1 @ 71585112748760
Count is 2 @ 71585113580259
Count is 1 @ 71585113729378
Count is 2 @ 71585114579922
Count is 1 @ 71585114712832
Count is 2 @ 71585115578775
Count is 1 @ 71585115722626
Count is 2 @ 71585116578843
Count is 1 @ 71585116719452
Count is 0 @ 71585117583368
需要注意的重要一点是当对象被锁住的时候,同步的方法和代码都会被阻塞。而非同步方法不受锁影响继续执行,因此有必要同步所有操作共享资源的方法。比如,如需要对一变量访问进行同步,那么所有指向该变量方法都应被同步。否则,非同步方法不需先获取锁而直接访问会影响该变量的状态。
5.2 wait(), notify() & notifyAll() 实现线程间同步
这些方法都定义在java.lang.Object 类中(而不是java.land.Thread),这些只能在同步代码中调用。
方法wait()和notify()为共享对象提供了一种暂停线程,在合适的时候继续该线程的方法。
例子: 消费者和生产者
在本例中,生产者生产信息(通过方法putMessage()),该信息在下一条信息生产出来之前被消费者消费 (通过方法getMessage())。在这样一个producer-consumer模式中,一个线程调用wait()将自己挂起(并释放锁),直到另一线程调用notify()或notifyAll()将其唤醒。
// Testing wait() and notify()
public class MessageBox {
private String message;
private boolean hasMessage;
// producer
public synchronized void putMessage(String message) {
while (hasMessage) {
// no room for new message
try {
wait(); // release the lock of this object
} catch (InterruptedException e) { }
}
// acquire the lock and continue
hasMessage = true;
this.message = message + " Put @ " + System.nanoTime();
notify();
}
// consumer
public synchronized String getMessage() {
while (!hasMessage) {
// no new message
try {
wait(); // release the lock of this object
} catch (InterruptedException e) { }
}
// acquire the lock and continue
hasMessage = false;
notify();
return message + " Get @ " + System.nanoTime();
}
}
public class TestMessageBox {
public static void main(String[] args) {
final MessageBox box = new MessageBox();
Thread producerThread = new Thread() {
@Override
public void run() {
System.out.println("Producer thread started...");
for (int i = 1; i <= 6; ++i) {
box.putMessage("message " + i);
System.out.println("Put message " + i);
}
}
};
Thread consumerThread1 = new Thread() {
@Override
public void run() {
System.out.println("Consumer thread 1 started...");
for (int i = 1; i <= 3; ++i) {
System.out.println("Consumer thread 1 Get " + box.getMessage());
}
}
};
Thread consumerThread2 = new Thread() {
@Override
public void run() {
System.out.println("Consumer thread 2 started...");
for (int i = 1; i <= 3; ++i) {
System.out.println("Consumer thread 2 Get " + box.getMessage());
}
}
};
consumerThread1.start();
consumerThread2.start();
producerThread.start();
}
}
Consumer thread 1 started...
Producer thread started...
Consumer thread 2 started...
Consumer thread 1 Get message 1 Put @ 70191223637589 Get @ 70191223680947
Put message 1
Put message 2
Consumer thread 2 Get message 2 Put @ 70191224046855 Get @ 70191224064279
Consumer thread 1 Get message 3 Put @ 70191224164772 Get @ 70191224193543
Put message 3
Put message 4
Consumer thread 2 Get message 4 Put @ 70191224647382 Get @ 70191224664401
Put message 5
Consumer thread 2 Get message 5 Put @ 70191224939136 Get @ 70191224965070
Consumer thread 1 Get message 6 Put @ 70191225071236 Get @ 70191225101222
Put message 6
输出信息 (System.out)可能是乱序的,仔细检查put/get时间戳可确证正确的操作顺序。
同步的生产者方法putMessage()获取对象锁,检查之前信息是否已被清理。如果没有清理则调用wait(),释放对象锁并进入WAITING状态,将该线程置于对象的"等待"组。另一方面,同步的消费者方法getMessage()获取该对象锁并检查新信息。如果有一条新信息,清理之并调用notify(),这会在该对象的"等待"组上任意选择一个线程(在本例中刚好是生产者线程)并将其置于BLOCKED状态。消费者线程相应地进入WAITING状态并将自己置于对象的"等待"组(在wait()方法之后)。生产者线程之后获取锁并继续执行后面操作。
notify()和notifyAll()的区别在于:notify()从该对象的等待池中随意选择一个线程并将其置于找锁状态;而notifyAll()唤醒对象等待池中所有线程。然后被唤醒的线程以正常方式竞争执行。
多线程在类java.lang.Object处编入Java语言,同步锁保存在Object中,用来协调线程的方法wait(),notify(),notifyAll()正好都在在类Object中。
带超时参数的wait()
下面几种形式的wait()方法,带一个超时参数:
public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
超过超时时间线程将也进入BLOCKED状态。
5.3 饥饿与死锁
饥饿(Starvation )指的是一个(或更多)线程不能访问对象的状态。此问题可通过对所有线程设置正确的优先级加以解决。
死锁(Deadlock)指的是一个线程正在等待一个条件,但是程序的其他地方阻止了该条件的满足,因而阻止了线程的执行。典型的例子,也称"死亡拥抱":线程1持有对象A的锁,线程2持有对象B的锁。线程1等待获取对象B的锁,而线程2则等待获取对象A的锁。两个线程都处于死锁状态而不能继续执行。如果两个线程都以相同顺序找锁,这种情况就不会出现。但是在程序中实现这种安排较为复杂。可以选择的方法是要么在另一对象(而不是对象A或B)上同步;要么只同步方法的一部分,而非整个方法。死锁可能会很复杂,因为它可能涉及到很多线程和对象且难于检测。
翻译自:
https://www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html