加入收藏 | 设为首页 | 会员中心 | 我要投稿 汽车网 (https://www.0577qiche.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 教程 > 正文

少堆概念、换个思路来聊聊多线程并发程式

发布时间:2023-04-01 11:31:56 所属栏目:教程 来源:
导读:不堆概念、换个角度聊多线程并发编程
大家好,又见面了。

在上一篇文档《JAVA基于CompletableFuture的流水线并行处理深度实践,满满干货》中,我们一起探讨了JAVA中并行编码的相关内容,在文中也一起比较了并行与
不堆概念、换个角度聊多线程并发编程
大家好,又见面了。

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

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

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

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

同一个事情,老王和小张都以为还没处理,结果都去处理了,最后造成了成员工作量的浪费、甚至因为重复处理了一遍导致数据错误
两个有关联的事情分别给了老王和翠花,结果老王在等待翠花先给出结果再开始处理自己的事情,翠花也在等待老王先给出结果然后再处理自己的事情,结果两个人就这么一致等下去,事情一直没完成
同一个文档,小张和翠花各自更新的时候,出现相互覆盖的情况
...
编码源于生活、代码世界其实也处处体现着生活中的朴素哲学思维。纵然并发场景存在一些可能的隐患问题,但我们也不必因噎废食,正所谓先了解它、再掌控它。

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

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

⭐️子线程⭐️

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

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分别同时请求method1和method2,虽然调用的是不同的方法,但是两个线程其实是在争夺同一把锁:

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

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

public void test() {
    Lock lock = ...;
    lock.lock();
    try{
         // ...
    }catch(Exception ex){
         // ...
    }finally{
        // ...
        lock.unlock(); 
    }
}
与synchronized不同,使用Lock的时候需要特别注意最后一定要可靠的释放掉占用的锁。

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

如果并发编程仅仅就这么点内容,那显然对不上它在编码界的地位。我们接着往下看。

死锁——不期而遇的小惊吓
经过前面的内容,我们知道了使用多线程的方式来实现并发处理,也知晓了可以通过加锁的方式来保证对共享数据编辑的顺序性与准确性。而加了锁之后稍不留神间,也许就会出现死锁。

(编辑:汽车网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章