答案:Java中创建和启动线程需定义任务并调用start()方法。可通过实现Runnable接口或继承Thread类定义任务,前者更灵活且推荐;启动时调用start()而非run(),因start()由JVM创建新线程并执行run(),而直接调用run()仅在当前线程执行,无并发效果。

Java中创建和启动线程,核心思路其实很简单:你需要定义一个线程要执行的任务(也就是它要“跑”的代码),然后把这个任务交给一个线程对象,最后让这个线程对象“动起来”。具体来说,我们通常通过实现
Runnable接口或者继承
Thread类来定义任务,再通过调用线程对象的
start()方法来真正启动它,让它在独立的执行路径上运行。
解决方案
在Java里创建并启动线程,最常见且推荐的做法有两种:实现
Runnable接口,或者继承
Thread类。
1. 实现Runnable
接口
这是更灵活也更推荐的方式。你定义一个类去实现
Runnable接口,然后重写它的
run()方法。这个
run()方法里就是线程要执行的业务逻辑。
立即学习“Java免费学习笔记(深入)”;
class MyRunnableTask implements Runnable {
private String taskName;
public MyRunnableTask(String name) {
this.taskName = name;
}
@Override
public void run() {
// 这就是线程要执行的任务代码
System.out.println(Thread.currentThread().getName() + " 正在执行任务: " + taskName);
try {
// 模拟任务执行耗时
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 的任务被中断了。");
Thread.currentThread().interrupt(); // 重新设置中断状态
}
System.out.println(Thread.currentThread().getName() + " 任务 " + taskName + " 完成。");
}
}
// 如何启动:
// MyRunnableTask task1 = new MyRunnableTask("任务A");
// Thread thread1 = new Thread(task1, "工作线程-A"); // 将任务封装进Thread对象
// thread1.start(); // 启动线程这种方式的好处在于,你的任务类可以继续继承其他类,因为Java是单继承的。同时,多个
Thread对象可以共享同一个
Runnable实例,这在需要共享数据或状态的场景下非常有用。
2. 继承Thread
类
另一种方式是直接创建一个类继承
Thread类,然后重写其
run()方法。
class MyThreadWorker extends Thread {
private String workerName;
public MyThreadWorker(String name) {
super(name); // 调用父类构造器设置线程名
this.workerName = name;
}
@Override
public void run() {
// 线程的业务逻辑
System.out.println(Thread.currentThread().getName() + " 启动,作为工作者: " + workerName);
try {
Thread.sleep(150);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断了。");
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " 完成工作。");
}
}
// 如何启动:
// MyThreadWorker worker1 = new MyThreadWorker("专属工人-1");
// worker1.start(); // 直接启动Thread子类实例这种方式虽然也能工作,但由于Java的单继承限制,如果你的类还需要继承其他父类,就不能再继承
Thread了。所以,我个人更倾向于使用
Runnable接口。
3. 使用Lambda表达式(Java 8+)
对于简单的任务,我们还可以结合Lambda表达式来创建和启动线程,代码会更加简洁:
// new Thread(() -> {
// System.out.println(Thread.currentThread().getName() + " 正在执行一个匿名任务。");
// // ... 任务逻辑 ...
// }).start();无论哪种方式,关键都是调用
start()方法。
为什么不直接调用run()方法,而是要用start()?
这个问题其实挺基础,但很多初学者可能都会在这里犯迷糊。说实话,我刚开始学的时候也纳闷过,
run()方法就在那儿,为什么非要绕个弯子去调用
start()呢?
答案是:start()
方法才是真正创建新线程的关键,而run()
方法只是一个普通的方法调用。
当你调用
thread.start()时,JVM(Java虚拟机)会做几件事:
- 它会向操作系统申请创建一个新的线程。
- 这个新线程会被分配它自己的调用栈(call stack)。
- 一旦新线程创建成功并准备就绪,JVM会安排它去执行你
Runnable
或Thread
子类中定义的那个run()
方法。这个run()
方法就会在新创建的线程上独立运行。
但如果你直接调用
thread.run()呢?那它就和调用任何其他普通Java方法没什么两样了。
run()方法会在当前的线程上执行,根本不会创建新的线程。这意味着你的代码仍然是单线程执行的,完全失去了多线程并发的意义。
举个例子,你有一个
MyRunnableTask实例
task。
new Thread(task).start();
:这会启动一个全新的线程,让task.run()
在新线程上跑。task.run();
:这只是在当前线程(比如主线程)上顺序执行task
的run()
方法,没有任何并发可言。
所以,记住,
start()是启动线程的“发令枪”,它告诉JVM“我需要一个新的执行路径”,而
run()仅仅是新路径上要执行的“指令集”。
线程的生命周期是怎样的?有哪些状态?
线程的生命周期,在我看来,就像是一个人的成长过程,从出生到消亡,中间会经历各种状态。理解这些状态对于调试和优化多线程程序至关重要。Java的
Thread.State枚举定义了六种线程状态:
这是一个免费的企业网站系统,任何人可以免费下载、修改和使用本程序,也可以用来为企业建网站。没有任何功能限制,且不发布收费版。容兴免费企业网站系统后台功能简介:1.基本设置:基本信息,联系方式,网站设置,导航管理,模块启闭,静态设置,安全设置,数据库管理2.产品管理:产品列表,添加产品,产品分类3.文章管理:文章列表,发表文章,文章分类,公司简介,网站公告4.客服互动:留言管理,在线客服,友情链接5
-
NEW (新建) 当一个
Thread
对象被创建,但尚未调用start()
方法时,它就处于NEW状态。就像一个人刚出生,但还没开始他的人生旅程。Thread t = new Thread(() -> System.out.println("Hello")); // t 处于 NEW 状态 -
RUNNABLE (可运行/运行中) 当线程调用了
start()
方法后,它就进入了RUNNABLE状态。这意味着线程可能正在JVM中运行,或者正在等待操作系统调度器分配CPU时间片。Java并不区分“可运行”和“正在运行”,都归为RUNNABLE。t.start(); // t 进入 RUNNABLE 状态
-
BLOCKED (阻塞) 当线程试图获取一个内部锁(
synchronized
关键字)但该锁被其他线程持有时,线程会进入BLOCKED状态。它在等待进入一个synchronized
块或方法。// 假设有两个线程同时尝试进入一个同步方法 public synchronized void syncMethod() { // ... } // 如果一个线程在执行syncMethod,另一个线程调用syncMethod就会进入BLOCKED状态 -
WAITING (等待) 线程进入WAITING状态通常是因为调用了以下方法之一:
Object.wait()
(不带超时参数)Thread.join()
(不带超时参数)LockSupport.park()
处于WAITING状态的线程会一直等待,直到被其他线程显式地唤醒(例如通过notify()
、notifyAll()
或join
的线程执行完毕)。
// 线程A调用 obj.wait(); // 线程B调用 obj.notify(); 才能唤醒线程A
-
TIMED_WAITING (定时等待) 与WAITING类似,但它会等待一个指定的时间。如果时间到了,即使没有被其他线程唤醒,线程也会自动回到RUNNABLE状态。进入TIMED_WAITING状态的方法包括:
Thread.sleep(long millis)
Object.wait(long millis)
Thread.join(long millis)
LockSupport.parkNanos(long nanos)
LockSupport.parkUntil(long deadline)
Thread.sleep(1000); // 线程进入 TIMED_WAITING 状态
-
TERMINATED (终止) 当线程的
run()
方法执行完毕,或者因未捕获的异常而退出时,线程就进入了TERMINATED状态。线程一旦进入这个状态,就不能再被重新启动了。// 当 t 的 run() 方法执行完毕后,t 就会进入 TERMINATED 状态
理解这些状态以及它们之间的转换条件,对于诊断多线程程序的性能问题(比如死锁、活锁、线程饥饿)非常关键。在实际开发中,我经常会用JStack或者IDE的调试工具来查看线程的当前状态,这能帮助我快速定位问题。
在多线程编程中,有哪些常见的线程安全问题及解决方案?
多线程编程就像是在一个厨房里,多个厨师(线程)同时操作食材(共享资源)。如果大家各干各的,不注意协作,就很容易出问题。线程安全问题是多线程编程中避不开的坎,我个人觉得,理解这些问题以及对应的解决方案,是写出健壮并发程序的基石。
常见的线程安全问题:
竞态条件 (Race Condition): 这是最常见的问题。当多个线程尝试访问和修改同一个共享资源(比如一个变量、一个集合)时,如果操作的最终结果依赖于这些线程执行的相对时序,就可能发生竞态条件。结果往往是不可预测的、错误的。 例子: 多个线程同时对一个计数器
i++
,i++
不是原子操作,它包含读取、修改、写入三个步骤。如果线程A读取了10,线程B也读取了10,然后各自加1写入11,那么最终结果就不是12,而是11。-
死锁 (Deadlock): 两个或更多的线程被无限期地阻塞,互相等待对方释放资源。这就像两个哲学家,每人拿着一只筷子,都在等对方放下另一只筷子才能吃饭,结果谁也吃不了。 死锁发生的四个必要条件:
- 互斥条件: 资源不能共享,只能被一个线程占用。
- 请求与保持条件: 线程已经持有了至少一个资源,但又请求新的资源,同时不释放已持有的资源。
- 不剥夺条件: 已经分配给一个线程的资源不能被强制性地剥夺。
- 循环等待条件: 存在一个线程资源的循环链,每个线程都在等待链中下一个线程所持有的资源。
活锁 (Livelock): 线程虽然没有被阻塞,但它们却在不断地改变状态以响应其他线程,导致没有任何实际的进展。它比死锁更隐蔽,因为线程看起来是活跃的,但实际上是无效的忙碌。 例子: 两个人过窄桥,同时走到中间,都想给对方让路,于是同时向左,又同时向右,结果谁也过不去。
饥饿 (Starvation): 一个或多个线程由于调度策略不公平,或者优先级太低,或者总是得不到所需的资源,而导致它们永远无法获得CPU时间或资源来执行任务。
解决方案:
针对这些问题,Java提供了丰富的工具和机制:
-
synchronized
关键字: 这是Java内置的同步机制,可以用于方法或代码块。它确保在任何给定时间,只有一个线程可以执行被synchronized
保护的代码。它提供了互斥性和内存可见性。// 同步方法 public synchronized void increment() { count++; } // 同步代码块 public void update() { synchronized (this) { // 或 synchronized (someObject) // 访问共享资源的代码 } }synchronized
用起来很方便,但它是一种“粗粒度”的锁,有时候可能会导致性能瓶颈。 -
java.util.concurrent.locks
包: 提供了更高级、更灵活的锁机制,比如ReentrantLock
、ReadWriteLock
。-
ReentrantLock
: 可重入锁,比synchronized
更灵活,可以尝试获取锁(tryLock()
)、可中断地获取锁(lockInterruptibly()
)、公平锁等。// ReentrantLock lock = new ReentrantLock(); // lock.lock(); // try { // // 访问共享资源 // } finally { // lock.unlock(); // } -
ReadWriteLock
: 读写锁,允许多个读线程同时访问,但写线程是独占的。这对于读多写少的场景性能提升非常明显。
-
-
java.util.concurrent.atomic
包: 提供了原子操作类,如AtomicInteger
、AtomicLong
、AtomicReference
等。这些类利用CAS(Compare-And-Swap)操作实现无锁(Lock-Free)的线程安全。它们比使用锁的性能更高,因为避免了线程上下文切换的开销。// AtomicInteger counter = new AtomicInteger(0); // counter.incrementAndGet(); // 原子地执行 i++
-
volatile
关键字:volatile
主要保证了内存可见性,即一个线程对volatile
变量的修改,对其他线程是立即可见的。它也能防止指令重排序。但它不保证原子性,所以不能替代synchronized
或原子类来解决竞态条件。// public volatile boolean flag = false; // 当一个线程修改 flag 为 true 时,其他线程能立即看到这个变化。
-
并发集合 (Concurrent Collections):
java.util.concurrent
包提供了许多线程安全的集合类,如ConcurrentHashMap
、CopyOnWriteArrayList
、BlockingQueue
等。这些集合内部已经处理了线程安全问题,使用它们通常比手动加锁更高效、更安全。// ConcurrentHashMap
map = new ConcurrentHashMap<>(); // map.put("key", "value"); // 线程安全 不可变对象 (Immutable Objects): 如果一个对象在创建后其状态就不能再被修改,那么它就是线程安全的。例如
String
类就是不可变的。创建不可变对象可以彻底避免共享状态的竞态条件。-
ThreadLocal
: 为每个线程提供一个独立的变量副本。这样每个线程操作的都是自己的副本,互不影响,自然也就没有线程安全问题。// private static ThreadLocal
threadCount = ThreadLocal.withInitial(() -> 0); // threadCount.get(); // 获取当前线程的副本 // threadCount.set(1); // 设置当前线程的副本
在实际开发中,选择哪种解决方案,往往需要根据具体场景、性能要求以及代码的复杂性来权衡。我通常会优先考虑使用并发集合和原子类,如果不行再考虑
ReentrantLock,最后才是
synchronized。避免死锁则需要仔细设计锁的获取顺序,以及尽量减少锁的持有时间。
如何选择实现Runnable接口还是继承Thread类?
这确实是一个老生常谈的问题,但它背后的考量却很实际。我个人的经验是,在绝大多数情况下,实现Runnable
接口是更优的选择。
我们来深入分析一下:
1. 实现Runnable
接口的优势:
-
避免Java单继承的限制: 这是最主要的原因。Java只支持单继承,如果你的类已经继承了另一个父类,那么它就不能再继承
Thread
类了。而实现接口则没有这个限制,你的任务类可以同时继承其他类并实现Runnable
接口。这在复杂的业务场景中非常重要。// class MyBusinessLogic extends SomeBaseClass implements Runnable { ... } // 这种组合在继承Thread时是不可能实现的。 -
任务与执行者解耦: 实现
Runnable
接口意味着你定义的是一个“任务”(What to do),而不是一个“线程”(How to do it)。Thread
类代表的是一个执行者,它封装了线程的创建和管理逻辑。这种解耦让你的代码更清晰,更符合面向对象的设计原则。一个Runnable
实例可以被多个Thread
实例共享,或者被线程池复用。// MyRunnableTask task = new MyRunnableTask(); // new Thread(task).start(); // 一个任务 // new Thread(task).start(); // 另一个线程执行同一个任务实例
-
更适合资源共享: 当多个线程需要处理同一个任务实例中的数据时,实现
Runnable
接口可以方便地将同一个Runnable
实例传递给多个Thread
对象。这样,这些线程就可以共享Runnable
实例的成员变量,从而实现数据共享。// Counter counter = new Counter(); // 共享的计数器实例 // new Thread(new MyRunnable(counter)).start(); // new Thread(new MyRunnable(counter)).start(); // 两个线程操作同一个 counter 对象
-
更好的可测试性: 任务(
Runnable
)是纯粹的业务逻辑,不涉及线程创建和管理,因此更容易进行单元测试。
2. 继承Thread
类的劣势:
- 单继承限制: 这是最大的缺点,前面已经提到了。
-
耦合度高: 任务逻辑与线程本身紧密耦合在一起。如果你想复用任务逻辑,但又不想每次都创建一个新的
Thread
子类实例,就会比较麻烦。 -
不适合线程池: 在使用线程池时,通常是提交
Runnable
(或Callable
)任务,而不是Thread
子类实例。线程池内部会管理Thread
对象,你只需要提供任务。
什么时候可以考虑继承Thread
类?
坦白说,我几乎很少直接继承
Thread类。但如果你的场景非常简单,你的类不需要继承其他任何类,并且这个线程的生命周期和任务逻辑是完全一体的,不需要解耦,那么继承
Thread也并非不可。它可能在某些极度简单的示例代码中看起来更直接。
总结我的选择偏好:
在实际开发中,我几乎总是会选择实现
Runnable接口。它提供了更好的灵活性、可维护性和扩展性。尤其是在现代Java应用中,我们更多地会使用
ExecutorService(线程池)来管理线程,而
ExecutorService通常接受
Runnable或
Callable作为任务。所以,养成实现
Runnable的习惯,会让你的并发编程之路走得更顺畅。









