一、继承Thread
继承Thread的方式确实很简单,但是缺点也是很明显的:
- 线程类只能单继承,无法再继承别的业务类;
- 无法使用线程池;
@Slf4j
public class FruitThread extends Thread {
private String fruitName;
public FruitThread(String fruitName){
this.fruitName = fruitName;
}
@Override
public void run() {
log.info("get fruit: {}", fruitName);
}
}
@Slf4j
public class FruitTest {
public static void main(String[] args) {
Thread appleThread = new FruitThread("apple");
Thread bananaThread = new FruitThread("banana");
Thread orangeThread = new FruitThread("orange");
log.info("开始执行线程...");
appleThread.start();
bananaThread.start();
orangeThread.start();
log.info("结束执行线程...");
}
}
输出结果中可以看到,每个线程打印日志的线程编号是不同的,执行事件确实一样的。
11:27:34.297 [main] INFO thread.FruitTest - 开始执行线程...
11:27:34.301 [main] INFO thread.FruitTest - 结束执行线程...
11:27:34.301 [Thread-2] INFO thread.FruitThread - get fruit: orange
11:27:34.301 [Thread-0] INFO thread.FruitThread - get fruit: apple
11:27:34.301 [Thread-1] INFO thread.FruitThread - get fruit: banana
二、实现Runnable
Runnable的优点:
- 可以实现多个类;
- 可以使用线程池;
@Slf4j
public class FruitThread implements Runnable {
private String fruitName;
public FruitThread(String fruitName){
this.fruitName = fruitName;
}
@Override
public void run() {
log.info("get fruit: {}", fruitName);
}
}
@Slf4j
public class FruitTest {
public static void main(String[] args) {
Runnable appleRunnable = new FruitThread("apple");
Runnable bananaRunnable = new FruitThread("banana");
Thread appleThread = new Thread(appleRunnable);
// 可以使用同一个Runnable再次创建一个线程
Thread appleThread2 = new Thread(appleRunnable);
Thread bananaThread = new Thread(bananaRunnable);
log.info("开始执行线程...");
appleThread.start();
bananaThread.start();
appleThread2.start();
log.info("结束执行线程...");
}
}
输出结果如下:
16:07:32.527 [main] INFO thread.FruitTest - 开始执行线程...
16:07:32.529 [main] INFO thread.FruitTest - 结束执行线程...
16:07:32.530 [Thread-1] INFO thread.FruitThread - get fruit: apple
16:07:32.530 [Thread-0] INFO thread.FruitThread - get fruit: apple
16:07:32.530 [Thread-2] INFO thread.FruitThread - get fruit: banana
三、实现Callable
Callable除了具有Runnable的优点之外,还有如下优点:
- 能够返回线程的执行结果;
- 允许向上层抛出异常,而Runnable是不允许的;
@Slf4j
public class FruitThread implements Callable<String> {
private String fruitName;
public FruitThread(String fruitName){
this.fruitName = fruitName;
}
@Override
public String call() {
log.info("get fruit: {}", fruitName);
return fruitName + " end";
}
}
@Slf4j
public class FruitTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> appleCallable = new FruitThread("apple");
FutureTask<String> appleTask = new FutureTask<>(appleCallable);
Callable<String> orangeCallable = new FruitThread("apple");
FutureTask<String> orangeTask = new FutureTask<>(orangeCallable);
Thread appleThread = new Thread(appleTask);
Thread orangeThread = new Thread(orangeTask);
log.info("开始执行线程...");
appleThread.start();
// 获取线程执行结果,会阻塞主线程
log.info("apple return : {}", appleTask.get());
orangeThread.start();
log.info("orange return : {}", orangeTask.get());
// 如果需要获取子线程的执行结果,那主线程就会就等待该执行结果处理完成才执行
log.info("结束执行线程...");
}
}
当然,正式的生产上都是不允许直接这样创建线程的,而是使用线程池的方式,后面的文章会详细讲解。
四、线程间变量的共享
- 线程独享变量
@Slf4j
public class MyThread implements Runnable {
// 线程间独享变量
private int count = 200;
@Override
public void run() {
count--;
log.info("当前线程名称:{},计数器为:{}", Thread.currentThread().getName(), count);
}
}
- 线程共享变量
@Slf4j
public class MyThread implements Runnable {
// 线程间共享变量
private static int count = 200;
@Override
public void run() {
count--;
log.info("当前线程名称:{},计数器为:{}", Thread.currentThread().getName(), count);
}
}
- 线程安全地共享变量
@Slf4j
public class MyThread implements Runnable {
// 线程间共享变量
private static int count = 200;
@Override
public synchronized void run() {
count--;
log.info("当前线程名称:{},计数器为:{}", Thread.currentThread().getName(), count);
}
}
五、停止线程
- 等待线程执行完毕,正常停止;
- stop,已经废弃了,不建议使用,强制使用可能会造成不可预料的结果;
- interrupt,建议使用。
关于interrupt的使用,需要做下详细介绍。
当我们在主线程中调用interrupt方法想停止子线程的时候,子线程并不是就立即停止了,而仅仅是给子线程打上了一个停止的标记。
@Slf4j
public class MyThread implements Runnable {
private int count = 0;
@Override
public void run() {
while(true){
count++;
log.info("当前线程名称:{},计数器为:{}", Thread.currentThread().getName(), count);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyThread());
thread.start();
Thread.sleep(2000);
thread.interrupt();
}
我们运行main方法后发现,2秒后interrupt并没有使得子线程停止,还是在一直运行中。
那么如何使用interrupt才能停止一个线程呢?
我们使用interrupted或者isInterrupted来判断当前线程的状态,从而决定线程是否停止运行。
@Slf4j
public class MyThread implements Runnable {
private int count = 0;
@Override
public void run() {
while(true){
// 此处也可以使用Thread.interrupted()
if(Thread.currentThread().isInterrupted()){
log.info("线程终止执行!");
// 终止线程的执行
return;
}
count++;
log.info("当前线程名称:{},计数器为:{}", Thread.currentThread().getName(), count);
}
}
}
此时再次执行main方法,就能在2秒后停止线程的运行了。
需要改进的是,我们应该在检测到线程终止时抛出异常,而不是简单地return,这样的话,线程catch异常之后可以继续向外部抛出,供调用者进一步处理。
六、暂停和恢复线程
suspend和resume已经被废弃使用了。主要是因为如下两个原因:
-
独占锁问题
suspend方法暂停线程时,如果有锁的情况就不会释放锁,容易造成其他线程无法持有锁,从而就无法进入run方法。
-
两个方法无法同步数据
suspend方法和resume方法不是同步方法,可能造成脏读。
七、yield
将当前线程的CPU时间片让给其它线程,回到就绪状态等待分配CPU时间片继续执行,则个等待的时间是不可确定的。
八、线程优先级
线程的优先级总共分为1~10个级别,还有三个常量代表常用的优先级:
- MIN_PRIORITY = 1
- NORM_PRIORITY = 5
- MAX_PRIORITY = 10
线程的优先级具有如下特性:
- 继承性,子线程的优先级继承自父线程的优先级;
- 规则性,优先级高的线程会大概率被CPU优先执行;
- 随机性,优先级低的线程并不一定比优先级高的线程晚执行结束。
九、守护线程
可以通过setDaemon(true)来设置守护线程,当所有的用户线程都运行结束后,守护线程就会开始自动销毁。
网友评论