关于程序员:并发编程线程

43次阅读

共计 8037 个字符,预计需要花费 21 分钟才能阅读完成。

线程

什么是线程

  • 线程是计算机中的根本执行单元,是程序执行的流程,是操作系统分配资源的根本单位
  • 线程的作用:进步程序的并发性,进步 CPU 的利用率
  • 线程的分类:用户线程和守护线程

如何创立线程

在 Java 中,有三种罕用的创立线程的形式:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 应用 Callable 和 Future 接口

继承 Thread 类

继承 Thread 类是最根本的创立线程的形式之一。步骤如下:

  1. 定义一个类继承 Thread 类,并重写 run 办法
  2. 在 run 办法中实现须要执行的代码
  3. 调用 start 办法启动线程

示例代码:

public class MyThread extends Thread {
    @Override
    public void run() {// 须要执行的代码}
}

// 创立并启动线程
MyThread myThread = new MyThread();
myThread.start();

这种形式的长处是简略间接,容易了解和上手,然而如果须要继承其余类或者实现其余接口,就无奈再应用这种形式创立线程。

实现 Runnable 接口

实现 Runnable 接口是另一种创立线程的形式。步骤如下:

  1. 定义一个类实现 Runnable 接口,并重写 run 办法
  2. 在 run 办法中实现须要执行的代码
  3. 创立 Thread 对象,将 Runnable 对象作为参数传入 Thread 的构造方法中
  4. 调用 start 办法启动线程

示例代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {// 须要执行的代码}
}

// 创立并启动线程
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();

这种形式的长处是能够防止单继承的限度,同时也能够实现资源共享,毛病是比拟繁琐。

应用 Callable 和 Future 接口

应用 Callable 和 Future 接口是一种更加灵便的创立线程的形式。步骤如下:

  1. 定义一个类实现 Callable 接口,并重写 call 办法
  2. 在 call 办法中实现须要执行的代码
  3. 创立 ExecutorService 对象,调用 submit 办法提交 Callable 对象
  4. 调用 Future 对象的 get 办法获取 Callable 的返回值

示例代码:

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 须要执行的代码
        return "result";
    }
}

// 创立 ExecutorService 对象
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 提交工作并获取 Future 对象
Future<String> future = executorService.submit(new MyCallable());

// 获取工作的返回值
String result = future.get();

// 敞开线程池
executorService.shutdown();

这种形式的长处是能够返回执行后果,能够抛出异样,毛病是绝对于其余形式更加简单。

创立线程的形式须要依据理论状况来思考。如果只是简略的多线程操作,继承 Thread 类或实现 Runnable 接口即可;如果须要返回后果或抛出异样,则须要应用 Callable 和 Future 接口。

线程的状态

Java 中的线程状态是指线程在运行过程中所处的状态,能够分为以下五种状态:

1. 新建状态

线程对象被创立后,然而还没有调用 start() 办法启动线程时,线程处于新建状态。此时线程的状态能够通过 getState() 办法获取,通常为 NEW。在新建状态下,线程还没有被调配 CPU 资源,因而不会执行任何工作。

2. 就绪状态

当线程调用 start() 办法后,线程进入就绪状态。在就绪状态下,线程曾经筹备好了,期待 CPU 调度执行。此时线程的状态能够通过 getState() 办法获取,通常为 RUNNABLE。在就绪状态下,线程曾经被调配了 CPU 资源,然而还没有开始执行工作。

3. 运行状态

当线程取得 CPU 工夫片,开始执行工作时,线程进入运行状态。此时线程的状态能够通过 getState() 办法获取,通常为 RUNNABLE。在运行状态下,线程正在执行工作,占用 CPU 资源。

4. 阻塞状态

当线程期待某个条件,例如 IO 操作、期待获取锁时,线程将进入阻塞状态。在阻塞状态下,线程不会占用 CPU 资源,因而也不会执行工作。当线程期待的条件满足时,线程将进入就绪状态,期待 CPU 调度执行。此时线程的状态能够通过 getState() 办法获取,通常为 BLOCKED。

5. 终止状态

线程执行完 run() 办法后,或者异样终止,线程进入终止状态。线程一旦进入终止状态就不能再进入其余状态。此时线程的状态能够通过 getState() 办法获取,通常为 TERMINATED。在终止状态下,线程曾经实现了工作,不再占用 CPU 资源。

线程状态的变动由 JVM 调度器负责,程序员能够通过 Thread 类提供的一些办法来察看和控制线程的状态。例如:

  • getState() 办法:获取线程的状态。
  • sleep() 办法:使以后线程休眠指定的工夫。
  • yield() 办法:让出以后线程所占用的 CPU 资源,让其余线程执行。
  • join() 办法:期待其余线程执行结束再执行以后线程。
  • interrupt() 办法:中断线程的执行。
  • setPriority() 办法:设置线程的优先级。

理解线程状态的变动能够更好地控制线程的执行,避免出现死锁、饥饿等问题

除了上述五种状态,Java 中还有一种非凡的状态,即 TIMED_WAITING 状态。当线程调用 sleep() 办法或 wait() 办法时,线程将进入 TIMED_WAITING 状态。在这种状态下,线程不会占用 CPU 资源,直到指定的工夫达到或者被其余线程唤醒,才会进入就绪状态。

线程同步

多个线程同时拜访同一个资源时,如果不加以协调,可能会导致数据的不一致性,这就是线程安全性问题。为了解决这个问题,须要对多个线程之间的拜访加以协调,这就是线程同步。

线程同步的形式

Java 中线程同步的形式次要有两种,一种是应用 synchronized 关键字,另一种是应用 Lock 接口。

synchronized 关键字

synchronized 关键字能够保障同一时刻只有一个线程访问共享资源。synchronized 关键字能够用于办法、代码块等多种场景。在应用 synchronized 关键字时,须要留神以下准则:

  • 保障锁定的是共享资源,而不是公有资源。
  • 尽量减小锁的粒度。
  • 防止死锁和饥饿问题。

Lock 接口

Lock 接口是 JDK1.5 中引入的新个性,与 synchronized 关键字相似,也能够用于线程同步。相比于 synchronized 关键字,Lock 接口具备以下长处:

  • 能够抉择偏心锁和非偏心锁。
  • 能够实现多个条件变量。
  • 能够防止死锁问题。

在应用 Lock 接口时,须要留神以下准则:

  • 在 finally 块中开释锁。
  • 防止应用锁定过长时间的代码块。
  • 防止应用重入锁。

线程同步的其余问题

线程同步不仅波及到同步的形式,还波及到一些其余的问题,例如竞态条件、死锁、饥饿等问题。

竞态条件

竞态条件指的是多个线程执行的程序不确定,导致后果的不确定性。例如,当多个线程同时对一个变量进行自增操作时,后果可能会呈现谬误。

死锁

死锁指的是多个线程相互期待对方开释资源,导致所有的线程都无奈继续执行。例如,当线程 A 持有锁 1,期待锁 2,而线程 B 持有锁 2,期待锁 1 时,就会呈现死锁。

饥饿

饥饿指的是某个线程长时间无奈取得所需的资源,导致始终无奈执行。例如,当一个线程始终无奈取得锁,就会始终处于饥饿状态。

线程死亡

线程死亡能够通过调用 stop() 办法或者 run() 办法完结,然而这两种办法都不举荐应用。正确的形式是让线程天然死亡,即让线程的 run() 办法失常执行结束。

理解线程同步的形式和问题能够更好地控制线程的执行,避免出现线程安全性问题。在多线程编程中,确保线程同步正当,避免出现竞态条件和线程平安问题

此外,还须要思考一些其余的问题,例如性能、可扩展性、代码复杂度等。因而,抉择适合的线程同步形式和解决方案,须要综合思考多个因素,能力失去最优的后果。

线程通信

多个线程协调实现一个工作,须要通过线程间通信来实现。Java 中提供了多种线程间通信形式,例如:wait() 和 notify() 办法,CountDownLatch 等。上面将介绍其中一些罕用的线程间通信形式。

wait() 和 notify() 办法

wait() 和 notify() 办法是 Java 中最根本的线程间通信形式。wait() 办法能够使调用线程进入期待状态,直到其余线程调用 notify() 办法唤醒该线程。notify() 办法能够唤醒一个期待该对象锁的线程。应用 wait() 和 notify() 办法实现线程间通信的步骤如下:

  1. 在共享资源上加锁,防止多个线程同时拜访。
  2. 线程 A 调用 wait() 办法进入期待状态,开释锁。
  3. 线程 B 获取锁,批改共享资源。
  4. 线程 B 调用 notify() 办法唤醒线程 A。
  5. 线程 B 开释锁。

应用 wait() 和 notify() 办法实现线程间通信的长处是简略易用,毛病是只能唤醒一个期待线程。

CountDownLatch

CountDownLatch 是 Java 中的一个同步工具类,它能够实现线程间的期待和唤醒。应用 CountDownLatch 实现线程间通信的步骤如下:

  1. 创立 CountDownLatch 对象,设置须要期待的线程数。
  2. 多个线程调用 await() 办法进入期待状态。
  3. 另一个线程调用 CountDownLatch 对象的 countDown() 办法进行计数,直到计数器为 0 时,所有期待线程被唤醒。

CountDownLatch 的长处是能够唤醒多个期待线程,毛病是计数器只能应用一次。

CyclicBarrier

CyclicBarrier 是 Java 中的另一个同步工具类,它也能够实现线程间的期待和唤醒。应用 CyclicBarrier 实现线程间通信的步骤如下:

  1. 创立 CyclicBarrier 对象,设置须要期待的线程数和期待实现后须要执行的工作。
  2. 多个线程调用 await() 办法进入期待状态。
  3. 当所有期待线程都达到屏障点时,执行指定的工作。

CyclicBarrier 的长处是能够重复使用,毛病是所有期待线程必须同时达到屏障点。

Semaphore

Semaphore 是 Java 中的另一个同步工具类,它能够管制同时拜访某个资源的线程数。应用 Semaphore 实现线程间通信的步骤如下:

  1. 创立 Semaphore 对象,设置容许同时拜访的线程数。
  2. 多个线程调用 acquire() 办法获取许可证,如果许可证数量为 0,则线程进入期待状态。
  3. 当某个线程不再须要拜访资源时,调用 release() 办法开释许可证。

Semaphore 的长处是能够管制并发拜访数量,毛病是须要手动开释许可证。

依据理论状况抉择适合的形式来实现线程间通信。在理论开发中,须要留神线程安全性问题,避免出现死锁、饥饿等问题,保障程序的正确性和稳定性。同时,须要依据具体需要抉择适合的同步工具类,避免出现线程安全性问题。

线程池

线程池是一种用于治理和复用线程的机制,能够优化多线程应用程序的性能和稳定性。线程池能够防止频繁创立和销毁线程的开销,同时能够限度并发线程的数量,防止资源适度占用。Java 中提供了 ThreadPoolExecutor 类作为线程池的实现,同时也提供了 Executors 类作为线程池的工厂,使得能够更加疾速地创立线程池。

线程池的组成

Java 线程池由以下四个局部组成:

  • 工作队列(Task Queue):用于寄存期待执行的工作。线程池中的工作能够是 Runnable 接口或者 Callable 接口的实现。
  • 工作线程(Worker Threads):用于执行工作的线程。线程池中的每个线程都会执行工作队列中的工作,直到工作队列为空。
  • 线程池管理器(ThreadPool Manager):用于管理工作线程和工作队列。线程池管理器负责创立新的线程和销毁闲置的线程,同时也负责将工作增加到工作队列中。
  • 工作(Task):须要执行的工作。工作能够是 Runnable 接口或者 Callable 接口的实现,能够通过 execute() 办法提交到线程池中执行。

线程池的参数

Java 线程池的参数包含以下几个:

  • corePoolSize:线程池中的外围线程数。当线程池中的线程数少于外围线程数时,新工作会创立新线程来执行,即便其余线程处于闲暇状态。
  • maximumPoolSize:线程池中的最大线程数。当线程池中的线程数等于最大线程数时,新的工作会被增加到工作队列中期待执行。
  • keepAliveTime:非核心线程的超时工夫。当线程池中的线程数大于外围线程数,而且有一些线程处于闲暇状态超过了设定的超时工夫,那么这些线程将被销毁。
  • unit:keepAliveTime 的单位。
  • workQueue:用于寄存期待执行的工作的队列。工作队列能够是有界队列或者无界队列,有界队列的大小能够通过构造函数或者工厂办法进行设置。
  • threadFactory:用于创立新线程的工厂。能够通过实现 ThreadFactory 接口自定义线程的创立形式,例如设置线程的名称、线程的优先级等。
  • handler:当工作无奈执行时的解决策略。能够通过实现 RejectedExecutionHandler 接口自定义工作无奈执行时的解决策略,例如抛出异样或者抛弃工作。

线程池的工作流程

Java 线程池的工作流程如下:

  1. 当一个工作提交到线程池时,线程池会查看以后外围线程数是否已满,如果没有满,则创立一个新线程执行该工作,否则将该工作退出到工作队列中期待执行。
  2. 当工作队列已满时,线程池会查看以后非核心线程数是否已满,如果没有满,则创立一个新线程执行该工作,否则依据指定的策略进行解决,例如抛出异样或者抛弃工作。
  3. 当一个线程实现工作后,会从工作队列中取出下一个工作执行,如果工作队列为空,则该线程会期待新工作的到来。
  4. 如果线程池中的线程数超过了外围线程数,而且有一些线程处于闲暇状态超过了设定的超时工夫,那么这些线程将被销毁。

线程池的优缺点

Java 线程池的长处包含:

  • 升高线程的创立和销毁开销。线程的创立和销毁是十分消耗资源的操作,通过线程池能够防止频繁创立和销毁线程,从而升高资源的占用。
  • 限度线程数量,防止资源适度占用。线程池能够限度并发线程的数量,防止资源适度占用,从而进步零碎的稳定性和可靠性。
  • 进步程序的响应速度和稳定性。线程池能够进步程序的响应速度和稳定性,因为线程池中的线程能够复用,从而防止了频繁的创立和销毁线程的开销。
  • 能够灵便调整线程池的参数,以适应不同的利用场景。线程池的参数能够依据具体利用场景进行调整,例如外围线程数、最大线程数、工作队列、超时工夫等。

Java 线程池的毛病包含:

  • 工作无奈勾销。一旦工作被提交到线程池中执行,就无奈勾销,这可能会导致一些问题,例如内存透露或者资源节约。
  • 工作的执行程序不确定。线程池中的工作的执行程序是不确定的,这可能会导致一些问题,例如死锁或者数据竞争。
  • 线程池的参数设置须要依据具体利用场景进行调整。线程池的参数设置须要依据具体利用场景进行调整,如果设置不当,可能会导致资源适度占用或者工作无奈执行等问题。

线程池的最佳实际

在应用线程池时,须要留神以下几点:

  • 应该防止应用 Executors 类创立线程池,因为它的默认参数可能不适宜所有利用场景。应该间接应用 ThreadPoolExecutor 类创立线程池,并依据具体利用场景调整线程池的参数。
  • 应该依据具体利用场景调整线程池的参数,例如外围线程数、最大线程数、工作队列、超时工夫等。应该依据具体利用场景进行调整,防止资源适度占用、工作无奈勾销等问题。
  • 应该应用 Callable 接口代替 Runnable 接口,因为它能够返回执行后果,不便进行错误处理和后果解决。应用 Callable 接口须要应用 submit() 办法提交工作。
  • 应该应用 Future 接口来获取工作执行的后果,能够异步地获取工作的执行后果,防止阻塞主线程。能够应用 submit() 办法返回 Future 对象,通过 Future 对象获取工作的执行后果。
  • 应该应用 CompletionService 类来批量执行工作,能够进步程序的并发度和效率。CompletionService 类能够异步地获取工作的执行后果,并且能够批量执行工作。

在应用线程池时,须要依据具体利用场景调整线程池的参数,避免出现资源适度占用、工作无奈勾销等问题。同时,还须要留神应用 Callable 接口和 Future 接口获取工作执行的后果,应用 CompletionService 类批量执行工作等最佳实际。

线程安全性问题

什么是线程平安?

线程平安是指当多个线程拜访同一个共享资源时,不会呈现不正确的后果或者不可预期的后果。线程平安是多线程编程中的一个重要问题,须要思考并发拜访的状况,避免出现数据竞争、死锁、饥饿等问题。

线程平安的技术手段

Java 中提供了多种技术手段来实现线程平安,次要包含以下几种:

同步办法和同步块

应用 synchronized 关键字来实现同步,保障同一时刻只有一个线程能够访问共享资源。同步办法和同步块能够保障线程平安,然而可能会升高程序的性能。

原子类

应用 Atomic 包中的原子类来实现线程平安的操作,例如 AtomicInteger、AtomicBoolean、AtomicLong 等。原子类能够保障线程平安,同时不会升高程序的性能。

应用 Lock 接口和 ReentrantLock 类来实现锁机制,反对更加灵便的线程同步,例如可重入锁、偏心锁、读写锁等。应用锁能够实现更加细粒度的线程同步,然而须要留神防止死锁和饥饿等问题。

信号量

应用 Semaphore 类来实现线程间的信号量管制,能够限度并发拜访数量。应用信号量能够控制线程的并发度,避免出现资源适度占用的问题。

条件变量

应用 Condition 接口和实现类来实现线程间的期待和告诉机制,能够更加灵便地控制线程的同步。应用条件变量能够实现更加简单的线程同步和通信。

并发汇合类

应用 Java 中提供的并发汇合类,例如 ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList 等,能够在多线程环境下平安地拜访汇合类。应用并发汇合类能够避免出现数据竞争等问题。

线程平安的最佳实际

在理论开发中,须要留神以下几点来保障线程平安:

防止应用共享变量

共享变量容易引起数据竞争和死锁等问题,能够应用线程本地变量或者消息传递等形式防止共享变量的应用。

应用不可变对象

不可变对象是指创立之后不可批改的对象,例如 String、Integer 等。应用不可变对象能够避免出现数据竞争和死锁等问题。

同步共享资源

对于须要共享的资源,应用同步办法、同步块、锁等机制来实现线程平安的拜访。

应用并发汇合类

Java 中提供了多种并发汇合类,能够平安地在多线程环境下拜访汇合类。

防止死锁和饥饿

死锁和饥饿是线程安全性的常见问题,须要避免出现这些问题,保障程序的正确性和稳定性。

在理论开发中,须要依据具体利用场景抉择适合的线程平安技术手段,保障程序的正确性和稳定性。同时,须要留神线程平安的最佳实际,避免出现数据竞争、死锁、饥饿等问题。

总结

在这篇文章中,咱们介绍了 Java 线程的创立, 状态, 同步, 通信, 平安,线程池。同时论述了他们的实现形式,优缺点,注意事项和最佳实际,在理论开发中,咱们须要依据具体利用场景抉择适合的线程池参数和线程平安技术手段,以避免出现数据竞争、死锁、饥饿等问题。心愿这篇文章对您有所帮忙!

本文由 mdnice 多平台公布

正文完
 0