深入 Java 多线程编程:理解 Thread 与 Runnable 接口,掌握线程的创建、启动、暂停与终止,实现并发程序的开发。

好的,朋友们,大家好!今天咱们聊点刺激的,聊聊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 { ... }

第二站:创建线程,就像“克隆”一个自己去干活

创建线程的方式,咱们上面已经演示过了,主要有两种:

  1. 继承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(); // 启动线程
        }
    }
  2. 实现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()方法会做以下两件事:

  1. 创建新的线程: 在操作系统层面创建一个新的线程。
  2. 调用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频繁切换,反而降低程序的执行效率。

并发编程的原则:

  • 尽量避免共享资源: 如果多个线程不需要访问同一个资源,就尽量避免共享。
  • 使用线程安全的数据结构: 例如ConcurrentHashMapCopyOnWriteArrayList等。
  • 使用锁来保护共享资源: 例如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多线程编程的基本知识。你已经从一个对多线程一无所知的“菜鸟”,蜕变成了一个可以编写并发程序的“老鸟”。 🥳

记住: 多线程编程是一个复杂而有趣的领域。只有不断学习和实践,才能真正掌握它。希望你能够继续探索,不断提高自己的编程水平,成为一名真正的多线程编程专家! 💪

最后,送你一句名言: “代码虐我千百遍,我待代码如初恋。” 祝你编程愉快! 🎉

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注