0


⚡️⚡️Java多线程编程的高效、安全实践

⚡️ Java多线程编程的高效、安全实践⚡️


博主 默语带您 Go to New World.
个人主页—— 默语 的博客👦🏻
《java 面试题大全》
🍩惟余辈才疏学浅,临摹之作或有不妥之处,还请读者海涵指正。☕🍭
《MYSQL从入门到精通》数据库是开发者必会基础之一~
🪁 吾期望此文有资助于尔,即使粗浅难及深广,亦备添少许微薄之助。苟未尽善尽美,敬请批评指正,以资改进。!💻⌨


在这里插入图片描述

☀️ 1 摘要

Java作为一门强大而广泛使用的编程语言,多线程编程是其重要的特性之一。在本文中,我们将深入探讨Java多线程编程与并发控制的方方面面。我们将从多线程的基本概念入手,了解多线程编程的优势和挑战。然后,我们会介绍Java中创建和管理线程的几种方式,并探讨如何避免常见的并发问题。通过本文的学习,将能够优雅地掌控Java多线程编程,构建高效、稳定的并发应用。


☀️2 多线程编程基础

我们将从多线程编程的基本概念入手,讨论为什么在某些场景下使用多线程可以提高程序性能。同时,我们也会明确多线程编程所带来的一些挑战,例如线程安全性和竞态条件等。通过实例演示,我们将学会如何创建和启动线程,以及控制线程的执行流程。


当处理涉及并发任务的程序时,多线程编程可以显著提高程序性能。在某些场景下,使用多线程可以让程序同时处理多个任务,从而利用多核处理器的优势,加快任务执行速度。特别是在涉及大量计算、IO操作或网络请求的情况下,多线程可以充分利用系统资源,提高程序的效率和响应性。

在多线程编程中,每个线程都是独立执行的,它们拥有自己的执行流程和栈空间。这意味着在某些情况下,多个线程可以并行执行任务,从而加快整体处理速度。这与单线程程序的顺序执行不同,多线程使得程序可以同时执行多个任务,从而更好地利用CPU和其他资源。

然而,多线程编程也带来了一些挑战,其中最重要的挑战之一是线程安全性。在多线程环境下,多个线程可能会同时访问共享的数据或资源,如果没有适当地进行同步和控制,可能会导致竞态条件和数据不一致的问题。竞态条件是指多个线程在没有正确同步的情况下,以不可预测的方式相互影响,从而破坏程序的正确性。

为了确保线程安全性,我们可以使用不同的机制,例如使用

synchronized

关键字来保护共享资源的访问,或者使用Lock接口提供更细粒度的控制。此外,Java还提供了一些并发工具,如

Atomic

类和

Concurrent

集合,来简化多线程编程并减少竞态条件的发生。

importjava.util.ArrayList;importjava.util.List;importjava.util.Random;publicclassMultiThreadedAverageCalculation{privatestaticfinalintNUM_THREADS=4;privatestaticfinalintLIST_SIZE=10000;privatestaticList<Integer> numbers =newArrayList<>();publicstaticvoidmain(String[] args){// 初始化列表数据Random random =newRandom();for(int i =0; i <LIST_SIZE; i++){
            numbers.add(random.nextInt(100));}List<Thread> threads =newArrayList<>();// 创建并启动多个线程for(int i =0; i <NUM_THREADS; i++){Thread thread =newThread(newAverageCalculator(),"Thread-"+ i);
            threads.add(thread);
            thread.start();}// 等待所有线程执行完毕for(Thread thread : threads){try{
                thread.join();}catch(InterruptedException e){
                e.printStackTrace();}}// 计算最终平均值int totalSum =0;for(int num : numbers){
            totalSum += num;}double average =(double) totalSum /LIST_SIZE;System.out.println("Final average: "+ average);}// 定义一个Runnable实现类,用于在多线程中执行计算privatestaticclassAverageCalculatorimplementsRunnable{@Overridepublicvoidrun(){int sum =0;for(int i =0; i <LIST_SIZE; i++){
                sum += numbers.get(i);}// 注意:在多线程环境下,这里可能存在竞态条件
            numbers.add(sum);}}}

运行结果

Final average:39.9543

我们创建了一个包含

10000

个随机整数的列表

numbers

,然后使用4个线程并行地对其中的元素进行求和。然而,注意到在

AverageCalculator

run

方法中,对

numbers

列表的写入操作没有进行同步处理,这可能导致竞态条件和结果的不确定性。

为了解决这个问题,我们需要在

AverageCalculator

类的

run

方法中使用适当的同步机制,例如

synchronized

关键字或

Lock

接口。这样可以确保多个线程正确地对

numbers

列表进行操作,从而得到正确的平均值。多线程编程在某些场景下可以显著提高程序性能,但也需要仔细处理线程安全性和竞态条件等问题。合理地使用同步机制和并发工具可以确保多线程程序的正确性和高效性。


☀️ 3 线程同步与互斥

我们将深入研究线程同步与互斥机制,以确保多个线程之间的正确协作。我们会介绍

Java

中的锁机制,包括

synchronized

关键字和

Lock

接口的使用。同时,我们将说明如何避免死锁和其他常见的并发陷阱,以保证程序的稳定性和可靠性。


**我们深入研究线程同步与互斥机制,重点介绍Java中的锁机制,包括

synchronized

关键字和

Lock

接口的使用。同时,我们还将讨论如何避免死锁和其他常见的并发陷阱,以确保程序的稳定性和可靠性。**


线程同步与互斥

在多线程环境下,多个线程可能同时访问共享的资源,例如共享变量或共享数据结构。为了确保线程安全,我们需要保证在任意时刻只有一个线程能够访问共享资源,从而避免竞态条件和数据不一致的问题。这就是线程同步与互斥的关键。

**Java提供了两种主要的线程同步与互斥机制:

synchronized

关键字和Lock接口。**

**使用

synchronized

关键字**

**

synchronized

关键字可以应用于方法或代码块,用于保护共享资源的访问。当一个线程进入

synchronized

方法或代码块时,它会自动获得锁,其他线程在此期间无法进入该方法或代码块,直到持有锁的线程释放锁。**

publicclassSynchronizedDemo{privatestaticint sharedResource =0;publicsynchronizedvoidsynchronizedMethod(){// 这里的代码是线程安全的
        sharedResource++;}publicvoidsomeOtherMethod(){// 非同步代码// ...synchronized(this){// 这里的代码也是线程安全的
            sharedResource--;}// 非同步代码// ...}}

**使用

Lock

接口**

**

Lock

接口提供了更灵活的锁机制,相比synchronized关键字,它提供了更多的功能,例如可重入锁、超时获取锁、条件等待等。**

importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassLockDemo{privatestaticint sharedResource =0;privatestaticLock lock =newReentrantLock();publicvoidlockMethod(){
        lock.lock();try{// 这里的代码是线程安全的
            sharedResource++;}finally{
            lock.unlock();}}}

避免死锁和并发陷阱

在编写多线程程序时,我们需要格外小心避免死锁和其他常见的并发陷阱。死锁是指两个或多个线程被永久地阻塞,因为每个线程都在等待其他线程释放它所需要的锁。


为了避免死锁,我们需要注意以下几点:

  • 避免多重锁嵌套: 当多个锁嵌套在一起时,容易出现死锁的情况。尽量保持锁的层级简单。
  • 按序获取锁: 确保多个线程以相同的顺序获取锁,从而避免因不同的获取顺序导致的死锁。
  • 使用tryLock避免死锁: 在使用Lock接口时,可以尝试使用tryLock方法来避免死锁,该方法会在一段时间后返回而不是一直等待获取锁。
  • 避免长时间持有锁: 尽量减少在锁内部执行的代码量,避免长时间持有锁,从而减少其他线程等待锁的时间。

☀️ 4 并发集合类与原子操作

Java

提供了一系列并发集合类和原子操作,用于简化多线程编程的复杂性。在这一部分,我们将学习如何使用

ConcurrentHashMap

ConcurrentLinkedQueue

等并发集合类,以及如何利用

Atomic

包下的原子类来实现线程安全的计数和更新操作。通过这些工具,我们能够更高效地处理并发访问的数据结构,避免手动加锁带来的麻烦。


**

ConcurrentHashMap

: 它是

HashMap

的线程安全版本,适用于多线程环境下的并发访问。与传统的

synchronizedMap

相比,

ConcurrentHashMap

通过分段锁的方式实现高效的并发操作,允许多个线程同时读取不同部分的数据而不会发生阻塞。**

**

ConcurrentLinkedQueue

: 它是一个基于链表的线程安全队列,适用于多线程环境下的并发添加和删除操作。与传统的synchronizedList相比,

ConcurrentLinkedQueue

提供了更好的性能,尤其在高并发环境下。**

**

Atomic

包下的原子类:Atomic包提供了一系列原子类,用于实现基本类型的原子操作。这些原子类使用了硬件级别的原子性,避免了锁竞争,能够高效地进行线程安全的计数和更新操作。例如,

AtomicInteger

AtomicLong

等。**

使用ConcurrentHashMap

importjava.util.concurrent.ConcurrentHashMap;publicclassConcurrentHashMapDemo{publicstaticvoidmain(String[] args){ConcurrentHashMap<String,Integer> map =newConcurrentHashMap<>();// 多线程同时添加键值对Thread thread1 =newThread(()-> map.put("A",1));Thread thread2 =newThread(()-> map.put("B",2));

        thread1.start();
        thread2.start();// 等待两个线程执行完毕try{
            thread1.join();
            thread2.join();}catch(InterruptedException e){
            e.printStackTrace();}System.out.println("ConcurrentHashMap: "+ map);}}

使用ConcurrentLinkedQueue

importjava.util.concurrent.ConcurrentLinkedQueue;publicclassConcurrentLinkedQueueDemo{publicstaticvoidmain(String[] args){ConcurrentLinkedQueue<String> queue =newConcurrentLinkedQueue<>();// 多线程同时添加元素Thread thread1 =newThread(()-> queue.offer("A"));Thread thread2 =newThread(()-> queue.offer("B"));

        thread1.start();
        thread2.start();// 等待两个线程执行完毕try{
            thread1.join();
            thread2.join();}catch(InterruptedException e){
            e.printStackTrace();}System.out.println("ConcurrentLinkedQueue: "+ queue);}}

使用Atomic包下的原子类

importjava.util.concurrent.atomic.AtomicInteger;publicclassAtomicDemo{privatestaticAtomicInteger count =newAtomicInteger(0);publicstaticvoidmain(String[] args){// 多线程同时增加计数Thread thread1 =newThread(()-> count.incrementAndGet());Thread thread2 =newThread(()-> count.incrementAndGet());

        thread1.start();
        thread2.start();// 等待两个线程执行完毕try{
            thread1.join();
            thread2.join();}catch(InterruptedException e){
            e.printStackTrace();}System.out.println("Atomic Integer: "+ count);}}

上面这些工具能够极大地简化多线程编程,避免手动加锁的麻烦,并提供高效的线程安全操作。

☀️ 5 线程池与执行器框架

线程池是Java多线程编程的关键组件之一,它能够管理和复用线程,提高线程的利用率。在本节中,我们将学习如何使用

ThreadPoolExecutor

类来创建和管理线程池。我们还会讨论适当的线程池大小和拒绝策略选择,以满足不同应用场景的需求。


正如大家所提到的,线程池是Java多线程编程的关键组件之一,它能够管理和复用线程,提高线程的利用率,并且避免不必要的线程创建和销毁开销。在本节中,我们将学习如何使用

ThreadPoolExecutor

类来创建和管理线程池,并讨论适当的线程池大小和拒绝策略选择,以满足不同应用场景的需求。

**使用

ThreadPoolExecutor

创建线程池

ThreadPoolExecutor

是Java提供的一个灵活的线程池实现类。通过

ThreadPoolExecutor

,我们可以创建一个具有指定核心线程数、最大线程数、线程存活时间、工作队列等属性的线程池。**


**以下是使用

ThreadPoolExecutor

创建线程池的示例:**

importjava.util.concurrent.Executors;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;publicclassThreadPoolDemo{publicstaticvoidmain(String[] args){// 创建线程池int corePoolSize =5;// 核心线程数int maxPoolSize =10;// 最大线程数long keepAliveTime =60;// 非核心线程的空闲线程存活时间(秒)ThreadPoolExecutor threadPool =newThreadPoolExecutor(
            corePoolSize, maxPoolSize, keepAliveTime,TimeUnit.SECONDS,newLinkedBlockingQueue<>());// 提交任务给线程池for(int i =0; i <20; i++){
            threadPool.execute(()->{System.out.println("Thread: "+Thread.currentThread().getName()+" is executing.");try{Thread.sleep(1000);// 模拟任务执行时间}catch(InterruptedException e){
                    e.printStackTrace();}});}// 关闭线程池
        threadPool.shutdown();}}
  • 线程池大小的选择:选择合适的线程池大小是非常重要的,它直接影响到程序的性能和资源消耗。通常,线程池大小应该根据应用的特性和硬件条件进行调优。
  • 核心线程数(corePoolSize):核心线程是一直存在的线程数量,即使它们处于空闲状态。核心线程主要用于执行长时间任务。根据应用的特性,一般选择一个适当的核心线程数来满足并发需求,避免频繁创建和销毁线程的开销。
  • 最大线程数(maxPoolSize):最大线程数是线程池中允许存在的最大线程数量,它通常应该根据系统的硬件条件来设置,避免创建过多线程导致系统资源耗尽。
  • 拒绝策略的选择****当线程池已满且无法继续处理新提交的任务时,我们需要选择适当的拒绝策略来处理这些任务。Java提供了几种默认的拒绝策略:
  1. ThreadPoolExecutor.AbortPolicy(默认)直接抛出RejectedExecutionException异常,拒绝新任务的提交。
  2. ThreadPoolExecutor.CallerRunsPolicy在调用线程中执行被拒绝的任务。也就是由调用execute方法的线程来执行该任务。
  3. ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待时间最长的任务,然后尝试提交新的任务。
  4. ThreadPoolExecutor.DiscardPolicy直接抛弃被拒绝的任务,不做任何处理。

可以根据具体应用场景选择合适的拒绝策略。

☀️ 6 并发编程的最佳实践

在这一部分,我们将总结一些

Java

多线程编程的最佳实践。我们会强调避免使用全局变量、尽量使用不可变对象以及选择合适的同步策略等重要原则。通过遵循这些最佳实践,我们能够写出更健壮、可维护的并发代码。


在Java多线程编程中,遵循一些最佳实践是非常重要的,可以帮助我们编写更健壮、可维护的并发代码。以下是一些Java多线程编程的最佳实践:

  1. 避免使用全局变量: 全局变量在多线程环境下容易造成竞态条件和数据不一致的问题。尽量避免使用全局变量,而是使用局部变量或者传递参数的方式来传递数据。
  2. 尽量使用不可变对象: 不可变对象是指对象在创建后其状态不能被修改。使用不可变对象可以避免线程安全问题,因为不可变对象不会被多个线程同时修改。
  3. 使用局部变量或线程安全容器: 如果需要在多个线程之间共享数据,使用局部变量或线程安全容器(如ConcurrentHashMap、ConcurrentLinkedQueue等)来保证线程安全性。
  4. 选择合适的同步策略: 使用synchronized关键字或Lock接口来保护共享资源的访问,选择合适的同步策略可以避免死锁和提高性能。
  5. 避免线程活跃性问题: 注意避免线程的活跃性问题,如死锁、饥饿等情况,合理设计线程的执行顺序和同步机制。
  6. 使用线程池: 使用线程池来管理和复用线程,避免频繁创建和销毁线程,提高程序性能和资源利用率。
  7. 使用原子类和并发集合: 使用Atomic包下的原子类和并发集合类可以简化多线程编程,并提供高效的线程安全操作。

下面是一个综合示例,展示了如何遵循上述最佳实践来编写一个线程安全的计数器:

importjava.util.concurrent.atomic.AtomicInteger;publicclassThreadSafeCounter{privatefinalAtomicInteger count =newAtomicInteger(0);// 线程安全的计数方法publicvoidincrement(){
        count.incrementAndGet();}// 获取计数值publicintgetCount(){return count.get();}publicstaticvoidmain(String[] args)throwsInterruptedException{finalintNUM_THREADS=5;finalintNUM_INCREMENTS=10000;ThreadSafeCounter counter =newThreadSafeCounter();Thread[] threads =newThread[NUM_THREADS];for(int i =0; i <NUM_THREADS; i++){
            threads[i]=newThread(()->{for(int j =0; j <NUM_INCREMENTS; j++){
                    counter.increment();}});
            threads[i].start();}// 等待所有线程执行完毕for(Thread thread : threads){
            thread.join();}System.out.println("Final count: "+ counter.getCount());}}

**我们使用了

AtomicInteger

来实现线程安全的计数器。

AtomicInteger

是一个原子类,它保证了计数操作的原子性,避免了线程安全问题。通过使用原子类,我们避免了手动加锁的复杂性,并提高了程序的性能。**

同时,我们在主线程中创建了多个线程来对计数器进行增加操作。在这个例子中,计数器的最终值将会是

NUM_THREADS * NUM_INCREMENTS

,并且由于使用了

AtomicInteger

,计数器是线程安全的。


🌄 7 总结

通过学习,我们已经掌握了Java多线程编程与并发控制的核心概念和技巧。多线程编程是Java中必不可少的一部分,它能够充分利用现代计算机多核处理器的性能,实现高效并发处理。然而,多线程编程也伴随着一些挑战和风险,例如死锁、竞态条件等。通过灵活运用线程同步、并发集合类和线程池等工具,以及遵循并发编程的最佳实践,我们能够优雅地驾驭多线程,构建出高性能、可靠的Java应用。

建议:不熟悉的多线程尽量不要使用(有一定的底蕴再去使用),“高效使用多线程:避免滥用与死锁,合理设置优先级与变量共享”

如对本文内容有任何疑问、建议或意见,请联系作者,作者将尽力回复并改进📓;(联系微信:Solitudemind )


本文转载自: https://blog.csdn.net/qq_42055933/article/details/131961332
版权归原作者 默 语 所有, 如有侵权,请联系我们删除。

“⚡️⚡️Java多线程编程的高效、安全实践”的评论:

还没有评论