好的,朋友们,大家好!今天咱们聊点刺激的,聊聊Java多线程编程。别看“多线程”这几个字听起来高大上,好像只有大神才能驾驭,其实只要你掌握了正确的“姿势”,也能玩转并发,让你的程序跑得飞起来!🚀
咱们今天就深入浅出地讲讲Thread和Runnable接口,手把手教你创建、启动、暂停、终止线程,最终让你能够开发出高效稳定的并发程序。准备好了吗?系好安全带,咱们要发车了!🚂
第一站:理解Thread与Runnable,好比武侠小说里的内功心法和招式
首先,我们要搞清楚Thread和Runnable的关系。这就像武侠小说里的内功心法和招式。
-
Thread类: 相当于内功心法,它本身就是一个线程类。你可以直接继承Thread类,重写
run()
方法,在run()
方法里写你的线程要执行的任务。就像你练了一门叫做“Thread神功”的内功,然后直接用这门内功去攻击敌人。// 继承Thread类 class MyThread extends Thread { @Override public void run() { System.out.println("MyThread running... from " + Thread.currentThread().getName()); // 打印当前线程的名称 } } public class ThreadDemo { public static void main(String[] args) { MyThread thread1 = new MyThread(); thread1.start(); // 启动线程 } }
运行结果大概是:
MyThread running... from Thread-0
-
Runnable接口: 相当于招式。Runnable接口定义了一个
run()
方法,但是它本身不是线程。你需要创建一个实现了Runnable接口的类,然后在run()
方法里写你的线程要执行的任务。然后,你需要把这个Runnable对象传递给Thread类,让Thread类去执行这个Runnable对象里的run()
方法。就像你学了一套叫做“Runnable剑法”的招式,但是你自己没有内力,你需要找一个有内力的人(Thread类)来帮你使出这套剑法。// 实现Runnable接口 class MyRunnable implements Runnable { @Override public void run() { System.out.println("MyRunnable running... from " + Thread.currentThread().getName()); // 打印当前线程的名称 } } public class RunnableDemo { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread2 = new Thread(myRunnable); // 将Runnable对象传递给Thread thread2.start(); // 启动线程 } }
运行结果大概是:
MyRunnable running... from Thread-0
那么问题来了,什么时候用Thread,什么时候用Runnable呢? 🤔
-
优先使用Runnable接口: 因为Java是单继承的,如果你继承了Thread类,就不能再继承其他的类了。但是你可以实现多个接口,所以使用Runnable接口更加灵活。就像你学了“Runnable剑法”,还可以学“Runnable刀法”,甚至“Runnable棍法”!
-
Thread类: 只有在你需要对Thread类进行特殊定制的时候,才考虑继承Thread类。这种情况比较少见。
总结一下:
特性 | Thread类 | Runnable接口 |
---|---|---|
本质 | 线程类 | 接口 |
继承性 | 继承Thread类,无法继承其他类 | 可以实现多个接口 |
灵活性 | 较低 | 较高 |
使用场景 | 需要对Thread类进行特殊定制的情况 | 一般情况,推荐使用Runnable接口 |
类比 | 内功心法 | 招式 |
代码示例 | class MyThread extends Thread { ... } |
class MyRunnable implements Runnable { ... } |
第二站:创建线程,就像“克隆”一个自己去干活
创建线程的方式,咱们上面已经演示过了,主要有两种:
-
继承Thread类:
class MyThread extends Thread { @Override public void run() { // 这里写线程要执行的任务 System.out.println("Thread: " + Thread.currentThread().getName() + " is running"); } } public class CreateThreadDemo { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 } }
-
实现Runnable接口:
class MyRunnable implements Runnable { @Override public void run() { // 这里写线程要执行的任务 System.out.println("Thread: " + Thread.currentThread().getName() + " is running"); } } public class CreateRunnableDemo { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); // 启动线程 } }
注意: 创建线程对象后,一定要调用start()
方法来启动线程。直接调用run()
方法,只是普通的方法调用,不会创建新的线程。这就像你“克隆”了一个自己,但是没有给它“灵魂”,它只是一个空壳子,不会帮你干活。
第三站:启动线程,让“克隆人”开始干活
启动线程的关键是调用start()
方法。start()
方法会做以下两件事:
- 创建新的线程: 在操作系统层面创建一个新的线程。
- 调用
run()
方法: 在新线程中执行run()
方法里的代码。
一定要注意: start()
方法只能调用一次。多次调用会抛出IllegalThreadStateException
异常。就像你不能给一个“克隆人”多次注入“灵魂”一样。
第四站:暂停线程,让“克隆人”休息一下
暂停线程,听起来很美好,但实际上很危险。Java提供了一些方法来暂停线程,例如Thread.sleep()
,Thread.yield()
。
-
Thread.sleep(long millis)
: 让当前线程休眠指定的毫秒数。线程会进入阻塞状态,让出CPU资源,让其他的线程有机会执行。这就像你让“克隆人”休息一下,打个盹。😴public class SleepDemo { public static void main(String[] args) throws InterruptedException { System.out.println("Thread: " + Thread.currentThread().getName() + " is running"); Thread.sleep(2000); // 休眠2秒 System.out.println("Thread: " + Thread.currentThread().getName() + " is awake"); } }
-
Thread.yield()
: 让当前线程放弃CPU资源,让其他的线程有机会执行。但是,具体是否放弃,取决于操作系统的调度策略。这就像你跟“克隆人”说:“你累了就休息一下吧”,但是它是否真的休息,取决于它的心情。public class YieldDemo extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); Thread.yield(); // 放弃CPU资源 } } public static void main(String[] args) { YieldDemo thread1 = new YieldDemo(); YieldDemo thread2 = new YieldDemo(); thread1.start(); thread2.start(); } }
不推荐使用的暂停方法: Thread.suspend()
和Thread.resume()
。这两个方法已经被废弃了,因为它们容易导致死锁。就像你突然把“克隆人”冻结住,然后又突然解冻,它可能会精神错乱。
第五站:终止线程,让“克隆人”彻底消失
终止线程,比暂停线程更危险。Java也提供了一些方法来终止线程,例如Thread.stop()
。
强烈不推荐使用Thread.stop()
方法: 这个方法已经被废弃了,因为它容易导致数据不一致。就像你突然把“克隆人”炸成碎片,它正在做的事情可能只完成了一半,导致数据损坏。💥
推荐的终止线程的方式:
-
使用
volatile
变量作为标志位: 在run()
方法里,使用一个volatile
变量作为标志位,当标志位为false
时,线程就停止执行。这就像你给“克隆人”一个开关,当开关关闭时,它就自动停止工作。public class StopThreadDemo extends Thread { private volatile boolean running = true; @Override public void run() { while (running) { System.out.println("Thread: " + Thread.currentThread().getName() + " is running"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread: " + Thread.currentThread().getName() + " is stopped"); } public void stopThread() { running = false; } public static void main(String[] args) throws InterruptedException { StopThreadDemo thread = new StopThreadDemo(); thread.start(); Thread.sleep(1000); thread.stopThread(); // 停止线程 } }
-
使用
interrupt()
方法: 调用interrupt()
方法会给线程发送一个中断信号。线程可以通过isInterrupted()
方法来判断是否收到了中断信号,然后决定是否停止执行。这就像你给“克隆人”发送一个“停止工作”的指令,它收到指令后,可以优雅地结束自己的工作。public class InterruptThreadDemo extends Thread { @Override public void run() { while (!isInterrupted()) { System.out.println("Thread: " + Thread.currentThread().getName() + " is running"); try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println("Thread: " + Thread.currentThread().getName() + " is interrupted"); interrupt(); // 重新设置中断状态 } } System.out.println("Thread: " + Thread.currentThread().getName() + " is stopped"); } public static void main(String[] args) throws InterruptedException { InterruptThreadDemo thread = new InterruptThreadDemo(); thread.start(); Thread.sleep(1000); thread.interrupt(); // 中断线程 } }
总结一下:
操作 | 不推荐方法 | 推荐方法 |
---|---|---|
暂停线程 | Thread.suspend() , Thread.resume() |
Thread.sleep() , Thread.yield() |
终止线程 | Thread.stop() |
使用volatile 变量作为标志位,使用interrupt() 方法 |
安全性 | 容易导致死锁和数据不一致 | 更加安全,可以保证数据的一致性 |
类比 | 突然冻结/炸毁“克隆人” | 给“克隆人”一个开关/发送“停止工作”的指令,让它优雅地结束自己的工作 |
第六站:并发程序的开发,让你的程序跑得更快更稳
掌握了线程的创建、启动、暂停和终止,我们就可以开始开发并发程序了。并发程序可以充分利用多核CPU的优势,提高程序的执行效率。
并发编程的挑战:
- 线程安全: 多个线程同时访问共享资源时,可能会出现数据不一致的问题。
- 死锁: 多个线程相互等待对方释放资源,导致程序无法继续执行。
- 性能问题: 过多的线程会导致CPU频繁切换,反而降低程序的执行效率。
并发编程的原则:
- 尽量避免共享资源: 如果多个线程不需要访问同一个资源,就尽量避免共享。
- 使用线程安全的数据结构: 例如
ConcurrentHashMap
,CopyOnWriteArrayList
等。 - 使用锁来保护共享资源: 例如
synchronized
关键字,ReentrantLock
等。 - 避免死锁: 例如避免循环等待资源。
- 合理控制线程数量: 根据CPU核心数和任务的特点,合理控制线程数量。
并发编程的工具:
synchronized
关键字: 用于保护共享资源,保证同一时刻只有一个线程可以访问该资源。ReentrantLock
类: 提供比synchronized
更灵活的锁机制。Condition
接口: 用于实现线程之间的等待和唤醒。ExecutorService
接口: 用于管理线程池,提高线程的利用率。CountDownLatch
类: 用于实现线程之间的同步。CyclicBarrier
类: 用于实现多个线程之间的同步。Semaphore
类: 用于控制同时访问某个资源的线程数量。
举个例子: 假设我们要统计一个大文件中每个单词出现的次数。我们可以把文件分成多个小块,每个线程负责统计一个小块中单词出现的次数,最后把所有线程的结果合并起来。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WordCount {
private static final int NUM_THREADS = 4; // 线程数量
public static void main(String[] args) throws IOException, InterruptedException {
String filePath = "large_file.txt"; // 替换为你的大文件路径
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
// 读取文件内容
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
long fileSize = new java.io.File(filePath).length();
long chunkSize = fileSize / NUM_THREADS;
long start = 0;
for (int i = 0; i < NUM_THREADS; i++) {
long end = (i == NUM_THREADS - 1) ? fileSize : start + chunkSize;
executor.submit(new WordCountTask(filePath, start, end));
start = end;
}
}
// 关闭线程池
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
// 合并结果
Map<String, Integer> finalCounts = new HashMap<>();
for (WordCountTask task : WordCountTask.taskList) {
for (Map.Entry<String, Integer> entry : task.wordCounts.entrySet()) {
String word = entry.getKey();
int count = entry.getValue();
finalCounts.put(word, finalCounts.getOrDefault(word, 0) + count);
}
}
// 打印结果
finalCounts.forEach((word, count) -> System.out.println(word + ": " + count));
}
}
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class WordCountTask implements Runnable {
private String filePath;
private long start;
private long end;
public Map<String, Integer> wordCounts = new HashMap<>();
public static List<WordCountTask> taskList = new ArrayList<>();
public WordCountTask(String filePath, long start, long end) {
this.filePath = filePath;
this.start = start;
this.end = end;
taskList.add(this);
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
reader.skip(start);
long currentPosition = start;
String line;
while (currentPosition < end && (line = reader.readLine()) != null) {
String[] words = line.split("\s+");
for (String word : words) {
word = word.toLowerCase().replaceAll("[^a-zA-Z]", ""); // 清理单词
if (!word.isEmpty()) {
wordCounts.put(word, wordCounts.getOrDefault(word, 0) + 1);
}
}
currentPosition += line.length() + 1; // +1 for newline character
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
第七站:总结,从“菜鸟”到“老鸟”的蜕变
恭喜你,朋友!经过今天的学习,你已经掌握了Java多线程编程的基本知识。你已经从一个对多线程一无所知的“菜鸟”,蜕变成了一个可以编写并发程序的“老鸟”。 🥳
记住: 多线程编程是一个复杂而有趣的领域。只有不断学习和实践,才能真正掌握它。希望你能够继续探索,不断提高自己的编程水平,成为一名真正的多线程编程专家! 💪
最后,送你一句名言: “代码虐我千百遍,我待代码如初恋。” 祝你编程愉快! 🎉