在JavaEE开发中,多线程是提升程序性能的核心手段之一。无论是处理高并发请求,还是优化IO密集型任务,理解线程的运作机制都至关重要。本文将从进程与线程的关系出发,深入剖析Java中创建线程的五种方式,并探讨线程生命周期中的关键方法与常见陷阱,帮助你构建扎实的并发编程基础。

一、进程与线程:并发编程的基石

在操作系统的世界里,进程是资源分配的基本单位,它拥有独立的内存空间,就像一栋大楼里的独立房间。而线程则是CPU调度的基本单位,它共享进程内的资源,如同在房间内工作的具体人员。当我们双击一个可执行文件(.exe),操作系统会为其创建一个进程,这个进程既包含资源容器(内存、文件句柄等),也承载着具体任务。

线程是进程内的执行路径,一个进程至少包含一个线程(如Java程序的主线程)。通过创建多个线程,我们可以实现并行处理,从而显著提升程序效率。例如,在处理网络请求时,为每个连接分配一个线程,可以避免因等待IO操作而阻塞整个程序。

二、多线程的权衡:效率与开销

虽然多线程能提升效率,但并非线程越多越好。⚠️ 过多的线程会导致上下文切换(Context Switch)开销增加——CPU需要频繁保存和恢复线程状态,同时资源竞争(如锁竞争)也会加剧,反而拖累整体性能。这就像在一个小房间里安排太多人工作,大家互相干扰,效率反而下降。

进程

线程

资源占用

拥有独立资源,内存空间

共享进程资源

创建开销

大(分配独立资源)

小(共享进程资源)

稳定性

进程间相互独立互不影响

一个线程崩溃影响整个进程

上下文切换

代价大

代价小

理解上下文切换是掌握多线程性能的关键。当CPU从任务A切换到任务B时,需要保存A的寄存器状态、程序计数器等信息,再加载B的上下文。这一过程需要时间,因此线程数量应基于CPU核心数和任务特性(CPU密集型 vs IO密集型)合理设置,必要时可参考Go语言中的Goroutine设计理念——轻量级调度。

实践建议:对于CPU密集型任务,线程数通常设置为CPU核心数+1;对于IO密集型任务,可适当增加线程数以减少等待时间。

三、Java线程的创建:五种方式详解

Java中创建线程的方式灵活多样,每种方式都有其适用场景。以下是五种主流写法,从基础到进阶,建议全部掌握,以便在不同场景下选择最合适的方案。

1. 继承Thread类

直接继承Thread类并重写run()方法,这是最直观的方式。注意,start()方法会创建新线程并执行run(),而直接调用run()只会在当前线程中作为普通方法执行,不会并发运行。

class MyThread1 extends Thread{
//run方法是线程执行的入口,跟“main”方法效果差不多
    @Override
    public void run() {
        while (true) {
            System.out.println("hello run");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread1 t = new MyThread1();
        t.start();
        while (true) {
            System.out.println("hello world");
            Thread.sleep(1000);
        }
    }
}

2. 实现Runnable接口

实现Runnable接口并重写run()方法,然后将实例传给Thread构造器。这种方式更灵活,因为Java不支持多继承,但可以实现多个接口。

class MyRunnable1 implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("hello run");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
}
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable1 myRunnable = new MyRunnable1();
        Thread t = new Thread(myRunnable);
        t.start();
        while (true) {
            System.out.println("hello world");
            Thread.sleep(1000);
        }
    }
}

3. 匿名内部类(继承Thread)

如果线程逻辑只在某处使用一次,可以用匿名内部类简化代码。这种方式相当于省略了类定义和命名,直接创建实例。

class MyThread1 extends Thread{
    //可以理解成自己写了Thread的"plus版本",加入了Thread原本没有的方法
    @Override
    public void run() {
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
   //创建你这个"plus版本"的实例
        MyThread1 t = new MyThread1();
}
public class Demo3 {
    public static void main(String[] args) {
        //这里的t不是指向父类Thread,指向的是Thread的一个匿名子类
        Thread t = new Thread(){
            public void run(){
                while (true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();
        //主线程
        while (true){
        System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
MyThread1 t = new MyThread1();

4. 匿名内部类(实现Runnable)

与第三种方式类似,但基于接口实现,省略的是实现类的定义。

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable(){
            public void run(){
                while (true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
        //主线程
        while (true){
        System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

5. Lambda表达式(最推荐)

Lambda表达式是Java 8引入的语法糖,它本质上是Runnable接口的简洁实现。按住Ctrl点击Lambda表达式,可以看到底层就是run()方法。这种方式代码最简洁,可读性最高,是现代Java开发的首选

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (true){
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        while (true){
            System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
对比参考:类似的语言如Python和JavaScript也提供了简洁的线程/异步语法(如Python的threading模块、JavaScript的async/await),但Java的线程模型更贴近操作系统底层,理解它有助于深入理解并发原理。

四、线程生命周期:关键方法与实战陷阱

1. 守护线程(Daemon Thread)与isAlive()

守护线程(通过setDaemon(true)设置)是“配角”线程,当所有前台线程结束时,后台线程会自动终止。例如垃圾回收线程就是守护线程。而isAlive()方法用于检查线程是否仍在运行。

package JavaEE;
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
            while (true) {
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //在线程创建前设置(将它设置成后台线程)
        t.setDaemon(true);
        //线程创建
        t.start();
        //让main(唯一的前台线程)等会儿再结束
        // 因为前台线程结束整个进程就会结束
        Thread.sleep(3000);
    }
}
一个线程    未执行/执行结束---------false
                              正在执行---------true
package JavaEE;
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
            for (int i = 0;i < 3;i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("hello Thread");
            }
        });
//        线程还未创建,因此是false
        System.out.println(t.isAlive());
//        创建线程
        t.start();
//        这个等待是让main这个前台线程执行的慢一些,
//        这样一来方便获取到正在执行的那个线程的存活状态
        Thread.sleep(2000);
//        线程正在执行,因此是true
        System.out.println(t.isAlive());
//        一个线程创建出来之后默认是前台线程
//        这个等待是为了让我们创建的那个先结束再观察存活状态
        Thread.sleep(4000);
//        线程执行结束了,因此是false
        System.out.println(t.isAlive());
    }
}

2. 线程休眠与中断

sleep()方法让线程暂停执行指定的毫秒数。但要注意,sleep()不会释放锁,且如果被中断,会抛出InterruptedException。在循环中,若使用局部变量作为标志位,需确保变量是finaleffectively final,否则会编译报错。

这里涉及到一个lambda变量捕获的点,涉及到lambda访问外部变量为什么需要"final/effectively final",必要的话可以具体搜一下
package JavaEE;
import java.util.Scanner;
public class Demo8 {
    private static boolean a = true;
    public static void main(String[] args) {
    //创建一个局部变量a
    //boolean a = true;
        Thread t = new Thread(()-> {
            while (a) {
                System.out.println("hello world");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread结束");
        });
        t.start();
        //作为局部变量它不能被修改,修改了就不满足effectively final了
        //反过来说,如果局部变量没有被修改,idea会判定它是effectively final
        //修改这个变量
        //a = false;
        Scanner scanner = new Scanner(System.in);
        System.out.println("请随便输入一些东西来终止线程");
        scanner.next();
        //修改成功了!!!
        //因为他是一个成员变量/(静态变量也可以)
        a = false;
    }
}

另外发现的点:假如在lambda内部尝试修改会出现什么结果?

线程中断interrupt())提供了一种协作机制,让长时间运行或阻塞的线程能够响应中断请求。⚠️ 注意:interrupt()会设置中断标志位,但sleep()wait()等方法在收到中断时会清空标志位并抛出异常,需在catch块中妥善处理。

.cyrrentThread是获取当前线程的对象

.isInterrupted()是检查是否设置了标志位interrupt

package JavaEE;
public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//InterruptedException是一种通信机制,不是错误,使用RuntimeException会丢失这个信息
                  //throw new RuntimeException(e);
                    //直接终止
                    break;
                }
            }
            System.out.println("线程结束");
        });
        t.start();
        System.out.println("hello main");
        Thread.sleep(1000);
        //不仅起到设置标志位的作用,同时也起到中断阻塞的作用(这里是lambda里的sleep)
        t.interrupt();
    }
}

3. 线程等待(join)

join()方法让当前线程等待指定线程结束后再继续执行。这在需要汇总多个子线程结果时非常有用。使用时务必通过线程实例调用,而非Thread类静态调用。

线程都可以互相等待,这个没有什么优先级
实际开发中更倾向于在join()中加个上限参数避免死等
package JavaEE;
public class Demo10 {
    private static int result = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            int sum = 0;
            for (int i = 1; i <= 100 ; i++) {
                sum += i;
            }
            result = sum;
        });
        t.start();
//可能会多余出来很多时间,时间不好把握
//        try {
//            Thread.sleep(1000);
//        } catch (InterruptedException e) {
//            throw new RuntimeException(e);
//        }
//这种处理看似可以,但不断进入循环也有开销
//        while (result == 0) {
//            try {
//                Thread.sleep(1);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//        }
        try {
            t.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //在main方法中打印结果
        System.out.println(result);
    }
}
这个是获取引用的写法,顺便展示了一下新线程等待主线程
package JavaEE;
public class Demo11 {
    private static int result = 0;
    public static void main(String[] args) {
        //获取线程名称
        Thread maintained = Thread.currentThread();
        System.out.println("线程名称: "+ maintained.getName());
        Thread t = new Thread(()->{
            try {
                maintained.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("计算结果: "+ result);
        });
        t.start();
        int sum = 0;
        for (int i = 1; i <= 100 ; i++) {
            sum += i;
        }
        result = sum;
    }
}

五、总结与最佳实践

多线程编程是JavaEE开发的必备技能,但也是一把双刃剑。掌握进程与线程的本质区别,理解上下文切换的开销,是设计高效并发系统的前提。在创建线程时,推荐优先使用Lambda表达式Runnable接口,以保持代码的灵活性和可读性。同时,务必注意守护线程的生命周期、中断机制的协作性,以及join()的调用方式。

[AFFILIATE_SLOT_1]

最后,建议在实际项目中结合线程池(如ExecutorService)来管理线程生命周期,避免频繁创建和销毁线程带来的性能损耗。类似地,Python的concurrent.futures和Go的Channel也提供了高级并发抽象,但理解底层的线程模型永远是进阶的关键。

[AFFILIATE_SLOT_2]

希望本文能帮助你构建清晰的多线程知识框架。如果你觉得有用,欢迎分享给更多开发者,一起交流进步!