可以创建一个继承自Thread类的子类,并重写其run()方法来定义线程的行为。然后可以通过创建该子类的实例来启动线程。
示例代码:
class MyThread extends Thread {
public void run() {
// 定义线程的行为
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
可以创建一个实现了Runnable接口的类,并实现其run()方法。然后可以通过创建该类的实例,并将其作为参数传递给Thread类的构造函数来创建线程。
示例代码:
class MyRunnable implements Runnable {
public void run() {
// 定义线程的行为
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start(); // 启动线程
}
}
Callable接口类似于Runnable接口,但是它可以返回执行结果,并且可以抛出异常。使用Callable需要通过ExecutorService的submit()方法来提交任务,并返回一个Future对象,可以通过该对象获取任务的执行结果。
示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
// 定义线程的行为
return 42;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start(); // 启动线程
Integer result = futureTask.get(); // 获取执行结果
System.out.println("结果:" + result);
}
}
可以直接使用匿名类来创建线程,可以是继承Thread类或实现Runnable接口的匿名类。
示例代码:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
public void run() {
// 定义线程的行为
}
};
thread.start(); // 启动线程
}
}
Java提供了Executor框架来管理线程池,通过线程池可以更好地管理和复用线程资源。可以通过Executors类提供的静态方法创建不同类型的线程池,然后提交任务给线程池执行。
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
public void run() {
// 定义线程的行为
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池
for (int i = 0; i < 10; i++) {
Runnable task = new MyTask();
executor.submit(task); // 提交任务给线程池执行
}
executor.shutdown(); // 关闭线程池
}
}
Runnable
和 Callable
接口都用于表示可以在新线程中执行的任务,但它们之间有一些区别:
返回值类型:
Runnable
的 run()
方法没有返回值,因此线程执行完任务后不会有返回结果。Callable
的 call()
方法可以有返回值,它使用泛型来指定返回类型,可以在执行完任务后返回计算结果。Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。异常抛出:
Runnable
的 run()
方法不能抛出已检查异常,只能捕获处理未检查异常(RuntimeException)。Callable
的 call()
方法可以抛出异常,它允许抛出任何类型的异常,包括已检查异常。多线程执行:
Runnable
通过将其实例作为参数传递给 Thread
对象,在新线程中执行。Callable
通常与 ExecutorService
结合使用,通过 submit(Callable)
方法提交任务并异步执行,在获得返回结果时可以通过 Future
对象获取。总的来说,Runnable
是较为简单的表示任务的接口,适合不需要返回结果或处理异常的任务;而 Callable
则更灵活,可以返回结果和处理异常,适合需要获取任务执行结果和可能抛出异常的情况。
线程的状态可以参考JDK中的Thread类中的枚举State,分为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。
状态之间的变化如下图:
在Java中,线程的启动有两种方式,一种是调用线程对象的start()方法,另一种是直接调用线程对象的run()方法。它们的区别如下:
start()方法:
run()方法:
因此,start()方法是启动一个新的线程并执行其中的run()方法,而run()方法是在当前线程中执行run()方法的代码,没有并发执行的效果。通常情况下,我们应该使用start()方法来启动线程,以实现多线程并发执行的效果。
线程同步:
线程调度:
notify()方法用于唤醒处于等待状态的单个线程,该线程是由对象的monitor所保护的。当调用notify()方法时,系统会在等待该对象monitor的线程中选择一个线程进行唤醒,但具体唤醒哪个线程是无法确定的。
notifyAll()方法用于唤醒所有处于等待状态的线程,这些线程是由对象的monitor所保护的。调用notifyAll()方法会唤醒所有等待该对象monitor的线程,让它们都有机会争取获取对象锁。
因此,notify()和notifyAll()的主要区别在于,notify()只能唤醒单个线程,而notifyAll()可以唤醒所有等待线程。在使用notify()时需要确保只有一个线程真正需要被唤醒,而在使用notifyAll()时则更适合一次性唤醒所有等待线程的情况。
共同点
不同点
当需要确保多个线程按照特定顺序执行时,可以使用join()方法来控制线程的执行顺序。例如,假设有三个线程t1、t2和t3,需要按照t3调用t2,t2调用t1的顺序执行。可以如下实现:
public class JoinExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1");
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // 等待t1执行完毕
System.out.println("t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
t2.join(); // 等待t2执行完毕
System.out.println("t3");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
线程t3调用t2的join(),确保t2会在t3执行前完成;线程t2调用t1的join(),确保t1会在t2执行前完成。这样,就能保证线程按照特定的顺序执行。
使用线程池的优势主要体现在以下几个方面:
节约系统资源:线程的创建和销毁是资源密集型的操作,而线程池可以重用已经存在的线程,避免重复创建和销毁,从而节约系统资源消耗。
提高响应速度:线程池可以使任务在有空闲线程时就立即执行,而不需要等待线程创建完成,从而提高任务的响应速度。
控制并发数量:线程池可以控制最大并发执行的线程数量,避免因线程数过多导致系统负载过重,从而提高系统的稳定性和性能。
增强可管理性:线程池统一管理线程的创建、销毁和调度,能够更好地对线程进行分配、调优和监控,从而提高线程的可管理性。
综合来看,使用线程池可以有效地降低系统资源消耗,提高任务响应速度,控制并发数量,并增强线程的可管理性。
Excutors 可以创建线程池的常见4 种方式:
ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
PriorityBlockingQueue:具有优先级别的阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
threadFactory:用于设置创建线程的工厂。ThreadFactory的作用就是提供创建线程的功能的线 程工厂。他是通过newThread()方法提供创建线程的功能,newThread()方法创建 的线程都是“非守护线程”而且“线程优先级都是默认优先级”。
handler RejectedExecutionHandler,线程池的拒绝策略。所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。当向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务。
线程池提供了四种拒绝策略:
AbortPolicy: 直接抛出异常,阻止系统正常运行。
CallerRunsPolicy: 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
DiscardOldestPolicy: 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
DiscardPolicy: 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案
虽然使用Executors创建线程池非常方便,但也确实存在一些不建议使用的原因。以下是一些主要的原因:
固定大小的线程池:Executors提供了一种固定大小的线程池,使用newFixedThreadPool
方法可以创建该类型的线程池。然而,固定大小的线程池可能导致资源的浪费,尤其是在不需要这么多线程来处理任务的情况下。
单线程的线程池:同样,Executors也提供了newSingleThreadExecutor
方法来创建一个单线程的线程池。但是,如果这个线程在工作过程中发生异常而终止,那么会创建一个新的线程来替代它,这可能会导致问题难以追踪。
无界队列:Executors提供的线程池通常使用无界的任务队列(如LinkedBlockingQueue
),如果任务的提交速度大于任务处理的速度,就会导致队列中积压大量的任务,最终可能耗尽系统资源。
默认的拒绝策略:Executors创建的线程池通常采用默认的拒绝策略(抛出异常),这可能会导致任务丢失或者系统不稳定。
因此,我们建议在实际开发中,应该根据具体的需求来手动创建ThreadPoolExecutor,以便更好地控制线程池的大小、队列的大小、拒绝策略以及线程工厂等参数。这样可以根据实际情况来优化线程池的性能,以及更好地处理任务。
在线程池中,常用的队列包括:
ArrayBlockingQueue:基于数组实现的有界阻塞队列。在使用线程池时,可以通过指定队列的大小来控制线程池的最大承载能力,防止资源耗尽的问题。
LinkedBlockingQueue:基于链表实现的无界阻塞队列。由于其容量理论上可以无限增长,因此在使用此队列时需要特别注意,当任务提交速度过快,可能会导致队列无限增长,最终消耗完系统资源。
PriorityBlockingQueue:基于最小二叉堆实现的优先级队列,属于无界阻塞队列。该队列会根据元素的优先级进行排序,但同样也存在无限增长的潜在问题。
DelayQueue:该队列用于延迟任务的执行,只有当指定的延迟时间到了,才能从队列中获取到该元素。这也是一个无界队列,需要小心使用以避免资源耗尽问题。
SynchronousQueue:不存储元素的阻塞队列,在队列中放入一个元素后必须等待另一个线程取出该元素,因此实际上不会存储元素,而是在传递元素。在某些情况下可以使用此队列实现线程间的同步。
总的来说,虽然无界队列可以在某些情况下提供灵活性,但为了防止资源耗尽等问题的发生,通常建议在使用线程池时选择有界队列,以限制线程池的最大承载能力。
你提到的这些区别都很准确:
参数类型不同:execute方法只能接受Runnable对象,而submit方法既可以接受Runnable对象,也可以接受Callable对象。
返回值不同:submit方法会返回一个Future对象,通过这个Future对象可以获取到任务执行的结果或者等待任务执行完成。execute方法没有返回值,因此无法获得任务执行的结果。
异常处理:通过使用submit方法,可以方便地控制和处理任务的异常。通过Future的get方法可以捕获线程中的异常。而execute方法在主线程无法捕获任务执行过程中的异常。
总的来说,如果需要获取任务执行的结果,或者需要控制任务的异常,使用submit方法更为合适。而如果不需要知道任务执行的结果,也不需要捕获任务执行过程中的异常,那就可以使用execute方法。
确定核心线程数时需要考虑业务类型以及系统的CPU和IO情况。以下是一些常见的指导原则:
CPU 密集型任务:
IO 密集型任务:
综合考虑:
在实际应用中,最佳的线程池配置可能需要根据具体的业务场景和系统负载进行调整和优化。可以通过监控系统的运行情况和性能指标,不断调整线程池参数来达到最优的配置。
线程死锁是指多个线程因竞争资源或相互等待对方释放资源而陷入的一种阻塞状态。具体来说,线程死锁通常发生在多个线程同时持有该资源,但又互相需要对方所持有的资源的情况下。这种情况下,每个线程都在等待对方释放资源,导致所有线程都无法继续执行,从而形成了死锁状态。
举例来说,线程A持有资源X并等待资源Y,而线程B持有资源Y并等待资源X。此时如果没有外部干预来打破这种相互等待的情况,那么线程A和线程B都无法继续执行,形成了死锁。
线程死锁是多线程编程中常见的问题,解决方法包括合理设计资源获取顺序、使用超时等待机制、以及避免持有过多的资源等。要避免和解决线程死锁问题需要深入理解并合理管理多线程中的资源竞争和同步问题。
避免循环等待条件:设计时避免形成头尾相接的资源循环等待关系,可以按照特定顺序请求资源,避免交叉依赖。
资源分配的统一性:尽量使用统一的资源分配方法,避免任务对资源的获取顺序做出假设,降低死锁的可能性。
引入超时机制:在获取资源时引入超时等待,如果等待超时则放弃当前资源请求,避免长时间等待。
死锁检测与恢复:定期检测系统中是否存在死锁,如果检测到死锁,可以采取一定的措施打破死锁,比如中断某些进程或回滚操作。
合理的资源释放:及时释放不再需要的资源,避免长时间占有资源。
总之,避免线程死锁需要在系统设计和实现中综合考虑多种因素,包括资源分配策略、等待超时机制等。
Synchronized的实现原理是基于对象的锁机制,当一个线程进入一个被synchronized修饰的代码块或方法时,它会尝试获取这个对象的锁。如果该对象的锁已经被其他线程获取,那么这个线程就会被阻塞,直到锁被释放。一旦获取了对象的锁,线程就可以执行代码块或方法,执行完后释放锁,其他线程才能再次获取锁。
Synchronized的作用范围可以是代码块或方法,也可以是对象或类。在代码块或方法上加synchronized关键字时,意味着对当前对象的同步,只有持有该对象锁的线程才能执行该代码块或方法。而在对象或类上加synchronized关键字时,意味着对该对象或类的所有实例或静态成员的同步,只有持有该对象或类锁的线程才能执行相关代码块或方法。
在下面代码中,increment方法被synchronized修饰,意味着只有一个线程能够同时调用这个方法,并且会保证对count的操作是线程安全的。
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
CAS是Compare And Swap(比较再交换)的缩写。它是一种现代CPU广泛支持的指令,用于对内存中的共享数据进行操作。CAS可以将read-modify-write操作转换为原子操作,且由CPU直接保证原子性。
CAS操作有三个操作数:内存值V,旧的预期值A,要修改的新值B。当且仅当旧预期值A和内存值V相同时,将内存值V修改为B并返回true,否则什么都不做,并返回false。
例如,在多线程环境下,线程1和线程2操作同一个内存变量a:
CAS的重要应用之一是在多线程环境下保证对共享数据的原子操作,避免了传统锁机制的开销和复杂性。在并发编程中,CAS有广泛的应用。Java中的Atomic包就是基于CAS实现的原子操作类。
Java内存模型 (Java Memory Model, JMM) 是定义了 Java 程序中各种变量(包括线程共享变量)的访问规则以及变量存储与读取细节的一种模型。绝大多数现代编程语言都有类似的内存模型。
JMM 的特点包括:
Java内存模型通过定义这些规则,确保了多线程环境下对共享变量的正确访问。同时,开发人员需要遵守这些规则来确保程序的正确性和可靠性。
说得更简单一点,Java内存模型定义了多线程环境下内存的工作方式,以及线程如何访问和交互共享变量。这有助于确保程序的正确性和可靠性。
synchronized、volatile、和lock是Java 中用于多线程编程的关键字和工具,它们在实现线程同步和数据可见性方面起着不同的作用。
synchronized:synchronized 是 Java 中的关键字,可以用于方法或代码块上。当 synchronized 用于方法时,它锁住的是整个方法,当用于代码块时,它锁住的是括号内的对象。synchronized 提供了互斥锁,确保同时只有一个线程可以访问同步代码块或方法,从而避免多个线程同时访问共享资源。synchronized 是基于对象锁的,每个对象都有一个与之相关联的锁。在多线程环境下,使用 synchronized 可以确保线程之间的安全访问共享变量。
volatile:volatile 是 Java 中的关键字,用于声明变量时指示编译器不要执行任何针对这个变量的优化,确保多线程访问时变量的可见性。被 volatile 修饰的变量在每次被线程读取时都会从主内存中重新获取最新的值,因此保证了线程之间对变量修改的可见性。但是 volatile 不能保证原子性,也就是不能保证多个线程同时修改变量时的线程安全性。
Lock:Lock 是 Java 中的一个接口,用于替代 synchronized 线程锁,它有很多实现类,如 ReentrantLock、Condition 等。相比 synchronized,Lock 提供了更丰富的同步操作,例如可以实现公平锁、可重入锁、尝试获取锁、定时获取锁等功能。与 synchronized 不同,Lock 是显示锁,需要手动加锁和释放锁,因此更加灵活。但同时使用 Lock 也需要开发者自行确保在获取锁后及时释放锁,以避免死锁等问题。
综上所述,synchronized 提供了隐式锁机制,使用方便,但功能相对较为简单;volatile 提供了变量的可见性,但不能保证线程安全性;而 Lock 提供了更加强大、灵活的锁机制,可以实现更多复杂的同步需求。在实际开发中,应根据具体的业务需求和线程安全问题选用适当的方式实现线程同步和数据可见性。
特性 | synchronized | volatile | Lock |
---|---|---|---|
使用方式 | 关键字 | 关键字 | 接口 |
锁类型 | 隐式锁 | N/A | 显式锁 |
锁范围 | 方法或代码块 | 变量 | 方法 |
锁特性 | 提供互斥锁,确保线程安全 | 保证线程之间变量改动的可见性 | 提供了更丰富的同步操作,可实现更多复杂的需求 |
可重入性 | 可重入 | N/A | 可重入 |
锁释放 | 自动释放 | N/A | 手动释放 |
功能 | 实现简单,使用方便 | 提供可见性,不能保证线程安全性 | 提供更加灵活的锁机制,可实现更多复杂的需求 |
在Java中,锁是多线程编程中常用的一种同步机制。以下是几种常见的锁和其特点:
悲观锁和乐观锁
自旋锁
读写锁
可重入锁和非可重入锁
ThreadLocal 的作用是实现资源对象的线程隔离,让每个线程都可以拥有自己的资源对象,避免了线程安全问题,并且同时实现了资源对象在线程内的共享。
其原理是通过 ThreadLocalMap 在每个线程内存储资源对象,并使用 ThreadLocal 自身作为 key 来获取和移除对应的资源值。
ThreadLocalMap 中的 key 被设计为弱引用,以便在内存不足时能释放其占用的内存,同时使用启发式扫描来清除临近的 null key 的 value 内存。
推荐使用 ThreadLocal,并把它作为静态变量来使用,因为无法被动依靠 GC 回收。同时,在实际使用中要注意主动移除 key,value,来释放相应的内存。
CyclicBarrier和CountDownLatch是两个在java.util.concurrent包下的类,都用于表示代码运行到某个点上的情况,但它们有以下区别:
CyclicBarrier: 当某个线程运行到某个点后,该线程停止运行,直到所有线程都到达这个点,然后所有线程才会重新运行;CountDownLatch: 当线程运行到某个点后,只是将某个数值-1,然后该线程继续运行。
CyclicBarrier只能唤起一个任务,而CountDownLatch可以唤起多个任务。
CyclicBarrier是可重用的,而CountDownLatch在计数值减到0之后就不可再使用了。
方面 | CyclicBarrier | CountDownLatch |
---|---|---|
激活 | 所有线程必须到达屏障点 | 单个或多个线程到达倒计数点 |
可重用性 | 在屏障被打破后可重复使用 | 当计数值减到0后不可再使用 |
用途 | 适用于线程之间的同步 | 适用于依赖其他任务完成的任务 |
任务数量 | 适用于固定数量的任务 | 适用于动态数量的任务 |