不堆概念、换个角度聊多线程并发编程

本文从并发编程的实际问题出发,介绍了多线程的实现方式,如子线程、线程池、定时器等。接着讲解了synchronized和Lock在并发控制中的作用,以及如何避免死锁。接着探讨了锁的优化策略,如降低锁范围、缩小锁粒度、使用读写锁等。还介绍了无锁编程中的ThreadLocal、volatile、发布订阅模式和CAS乐观锁策略。最后提到了分布式锁的应用和并发与并行的区别。
摘要由CSDN通过智能技术生成

🚀 优质资源分享 🚀

学习路线指引(点击解锁) 知识定位 人群定位
🧡 Python实战微信订餐小程序 🧡 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
💛Python量化交易实战💛 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

大家好,又见面了。

在上一篇文档《JAVA基于CompletableFuture的流水线并行处理深度实践,满满干货》中,我们一起探讨了JAVA中并行编码的相关内容,在文中也一起比较了并行与并发的区别。作为姊妹篇,这里我们就再展开聊一聊关于并发相关的内容。

image.png

俗话说,双拳难敌四手
俗话还说,人多力量大

在现实生活中,我们通过团队化的方式来获得比单兵作战更高的单位时间内整体产出速度。同样,在编码世界中,为了提升处理效率,并发一直以来都是软件开发设计场景中无法绕过的话题。不管是微观层面的单个进程内多线程处理模式,还是宏观层面整个系统集群化多节点部署策略,为了提升系统的整体并发吞吐量,程序员们可谓是煞费苦心。

image.png

当然,俗话也说,人多眼杂林子大了什么鸟都有。在现实中,团队中多人一起配合工作的时候,一系列的问题又会显现:

  • 同一个事情,老王和小张都以为还没处理,结果都去处理了,最后造成了成员工作量的浪费、甚至因为重复处理了一遍导致数据错误
  • 两个有关联的事情分别给了老王和翠花,结果老王在等待翠花先给出结果再开始处理自己的事情,翠花也在等待老王先给出结果然后再处理自己的事情,结果两个人就这么一致等下去,事情一直没完成
  • 同一个文档,小张和翠花各自更新的时候,出现相互覆盖的情况

image.png

编码源于生活、代码世界其实也处处体现着生活中的朴素哲学思维。纵然并发场景存在一些可能的隐患问题,但我们也不必因噎废食,正所谓先了解它、再掌控它。

作为提升吞吐性能的不二良方,下面我们就一起来尝试按照问题解决型的思路一步步推进,换个角度探讨下多线程并发相关的内容,全面了解下多线程并发世界的各种关联,进而更从容优雅的让并发为我们所用,成为我们提升系统性能的神兵利器。

多线程——并发第一步

并发探险的第一关,就是如何支持并发。下面大概列举下常见的几种方式:

⭐️子线程⭐️

一些简单的场景中,我们为了提升主线程的处理性能,会将过程中一些耗时操作放到一个单独的子线程中进行同步处理。在代码中可以通过创建临时子线程的方式来执行即可:

public void buyProduct() {
    int price = getPrice();
    // 子线程同步处理部分操作
    new Thread(this::printTicket).start();
    // 主线程继续处理其它逻辑
    doOtherOperations(price);
}

⭐️线程池⭐️

频繁创建线程、销毁线程的操作属于一种消耗性能的操作,而且创建线程的数量不可控。所以对于一些固定需要在子线程中并行处理的任务场景,我们可以通过创建线程池的方式,固定维护着一批可用线程,循环利用,去处理任务,以实现提升效率与便于管控的诉求:

private ExecutorService threadPool = Executors.newFixedThreadPool(3);

public void testReleaseThreadLocalSafely() {
    // 任务直接放到线程池中进行处理
    threadPool.submit(this::mockServiceOperations);
}

⭐️定时器⭐️

定时器是一种比较特殊的多线程并发场景,也是经常可能会被忽视掉的一种情况。定时器也是在子线程中执行的,多个定时器之间、定时器线程与主线程之间、定时器线程与业务子线程之间都会以多线程的形式并发处理。

@Scheduled(cron = "0 0/10 * * * ?")
public void syncBusinessInfo() {
    // do something here...
}

⭐️Tomcat等容器⭐️

常见的服务运行容器,比如Tomcat等,都是支持并发请求执行的。而常见的基于SpringBoot实现的服务,其service类都是由Spring进行托管单例对象。这种场景是比较常见的多线程场景。

改为多线程并发执行,虽然效率是提升了,但是问题也来了——数据执行结果不准确

结果不对,显然是我们无法接受的。所以摆在我们面前的下一难题,就是要保证执行结果数据的准确。

synchronized与lock

在JAVA中提到线程同步,使用最简单、应用频率最高的非synchronized关键字莫属了。它是 Java 内建的一种同步机制,代表了某种内在锁定的概念,当一个线程对某个共享资源加锁后,其他想要获取共享资源的线程必须进行等待,synchronized 也具有互斥排他的语义。具体用法如下:

  • synchronized 修饰实例方法,相当于是对类的实例(this)进行加锁,进入同步代码前需要获得当前实例的锁
public synchronized void test() {
    //...
}

  • synchronized 修饰代码块,相当于是给对象(syncObject)进行加锁,在进入代码块前需要先获得对象的锁
public void test() {
    synchronized(syncObject) {
        //允许访问控制的代码 
    }
    // 其它操作
}

  • synchronized 修饰静态方法,相当于是对LockTest.class)进行加锁
public class LockTest {
    public synchronized static void test() {
        //...
    }
}

对于被锁的目标对象而言,锁是具有排他性的,也就是同一个对象上的多个带锁方法,同一时刻只有1个线程可以抢到锁,其余都会被阻塞住。比如下面的代码,线程A和线程B分别同时请求method1method2,虽然调用的是不同的方法,但是两个线程其实是在争夺同一把锁

public class LockTest {
    public synchronized void method1() {
        // ... 
    }
    public synchronized void method2() {
        // ...
    }
}

image.png

由于synchronized属于JVM关键字,属于一种比较重量级的锁。在JDK中还提供了个Lock类,提供了众多不同类型的锁,供各种不同场景诉求使用。

public void test() {
    Lock lock = ...;
    lock.lock();
    try{
         // ...
    }catch(Exception ex){
         // ...
    }finally{
        // ...
        lock.unlock(); 
    }
}

synchronized不同,使用Lock的时候需要特别注意最后一定要可靠的释放掉占用的锁

到这里,再测试会发现,多线程并发执行,数据结果也对,似乎是没什么问题——但是这样真的就结束了吗?<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

[虚幻私塾】

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值