先说一下我最朴素的理解,进程是应用程序的实例,进程之间的通信代价比较高;而线程就要更加轻量化,可以方便地完成相互之间的通信。
线程的创建
在Java中,线程也是一个类,是一个抽象类,Thread。可以简单地通过new Thread()
来创建一个线程对象,但是要重写其run()
方法。
Thread thread = new Thread() {
@Override
public void run() {
// ...
}
};
在合适版本的Java中,可以用Lambda表达式。
Thread thread = new Thread(() -> {
// ...
});
像这样,就创建了一个线程对象thread
。
除了用Thread
抽象类来创建之外,还可以用Runnable
接口。
如果自己建一个类,继承于Thread,用static属性来保存公共数据,这样就把线程的控制和业务逻辑混合在一起了。
public class Study1 {
public static void main(String[] args) {
int size = 4;
String[] chineseNums = {"甲", "乙", "丙", "丁"};
TicketWindow[] ticketWindows = new TicketWindow[size];
for (int i = 0; i < size; i++) {
ticketWindows[i] = new TicketWindow("窗口" + chineseNums[i]);
}
for (int i = 0; i < size; i++) {
ticketWindows[i].start();
}
System.out.println("启动各柜台完毕!");
}
}
class TicketWindow extends Thread {
private final String name;
private static final int MAX = 50;
private static int index = 1; // 注意static
public TicketWindow(String name) {
this.name = name;
}
@Override
public void run() {
while (index <= MAX) {
System.out.println(name + ":" + (index++));
}
}
}
Runnable的对象可以作为Thread的参数,这样有一个好处,就是共享数据。可以把一个Runnable对象作为多个Thread的参数,那实际上这些线程都共享了这个Runnable对象的数据。
public class Study2 {
public static void main(String[] args) {
int size = 4;
String[] chineseNums = {"甲", "乙", "丙", "丁"};
TicketWindowRunnable runnable = new TicketWindowRunnable();
Thread[] threads = new Thread[size];
for (int i = 0; i < size; i++) {
threads[i] = new Thread(runnable, "窗口" + chineseNums[i]);
}
for (int i = 0; i < size; i++) {
threads[i].start();
}
System.out.println("启动各柜台完毕!");
}
}
/**
* 改进之后,不再使用static的index
*/
class TicketWindowRunnable implements Runnable {
private static final int MAX = 50;
private int index = 1; // 不需要static
@Override
public void run() {
while (index <= MAX) {
System.out.println(Thread.currentThread().getName() + ":" + (index++));
}
}
}
理想的情况下,上述代码会让甲乙丙丁四个窗口,发放1~50的票据。这里的index
不需要静态,因为它是TicketWindowRunnable对象的一个属性,是唯一的。
不得不指出,上述代码不是线程安全的,会有号码重复、遗漏,甚至会有号码溢出。
线程一定是由另一个线程创建的,那个创建它的线程称为“父线程”。如果没有显式地指定Group,那么新创建的子线程会和父线程在同一个Group之中,拥有相同的优先级,相同的daemon。
wait
wait一般放在循环体里,含义是一直等,直到等待到满足某一条件为止。
例如
synchronized void setProductId(int id) {
// 一直等 等到writeable为真为止
while (!writeable) {
try {
wait();
} catch (InterruptedException ignored) {
}
}
productId = id;
writeable = false;
notify();
}
synchronized int getProductId() {
// 一直等 等到writeable为假为止
while (writeable) {
try {
wait();
} catch (InterruptedException ignored) {
}
}
writeable = true;
notify();
return productId;
}
wait让当前线程处于阻塞状态,直到其它的线程notify或notifyAll,这个时候才会尝试接触阻塞。是工程上常用notifyAll。
这方面可以参考
java wait()方法用法详解
守护线程
守护线程(Daemon Thread)会自动退出,这一自动退出的时机就是其它非守护线程都退出了,它就会自动退出。
比如一辆大巴车,上面的乘客都是普通线程(非守护线程),而公交车司机是守护线程。当乘客全部下车之后,司机就会自动下车。这样的线程就是守护线程。
Java中的垃圾回收线程,就是典型的守护线程。
Thread API
sleep
让当前线程暂停,进入休眠,可以设定休眠时间。休眠时,不会放弃monitor锁的所有权。
在JDK1.5之后,可以用TimeUnit代替Thread.sleep。
例如
TimeUnit.SECONDS.sleep(1);
即休眠1秒。TimeUnit有单位,而sleep只有毫秒和纳秒,不太方便。
yield
提醒调度器我愿意放弃当前的CPU资源,如果CPU资源不紧张,则会忽略这种提醒。
interrupt
使用wait、sleep、join等方法会让线程进入阻塞状态。如果这个时候另外一个线程来打断它(使用interrupt方法),就会打断这种阻塞。
打断一个线程并不意味着这个线程生命周期的结束,而是仅仅打断了阻塞状态。被打断了之后,会抛出一个InterruptedException的异常。
如果一个线程已经是死亡状态,则任何打断都会被忽略。
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println(thread.isInterrupted()); // false
thread.interrupt();
System.out.println(thread.isInterrupted()); // true
以上代码所示,在调用interrupt()
打断之前,输出为false,之后为true。
如果我们在thread
之中加入了捕获异常的内容。
Thread thread = new Thread(() -> {
while (true) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
System.out.println("A");
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println(thread.isInterrupted()); // false
thread.interrupt();
TimeUnit.SECONDS.sleep(1);
System.out.println(thread.isInterrupted()); // false
那么两次得到的结果就都是false了。因为sleep这个可中断方法,在捕捉到中断信号之后,会擦除interrupt标识。
isInterrupted
仅用于判断是否被打断,不会擦除标识。
interrupted
用于判断是否被打断,之后再擦除interrupt标识。
join
在a线程的代码里调用b.join()
,表示a开始等b,直到b线程结束生命周期。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
});
thread.start();
thread.join(); // 注意
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
会先输出Thread-0 0
Thread-0 1
……,全部输出完毕后,再输出main 0
……
如果把// 注意
所在行的代码删除,那么就会是Thread-0和main交替输出。
例子
现在欲用个线程读取个API。我们创建并start了各个线程之后,都需要join,等待这个线程都结束,才汇总数据。
优先级
setPriority()
和getPriority
。
优先级介于1(含)和10(含)之间,而且设置的优先级不能大于线程所在group的优先级。如果给线程设定大于group优先级的优先级,则会被group的最大优先级取而代之。
网友评论