Java 创建线程的方式
Java 创建线程主要有四种方式:继承 Thread 类、实现 Runnable 接口、使用Callable和Future,以及使用线程池
- 继承Thread类
创建一个类继承java.lang.Thread,重写run()方法定义线程执行任务,然后创建实例调用start()。
java
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 启动线程
}
}
- 实现 Runnable 接口
实现java.lang.Runnable接口,实现run()方法,将该实例作为参数传入Thread对象,再调用start()。
java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
}
}
- 实现 Callable 接口 + FutureTask
使用Callable和Future接口 :创建一个实现了Callable接口的类,并实现其call方法,该方法可以返回结果并抛出异常。使用ExecutorService来管理线程池,并提交Callable任务获取Future对象,以便在未来某个时刻获取Callable任务的计算结果
java
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 模拟耗时操作
Thread.sleep(2000);
return "任务结果";
}
public static void main(String[] args) throws Exception {
// 方式1:使用 FutureTask
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(futureTask);
t1.start();
String result = futureTask.get(); // 阻塞获取结果
System.out.println(result);
// 方式2:配合线程池(更常用)
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
String result2 = future.get();
executor.shutdown();
}
}
- 通过线程池创建
通过使用Executors类创建线程池,并通过线程池来管理线程的创建和复用。
java
public class ThreadPoolDemo {
public static void main(String[] args) {
// 常用静态工厂方法(不推荐在生产环境使用,易导致 OOM)
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
ExecutorService cachedPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 推荐:自定义 ThreadPoolExecutor,更可控
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
5, // maximumPoolSize
60, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 工作队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交 Runnable 任务
executor.execute(() -> System.out.println("Runnable 任务"));
// 提交 Callable 任务
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Callable 结果";
});
executor.shutdown(); // 优雅关闭
}
}
线程中 run() 和 start() 区别
run 方法是线程的执行体,包含线程要执行的代码 ,当直接调用 run 方法时,它会在当前线程的上下文中执行,而不会创建新的线程。
start 方法用于启动一个新的线程,并在新线程中执行 run 方法的代码 。
调用 start 方法会为线程分配系统资源,并将线程置于就绪状态,当调度器选择该线程时,会执行 run 方法中的代码。
关键点 :start() 只能被调用一次,重复调用会抛出 IllegalThreadStateException。
一句话总结:start() 是让操作系统介入创建真正的线程,run() 只是一个普通方法调用。
Java 线程的六个状态:
- NEW (新建):
new Thread ()但没start () - RUNNABLE (就绪 + 运行,Java 合在一起):正在跑,或等着 CPU 调度。调用
start()后 - BLOCKED(阻塞,等锁):等着进 synchronized 同步块 / 方法
- WAITING (无限等待):调用了
wait()、join()、LockSupport.park() - TIMED_WAITING (限时等待):
sleep(xx)、wait(xx)、join(xx)等带超时等待 - TERMINATED(终止 / 结束):执行完 / 异常退出
线程执行时的一般状态:
- 就绪状态
线程已经准备好,就等 CPU 分配时间片。 - 运行状态
获得 CPU,正在执行代码。 - 阻塞状态
主动 / 被动暂停,不参与 CPU 竞争,等条件满足。 - 终止状态
执行完毕或异常退出,生命周期结束。
Java 中 有哪些锁
- 乐观锁 vs. 悲观锁:
悲观锁认为多线程同时修改资源的概率比较高,所以访问资源时候要上锁,确保数据安全。
乐观锁认为多线程同时修改资源的概率比较低,因此不加锁直接操作,乐观的认为,不加锁的并发操作是没有事情的。悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加 锁会带来大量的性能提升。
典型代表悲观锁是synchronized和ReentrantLock;CAS是一种无锁编程技术,体现了乐观锁 - 公平锁 vs. 非公平锁:
公平锁指多个线程按照申请锁的顺序来获取锁,
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优 先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,默认是非公平锁 , 也可以指定为公平锁,对于Synchronized而言,只支持非公平锁。 - 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。(解决了线程在持有锁的情况下再次请求同一锁时的死锁问题 )
对于Java ReentrantLock而言,是可重入锁,对于Synchronized而言,也是一个可重入锁。 - 排他锁 vs. 共享锁:排他锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock、Synchronized而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁 是共享锁,其写锁是独享锁。 - 分段锁:这是一种设计思想(锁粒度的优化),将数据分段,每段独立加锁,用来提升并发度 。典型应用是 JDK 1.7 的
ConcurrentHashMap,它由 16 个 Segment 组成,每个 Segment 独立加锁。 - 自旋锁:让等待锁的线程通过循环重试来获取锁,而不是立即挂起,减少线程上下文切换的开销。在 JVM 内部,轻量级锁竞争时就会用到自旋。
- 偏向锁/轻量级锁/重量级锁:
在 Java 1.6 之前,synchronized是一个纯粹的 重量级锁 ,每次加锁、解锁都需要通过操作系统的互斥量(mutex)实现,涉及 用户态与内核态的切换,开销非常大。但在实际应用中,绝大多数锁的竞争并不激烈,甚至很多同步代码块只被一个线程反复访问。
因此,JVM 引入了 锁升级机制 ,根据竞争程度动态调整锁的状态,在低竞争或无竞争场景下用更轻量的方式处理,减少不必要的开销;在高竞争场景下才退化为重量级锁,保证线程安全。
具体来说:
-
偏向锁 :解决 单线程重复获取锁 的问题。当锁一直被同一个线程获取时,直接记录线程 ID,后续无需任何同步操作,几乎零开销。
-
轻量级锁 :解决 两个线程交替或短暂竞争 的问题。此时通过 自旋(CAS) 尝试获取锁,避免线程阻塞和唤醒带来的上下文切换,适用于锁持有时间短、竞争不激烈的场景。
-
重量级锁 :解决 高并发激烈竞争 的问题。当自旋次数过多或竞争线程数增加时,升级为重量级锁,让未获取锁的线程阻塞,由操作系统调度,避免 CPU 空转,保证系统整体吞吐量。
简单来说,这套机制的设计目标是:让锁在大部分场景下尽可能"轻",只在必要时才变"重",从而在吞吐量、响应时间和资源消耗之间达到最佳平衡。
CAS(Compare And Swap,比较并交换)是一种无锁并发操作 ,它通过一条原子性的 CPU 指令完成"比较并更新"的动作:如果内存中的当前值与预期值相等,则更新为新值;否则不做任何操作。
在 Java 中,CAS 是
java.util.concurrent包的基础,被广泛用于原子类、AQS 等组件中。它体现了乐观锁的思想------不加锁,而是通过尝试和重试来保证线程安全,避免了线程阻塞和上下文切换带来的开销。简单来说:CAS = 原子化的"判断 + 赋值",失败则重试,让线程自己处理冲突,而非被动等待。
synchronized 的理解
synchronized 是 Java 中解决并发问题最基础、最常用的手段 ,它可以保证原子性 、可见性 和有序性。
当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。其他线程必须等待当前线程退出临界区才能进入。确保多个线程在访问共享资源时不会产生冲突
synchronized 可以应用于方法或代码块。当它应用于方法时,整个方法被锁定;当它应用于代码块时,只有该代码块被锁定,这样做的好处是,可以选择性地锁定对象的一部分,而不是整个方法,锁的粒度更细。
在 Java 1.6 之前,synchronized 是一个纯粹的 重量级锁 ,每次加锁、解锁都需要通过操作系统的互斥量(mutex)实现,涉及 用户态与内核态的切换 ,开销非常大。
但在实际应用中,绝大多数锁的竞争并不激烈,甚至很多同步代码块只被一个线程反复访问。
因此,在Java 1.6, JVM 引入了 锁升级机制 ,根据竞争程度动态调整锁的状态,在低竞争或无竞争场景下用更轻量的方式处理,减少不必要的开销;在高竞争场景下才退化为重量级锁,保证线程安全。
具体来说:
-
偏向锁 :解决 单线程重复获取锁 的问题。当锁一直被同一个线程获取时,JVM 直接记录线程 ID,后续无需任何同步操作(加锁),几乎零开销。
-
轻量级锁 :解决 两个线程交替或短暂竞争 的问题。此时通过 自旋(CAS) 尝试获取锁,避免线程阻塞和唤醒带来的上下文切换,适用于锁持有时间短、竞争不激烈的场景。
-
重量级锁 :解决 高并发激烈竞争 的问题。当自旋次数过多或竞争线程数增加时,升级为重量级锁,让未获取锁的线程阻塞,由操作系统调度,避免 CPU 空转,保证系统整体吞吐量。
当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。
最后,尽管 Java 实现的锁机制有很多种,并且有些锁机制性能也比 synchronized 高,但还是强烈推荐在 多线程应用程序中使用该关键字,因为实现方便,后续工作由 JVM 来完成,可靠性高。只有在确定锁机 制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如 ReentrantLock 等。
原子性 :被
synchronized修饰的代码块或方法,同一时刻只能有一个线程执行,相当于一个不可分割的操作。防止了中间状态被其他线程看到 ,从而避免数据不一致。可见性 :线程解锁前,必须把共享变量的最新值刷新到主内存;线程加锁时,会清空工作内存中该变量的值,从而强制从主内存重新读取。这一机制保证了前一个线程的写结果对后一个线程立即可见。保证了跨线程的写结果能及时传播 ,让程序行为符合预期。
有序性 :虽然编译器和处理器可能会对指令进行重排序,但
synchronized修饰的代码块内部,在同一时刻只允许一个线程执行,所以不会出现指令重排导致的其他线程看到中间状态的问题。避免因重排序导致的意想不到的结果。
synchronized 和 lock 的区别是什么?
从功能来看:
lock和synchronized都是java中解决线程安全问题的一个工具
从使用来看:
-
synchronized 是 Java 内置的关键字,基于 JVM 层面的内置锁实现。锁的获取与释放由 JVM 自动管理;,无需开发者手动干预。
-
Lock 是 juc (
java.util.concurrent.locks) 包下的接口 ,典型的实现如ReentrantLock。Lock比Synchronized的灵活性更高,Lock可以自主决定什么时候加锁,什么时候释放锁,锁的获取和释放需要开发者在代码中显式调用lock()和unlock()方法。
从锁粒度来看:
synchronized可以通过两种方式控制锁的粒度:
1.把synchronized关键字修饰在方法层面
2.修饰在同步代码块上
并且我们可以通过synchronized加锁对象的生命周期,来控制锁的作用范围,比如锁对象是静态对象,或者类对象,那么这个锁就是属于全局锁,如果锁对象是实例对象,那么这个锁的范围取决于这个对象的生命周期。
lock 中锁的粒度是通过它里面提供的 lock() 方法 和 unlock() 方法来决定的,包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于Lock实例的生命周期。
功能方面
竞争锁:
Lock还提供了非阻塞的竞争锁方法 tryLock()方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁。
Synchronized由于是关键字,所以它无法实现非阻塞竞争锁的方法,synchronized拿不到锁就一直阻塞等待,另外Synchronized锁的释放是被动的,就是当Synchronized同步代码块执行完以后或者代码出现异常时才会释放。
公平锁和非公平锁:
Lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized只提供了一种非公平锁的实现。
性能方面
synchronized在性能方面和lock相差不大,在实现上会有一个区别synchronized引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock则用到了自旋锁的方式实现性能优化。
使用场景
总结来说,synchronized使用简单,适合锁的粒度较小、竞争不激烈、实现简单的场景。而Lock提供了更多的灵活性和控制能力,适用于需要更复杂同步控制的场景。
synchronized 和 ReentranLock的区别是什么?
核心区别:
synchronized是Java 内置的关键字,锁的获取与释放由 JVM 自动管理;ReentrantLock是 juc 包(java.util.concurrent.locks)包中Lock接口的一个实现类,需要显式创建,ReentrantLock比synchronized的灵活性更高,并通过调用lock()和unlock()方法来管理锁的获取和释放。(因为是一个类,所以提供的功能更多)
底层实现区别:
- 实现原理是不一样,ReentrantLock 基于 AQS 实现的,synchronized 是基于 ObjectMonitor
效率区别:
synchronized在 JVM 层面引入了锁升级(偏向锁 → 轻量级锁 → 重量级锁),一旦升级为重量级锁后不会降级。低竞争或无竞争场景下,偏向锁和轻量级锁的开销很小,性能往往优于ReentrantLock。ReentrantLock没有锁升级机制, 在高竞争场景下,ReentrantLock的调度策略更加灵活(功能更多),性能可能略优于synchronized的重量级锁
功能向的区别:
- ReentrantLock 的功能比 synchronized 更全面。
- ReentrantLock 支持公平锁和非公平锁(默认非公平锁),synchronized 只支持非公平锁
- ReentrantLock 提供了非阻塞的竞争锁方法 tryLock() 方法,尝试获取锁并立即返回 true/false,不会阻塞线程,也可以指定时间内尝试拿锁;而 synchronized 若获取锁失败则只能阻塞等待。
使用场景:
总结来说,synchronized适合简单的同步需求,而ReentrantLock提供了更丰富的控制能力和灵活性,适用于需要复杂同步控制的场景。
ReentrantLock 提供了更灵活的锁获取方式:
tryLock():非阻塞获取锁,尝试后立即返回true(获取成功)或false(获取失败),线程不会阻塞等待。
tryLock(long time, TimeUnit unit):支持超时等待,在指定时间内尝试获取锁,若超时仍未获取则返回false,同时该方法支持响应中断(需处理InterruptedException)。而 synchronized 在获取锁失败时,线程会一直阻塞,直到锁被释放或被中断(但 synchronized 等待锁时无法响应中断)。
synchronized:基于ObjectMonitor,是 JVM 内部机制。锁状态存储在对象头,通过monitorenter/monitorexit字节码实现,自动释放锁,支持锁升级(偏向→轻量→重量)。
ReentrantLock:基于AbstractQueuedSynchronizer(AQS) 抽象类 ,是 JDK 提供的 Java 层框架。通过volatile int state+ CLH 队列 管理锁竞争,显式lock()/unlock(),支持非阻塞tryLock()、可中断、公平/非公平等灵活策略。本质 :一个是 JVM 内置 (关键字),一个是 API 实现(类)。
volatile 关键字的作用有哪些?
volatile 和 synchronized 对比
线程池
BIO、NIO、AIO
BIO、AIO和NIO是Java中不同的I/O模型,它们在处理输入输出操作时有不同的特点。
-
BIO:阻塞式的I/O模型。当一个线程执行I/O操作时,如果数据还没准备好,这个线程会被阻塞,直到数据到达。适合连接数较少且固定的场景,但扩展性较差。
-
NIO:非阻塞的I/O模型。NIO使用缓冲区和通道来处理数据,提高了I/O操作的效率。支持面向缓冲区的读写操作,可以处理大量并发的连接。
-
AIO:异步I/O模型,从Java 7开始引入。在AIO中,I/O操作被发起后,线程可以继续执行其他任务,一旦I/O操作完成,操作系统会通知线程。适合需要处理大量并发I/O操作,且希望避免I/O操作阻塞线程的场景。
- 异步:IO操作会在后台线程完成,主线程不需要等待。
- 非阻塞:主线程不会被IO操作阻塞,可以自由执行其他任务。
- 回调机制:AIO通过回调函数来处理IO操作完成后的事件。
-
使用场景:
- BIO 适合低并发、连接数较少的应用。
- NIO 适合高并发、需要处理大量连接的应用。
- AIO 适合需要高性能、异步处理I/O操作的场景。