程序、进程与线程的关系
进程是程序运行后在内存中的实例
当一个程序执行进入内存运行时,即变成一个进程
进程的资源是彼此隔离的,其他进程不允许访问
线程是进程内执行的“任务”
线程是进程内的一个“基本任务”,每个线程都有自己的功能是CPU分配与调度的基本单位
一个进程内可以包含多个线程,反之一个线程只能隶属于某一个进程
进程内至少拥有一个
线程
,这个线程叫主线程
,主线程消亡则进程结束
CPU、进程、线程的关系
单核心上的多线程为并发执行,因为你感觉它是同时在运行,其实是由时间片控制的不同时间执行,但由于时间极短让你以为是同一时间执行的
多核心上的多线程为并行执行,多核心从物理上使用多线程可以真正的同时执行,所以是并行的
Java中进程与线程
通常所说的java单线程程序指的是只有一个主线程的程序(忽略了垃圾收集线程),而java多线程是指在代码中所创建出来的多个线程。
创建多线程的三种方式
继承Thread类创建线程
package cn.chengaofeng;
import java.util.Random;
import org.junit.Test;
public class ThreadTest {
class Runner extends Thread {
@Override
public void run() {
Integer speed = new Random().nextInt(10);
for (int i = 0; i < 10; i++) {
System.out
.println("第" + (i + 1) + "秒:" + this.getName() + "已经跑了" + (i * speed) + "米(" + speed + "米/秒)");
}
}
}
public void start() {
Runner r1 = new Runner();
r1.setName("兔子");
Runner r2 = new Runner();
r2.setName("乌龟");
Runner r3 = new Runner();
r3.setName("猪");
r1.start();
r2.start();
r3.start();
}
@Test
public void createThread() {
// Thread t = new Thread(() -> {
// System.out.println("Hello, world!");
// });
// t.start();
System.out.println("开始比赛");
new ThreadTest().start();
// 确保主线程等待子线程完成
try {
Thread.sleep(11000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
实现Runnable接口创建线程
package cn.chengaofeng;
import org.junit.Test;
public class RunnableTest {
class Runner implements Runnable {
@Override
public void run() {
Integer speed = new java.util.Random().nextInt(10);
for (int i = 0; i < 10; i++) {
System.out.println("第" + (i + 1) + "秒:" + Thread.currentThread().getName() + "已经跑了" + (i * speed) + "米("
+ speed + "米/秒)");
}
}
}
public void start() {
Runner r1 = new Runner();
Runner r2 = new Runner();
Runner r3 = new Runner();
Thread t = new Thread(r1);
t.setName("兔子");
t.start();
Thread t2 = new Thread(r2);
t2.setName("乌龟");
t2.start();
Thread t3 = new Thread(r3);
t3.setName("猪");
t3.start();
}
@Test
public void run() {
System.out.println("开始比赛");
new RunnableTest().start();
try {
Thread.sleep(11000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
Callable接口创建线程
package cn.chengaofeng;
import org.junit.Test;
public class CallableTest {
class Runner implements java.util.concurrent.Callable<Integer> {
public String name;
@Override
public Integer call() throws Exception {
Integer speed = new java.util.Random().nextInt(10);
for (int i = 0; i < 10; i++) {
System.out.println("第" + (i + 1) + "秒:" + this.name + "已经跑了" + (i * speed) + "米("
+ speed + "米/秒)");
}
return speed;
}
}
public void start() {
Runner r1 = new Runner();
r1.name = "兔子";
Runner r2 = new Runner();
r2.name = "乌龟";
Runner r3 = new Runner();
r3.name = "猪";
// 线程池
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(3);
executor.submit(r1);
executor.submit(r2);
executor.submit(r3);
executor.shutdown();
}
@Test
public void run() {
System.out.println("开始比赛");
new CallableTest().start();
try {
Thread.sleep(11000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
小结
继承Thread,Java对继承不友好,不推荐使用
实现Runnable接口,Java编程友好,但无法返回执行后数据
实现Callable接口,可以返回多线程执行结果,编程稍显复杂
线程同步
代码中的同步机制
synchronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁,在多线程并发访问的时候,同时只允许一个线程可以获得这个锁,执行特定的代码。执行后释放锁,继续由其他线程争抢。
package cn.chengaofeng;
import org.junit.Test;
public class SyncTest {
class Printer {
public void print() {
synchronized (this) {
try {
/*
* 在同步上下文中调用 Thread.sleep
* 可能会导致性能问题,因为它会阻塞持有锁的线程,从而阻止其他线程访问该锁。这可能会导致线程饥饿和性能下降。
* 为了避免这种情况,可以将 Thread.sleep 移出同步块。此处仅作演示用途,实际应用中应该避免在同步上下文中调用 Thread.sleep。
*/
Thread.sleep(500);
System.out.print("我");
Thread.sleep(500);
System.out.print("是");
Thread.sleep(500);
System.out.print("中");
Thread.sleep(500);
System.out.print("国");
Thread.sleep(500);
System.out.print("人");
Thread.sleep(500);
System.out.println();
} catch (InterruptedException e) {
}
}
}
}
@Test
public void start() {
Printer printer = new Printer();
Thread t1 = new Thread(() -> {
printer.print();
});
Thread t2 = new Thread(() -> {
printer.print();
});
t1.start();
t2.start();
try {
Thread.sleep(13000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
synchronized的锁对象三种不同使用方式
synchronized代码块 - 任意对象即可
synchronized方法 - this当前对象
package cn.chengaofeng;
import org.junit.Test;
public class SyncTest {
class Printer {
public synchronized void print() {
try {
/*
* 在同步上下文中调用 Thread.sleep
* 可能会导致性能问题,因为它会阻塞持有锁的线程,从而阻止其他线程访问该锁。这可能会导致线程饥饿和性能下降。
* 为了避免这种情况,可以将 Thread.sleep 移出同步块。此处仅作演示用途,实际应用中应该避免在同步上下文中调用 Thread.sleep。
*/
Thread.sleep(500);
System.out.print("我");
Thread.sleep(500);
System.out.print("是");
Thread.sleep(500);
System.out.print("中");
Thread.sleep(500);
System.out.print("国");
Thread.sleep(500);
System.out.print("人");
Thread.sleep(500);
System.out.println();
} catch (InterruptedException e) {
}
}
}
@Test
public void start() {
Printer printer = new Printer();
Thread t1 = new Thread(() -> {
printer.print();
});
Thread t2 = new Thread(() -> {
printer.print();
});
t1.start();
t2.start();
try {
Thread.sleep(13000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
synchronized静态方法 - 该类的字节码对象
package cn.chengaofeng;
import org.junit.Test;
public class SyncTest {
class Printer {
public static synchronized void print() {
try {
/*
* 在同步上下文中调用 Thread.sleep
* 可能会导致性能问题,因为它会阻塞持有锁的线程,从而阻止其他线程访问该锁。这可能会导致线程饥饿和性能下降。
* 为了避免这种情况,可以将 Thread.sleep 移出同步块。此处仅作演示用途,实际应用中应该避免在同步上下文中调用 Thread.sleep。
*/
Thread.sleep(500);
System.out.print("我");
Thread.sleep(500);
System.out.print("是");
Thread.sleep(500);
System.out.print("中");
Thread.sleep(500);
System.out.print("国");
Thread.sleep(500);
System.out.print("人");
Thread.sleep(500);
System.out.println();
} catch (InterruptedException e) {
}
}
}
@Test
public void start() {
Thread t1 = new Thread(() -> {
Printer.print();
});
Thread t2 = new Thread(() -> {
Printer.print();
});
t1.start();
t2.start();
try {
Thread.sleep(13000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
线索安全
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
为什么要保证线程的安全?
保证线程安全是多线程编程中的一个重要概念,它涉及到在多个线程并发执行时,确保程序行为的正确性和一致性。以下是为什么要保证线程安全的几个关键原因:
数据一致性:当多个线程同时访问和修改同一个数据时,如果没有适当的同步措施,可能会导致数据不一致。线程安全确保所有线程看到的数据是准确和最新的。
防止竞态条件:竞态条件是指程序的输出依赖于线程的执行顺序,这通常是不可预测的。保证线程安全可以防止由于竞态条件导致的错误和不确定的行为。
避免死锁:死锁发生在两个或多个线程相互等待对方持有的资源,导致程序停滞。线程安全策略包括避免死锁的设计模式和编程实践。
提高性能:适当的线程安全措施可以提高程序的性能,例如通过减少锁的竞争和优化资源访问。
确保程序的可靠性:线程安全问题可能导致程序崩溃或产生不可预测的行为,这对于任何软件系统都是不可接受的。保证线程安全有助于提高程序的稳定性和可靠性。
简化调试和维护:线程安全的问题往往难以重现和调试。编写线程安全的代码可以减少调试的复杂性,并简化代码的维护。
满足业务需求:许多业务逻辑要求在并发环境下保持数据的完整性和准确性,线程安全是满足这些需求的基础。
避免资源泄露:不当的线程管理可能导致资源泄露,例如线程在执行长时间运行的任务时未能正确释放资源。线程安全措施包括确保资源在线程结束时被适当清理。
提高用户体验:对于用户交互的应用程序,线程安全确保用户操作的响应性和一致性,从而提供更好的用户体验。
遵守设计原则:线程安全是许多设计原则和编程范式的直接结果,例如封装和抽象,它们旨在创建易于理解和维护的代码。
保证线程安全对于开发可靠、高效和可维护的多线程应用程序至关重要。
线程池
java.util.concurrent(JUC)
关发是伴随着多核处理器的诞生而产生的,为了充分利用硬件资源,诞生了多线程技术。但是多线程又存在资源竞争的问题,引发了同步和互斥的问题,java.util.concurrent
(并发工具包)来解决这些问题。
Runnable接口的弊端
Runnable新建线程,性能差
线程缺乏统一管理,可能无限制的新建线程,相互竞争,严重时会占用过多系统资源导致死机或内存溢出
线程复用的理念
总线程量确定,空闲时执行不同的任务
ThreadPool线程池
重用存在的线程,减少线程对象创建、消亡的开销
线程总数可控,提高资源的利用率
提供额外功能,定时执行、定期执行、监控等
JUC支持的线程池种类
在java.util.concurrent
中,提供了工具类Executors
(调度器)对象来创建线程池,可创建的线程池有四种:
FixedThreadPool - 定长线程池:指定当前最大线程数量
package cn.chengaofeng.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Test;
public class FixedThreadPoolTest {
// 定长线程池
// 适用于负载较重的服务器,为了资源的合理利用,需要限制当前线程数量
@Test
public void run() {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int index = i;
// 面向对象编程
// executor.execute(new Runnable() {
// @Override
// public void run() {
// System.out.println("面向对象" + Thread.currentThread().getName() + " is running"
// + index);
// }
// });
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("面向对象" + Thread.currentThread().getName() + " is running"
+ index);
}
});
// 函数式编程
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is running" + index);
});
}
executor.shutdown();
}
}
CachedThreadPool - 可缓存线程池:无限大,如果线程池中没有可用的线程则创建,有空闲线程则利用起来
ExecutorService executor = Executors.newCachedThreadPool();
SingleThreadExecutor - 单线程池:只有一个线程,排队执行
ExecutorService executor = Executors.newSingleThreadExecutor();
ScheduledThreadPool - 调度线程池:可指定时间,去定时执行
package cn.chengaofeng.pool;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
public class ScheduledThreadPoolTest {
// 定长线程池
// 适用于负载较重的服务器,为了资源的合理利用,需要限制当前线程数量
@Test
public void run() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getName() + " is running " + new Date() + "延迟1秒,每3秒执行一次");
}, 1, 3, TimeUnit.SECONDS);
try {
Thread.sleep(11000); // 等待足够的时间让所有子线程完成
} catch (InterruptedException e) {
// do nothing or handle the exception as needed
}
}
}
executor.execute 和 executor.submit有什么区别
executor.execute
和 executor.submit
是 Java 中用于提交任务到线程池的方法,它们之间有一些关键的区别:
返回类型:
execute(Runnable command)
:这个方法没有返回值。它只接受一个Runnable
对象,并将其提交给线程池执行。submit(Callable<T> task)
和submit(Runnable task)
:这两个方法返回一个Future
对象。Future
可以用于检查任务的状态、获取任务的结果或取消任务。
任务类型:
execute
:只能接受Runnable
类型的任务。submit
:可以接受Runnable
和Callable
类型的任务。Callable
任务可以返回结果或抛出异常,而Runnable
任务不能。
异常处理:
execute
:如果任务在执行过程中抛出未捕获的异常,线程池会将异常传播到Thread.UncaughtExceptionHandler
。submit
:如果任务在执行过程中抛出未捕获的异常,异常会被捕获并存储在返回的Future
对象中。调用Future.get()
方法时会抛出ExecutionException
,其原因是任务抛出的异常。
多线程函数式编程思想
Java多线程 允许程序同时执行多个任务,提高了程序的性能和响应能力。在Java中创建线程主要有以下几种方式:
继承Thread类:通过创建一个继承自Thread类的子类,并重写run方法来定义线程的任务,然后通过创建子类的实例并调用start方法来启动线程。
实现Runnable接口:通过创建一个实现Runnable接口的类,并实现run方法,然后创建Thread类的实例并传递Runnable对象来启动线程。
实现Callable接口:Callable接口与Runnable类似,但它可以返回结果并能抛出异常。通过创建Callable接口的实现类,并使用FutureTask类来包装Callable对象,可以获取线程执行的结果。
线程在其生命周期内会经历不同的状态,包括新建、就绪、运行、阻塞和终止。线程同步和互斥是多线程编程中的重要概念,用于保护共享资源并防止数据不一致性和竞态条件。Java提供了synchronized关键字来实现同步块,确保只有一个线程可以访问同步块内的代码。
线程池 是一种管理和复用线程的机制,可以有效降低线程创建和销毁的开销。Java提供了Executor框架来创建和管理线程池。
函数式编程 是一种编程范式,它将计算机程序视为数学函数的计算,强调程序由一系列函数组成,并通过函数调用和组合来解决问题。函数式编程的特点包括纯函数、高阶函数、不可变性和避免副作用。
Java 8引入了Lambda表达式,它是函数式编程的核心,允许将函数作为一种参数进行传递,也可以将函数作为返回值返回。Lambda表达式简化了匿名内部类的编写,使得代码更加简洁和易读。
在实际应用中,多线程和函数式编程可以结合使用,例如使用并行流(Parallel Streams)来处理集合,可以利用多核处理器提高程序的执行效率。
Java 8的Lambda表达式在多线程编程中有哪些优势和局限性?
优势:
代码简洁性:Lambda表达式提供了一种简洁的方式来编写匿名函数,避免了冗长的匿名内部类定义,使得多线程代码更加简洁易读。
函数式编程支持:Lambda表达式支持函数式编程范式,使得并行处理和事件处理等多线程编程任务更加直观和易于实现。
并行流:Lambda表达式可以与Stream API结合使用,通过parallelStream方法轻松实现数据的并行处理,提高程序的性能。
线程池和异步处理:Lambda表达式与CompletableFuture等异步编程工具结合,可以简化多线程和异步编程的复杂性,提高代码的可维护性和可读性。
局限性:
性能问题:Lambda表达式在某些情况下可能会导致性能问题,例如装箱和拆箱操作可能会产生额外的开销,Lambda表达式创建新对象可能导致垃圾回收的开销增加。
闭包和作用域问题:Lambda表达式引用外部变量时会形成闭包,这可能导致额外的内存开销和作用域问题。
不可变性要求:Lambda表达式中引用的外部变量必须是不可变的(final或事实上不可变),这可能限制了Lambda表达式在多线程编程中的灵活性。
类型推断局限性:在某些情况下,Java编译器可能无法正确推断Lambda表达式的参数类型,需要手动指定类型,这可能会降低代码的简洁性。
Java 8的Lambda表达式在并发编程中如何与线程池结合使用?
使用
Runnable
接口的Lambda表达式: 你可以使用Lambda表达式来创建Runnable
任务,并提交给线程池执行。例如:ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> { // 任务代码 });
使用
Callable
接口的Lambda表达式: 类似于Runnable
,Callable
接口也支持Lambda表达式,并且可以返回结果和抛出异常。结果可以通过Future
获取。ExecutorService executor = Executors.newFixedThreadPool(4); Future<String> future = executor.submit(() -> { // 任务代码,返回结果 return "Result"; });
结合
CompletableFuture
:CompletableFuture
提供了更强大的异步编程能力,可以与Lambda表达式结合使用,实现复杂的异步逻辑。ExecutorService executor = Executors.newFixedThreadPool(4); CompletableFuture.supplyAsync(() -> { // 异步任务 return "Result"; }, executor).thenAccept(result -> { // 处理结果 });
错误处理: 在使用Lambda表达式和线程池时,错误处理也非常重要。可以通过
CompletableFuture
的exceptionally
方法来处理异常。CompletableFuture.supplyAsync(() -> { // 可能抛出异常的任务 return "Result"; }).exceptionally(ex -> { // 异常处理 return "Error"; });
线程池的自定义: 有时候,你可能需要自定义线程池的大小或者线程工厂。在这种情况下,你可以创建自己的
ExecutorService
并使用Lambda表达式提交任务。ExecutorService customExecutor = Executors.newFixedThreadPool(10, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setPriority(Thread.NORM_PRIORITY); return t; } }); customExecutor.submit(() -> { // 任务代码 });
资源清理: 在任务执行完毕后,记得关闭线程池以释放资源。这是一个良好的编程实践,可以避免潜在的内存泄漏。
executor.shutdown();
如何使用Java 8的Lambda表达式来实现线程池中的异步任务调度?
创建线程池:首先,你需要创建一个线程池来执行异步任务。可以使用
Executors
工厂方法来创建不同类型的线程池。ExecutorService executor = Executors.newFixedThreadPool(4);
提交异步任务:使用
CompletableFuture
的supplyAsync
或runAsync
方法提交异步任务。这些方法接受一个Supplier
或Runnable
任务,并且可以指定线程池作为第二个参数。CompletableFuture.supplyAsync(() -> { // 任务代码 return "Result"; }, executor);
或者
CompletableFuture.runAsync(() -> { // 任务代码 }, executor);
处理结果:可以使用
thenAccept
、thenApply
或其他then
方法来处理异步任务的结果。CompletableFuture.supplyAsync(() -> { // 任务代码 return "Result"; }, executor).thenAccept(result -> { // 处理结果 System.out.println(result); });
异常处理:可以使用
exceptionally
或handle
方法来处理异步任务中的异常。CompletableFuture.supplyAsync(() -> { if (true) throw new RuntimeException("Error"); return "Result"; }, executor).exceptionally(ex -> { // 异常处理 return "Error result"; });
等待所有任务完成:如果你有多个异步任务,可以使用
CompletableFuture.allOf
方法等待所有任务完成。CompletableFuture<Void> future1 = CompletableFuture.supplyAsync(() -> { // 任务1 }, executor); CompletableFuture<Void> future2 = CompletableFuture.supplyAsync(() -> { // 任务2 }, executor); CompletableFuture.allOf(future1, future2).join(); // 等待所有任务完成
关闭线程池:在所有异步任务执行完毕后,应该关闭线程池以释放资源。
executor.shutdown();
如何使用Java 8的Lambda表达式来优化现有的线程池任务执行?
简化任务提交:通过Lambda表达式,可以轻松地创建Runnable或Callable任务,而无需编写额外的类或匿名内部类。例如,使用
ExecutorService
的submit
方法提交任务时,可以直接使用Lambda表达式:ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> { // 任务代码 });
利用并行流:Java 8的Stream API支持并行操作,可以通过Lambda表达式轻松实现集合的并行处理。例如,对列表进行并行排序:
List<String> strings = Arrays.asList("a", "b", "c"); strings.parallelStream().sorted().forEach(System.out::println);
使用CompletableFuture:
CompletableFuture
提供了丰富的API来处理异步编程。结合Lambda表达式,可以简化异步任务的编写和管理。例如,异步执行任务并处理结果:CompletableFuture.supplyAsync(() -> { // 异步任务 return "Result"; }, executor).thenAccept(result -> { // 处理结果 System.out.println(result); });
优化性能:Lambda表达式可以避免不必要的对象创建,通过重用Lambda表达式实例或使用方法引用来减少性能开销。例如,重用Lambda表达式:
final Consumer<Integer> printer = System.out::println; list.parallelStream().forEach(printer);
合理配置线程池:根据应用的实际需求,合理配置线程池的大小、队列类型、拒绝策略等参数,以提高线程池的性能和资源利用率。
监控和调整线程池:通过监控线程池的运行状态,如活跃线程数、任务队列大小等,可以及时发现并调整线程池配置,以适应不同的负载情况。
避免线程池的过度使用:避免在高并发场景下过度使用线程池,以免造成资源竞争和线程饥饿。在必要时,可以考虑使用专门的线程池来处理特定类型的任务。
在Java 8中,除了使用Lambda表达式,还有哪些方法可以提高线程池的效率?
合理配置线程池参数:根据应用程序的负载和性能要求,调整线程池的核心线程数、最大线程数、线程存活时间和任务队列的大小。例如,对于CPU密集型任务,线程数可以设置为CPU核心数的1到2倍;对于I/O密集型任务,线程数可以更多一些,因为线程可能会等待外部资源,如磁盘或网络。
选择合适的任务队列:根据任务的特性选择合适的任务队列。例如,
ArrayBlockingQueue
适用于任务数量可预测的场景,而LinkedBlockingQueue
适用于任务数量较多且执行时间不确定的场景。SynchronousQueue
适用于高吞吐量且任务执行时间短的场景。使用合适的拒绝策略:当线程池和任务队列都满了,新提交的任务将被拒绝。可以通过实现
RejectedExecutionHandler
接口来定义自己的拒绝策略,例如记录日志、抛出异常或者运行任务的调用线程自己执行这个任务。动态调整线程池大小:在运行时根据系统负载动态调整线程池的核心线程数和最大线程数。这可以通过实现一个监控线程池性能的机制来完成,并根据性能指标来调整线程池参数。
避免耗时任务阻塞线程池:对于耗时较长的任务,考虑使用异步处理或专门的线程池来处理,以免阻塞线程池中的其他任务。
正确关闭线程池:在应用程序关闭或不再需要线程池时,正确关闭线程池以释放资源。可以使用
shutdown()
方法平滑关闭线程池,等待当前执行的任务完成后关闭,或者使用shutdownNow()
方法尝试立即停止所有正在执行的任务。监控线程池性能:定期监控线程池的性能指标,如活跃线程数、任务队列长度、任务完成时间等,以便及时发现并调整线程池配置。
避免线程池的过度使用:不要在每个方法调用或每个功能模块中都创建新的线程池。应该重用现有的线程池实例,或者使用全局的线程池管理器来管理线程池。
使用
CompletableFuture
:结合CompletableFuture
和线程池,可以简化异步编程的复杂性,提高代码的可读性和维护性。