Java并发编程入门,看这一篇就够了

时间:2022-06-16 04:14:05

Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容。这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类。当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富。

于是乎,就诞生了想写点东西记录下,以提升理解和对并发编程的认知。为什么需要用到并发?凡事总有好坏两面,之间的trade-off是什么,也就是说并发编程具有哪些挑战?以及在进行并发编程时应该了解和掌握的概念是什么?并发编程的三大特性是什么?这篇文章主要以这四个问题来谈一谈。

一.为什么要用到并发

一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过 并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升 。

*计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。

另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。 面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。正是因为这些优点,使得多线程技术能够得到重视,也是一名Java学习者应该掌握的:

  • 充分利用多核CPU的计算能力;
  • 方便进行业务拆分,提升应用性能

二. 并发编程有哪些挑战

多线程技术有这么多的好处,难道就没有一点缺点或者挑战么,就在任何场景下就一定适用么?很显然不是。

Java并发编程入门,看这一篇就够了

2.1 频繁的上下文切换

时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。

  • 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  • CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

由于上下文切换也是个相对比较耗时的操作,所以在"java并发编程的艺术"一书中有过一个实验,并发累加未必会比串行累加速度要快。 可以使用Lmbench3测量上下文切换的时长 vmstat测量上下文切换次数

2.2 线程安全(死锁)

多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。

public class DeadLockDemo {
private static String resource_a = "A";
private static String resource_b = "B"; public static void main(String[] args) {
deadLock();
} public static void deadLock() {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_a) {
System.out.println("get resource a");
try {
Thread.sleep(3000);
synchronized (resource_b) {
System.out.println("get resource b");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_b) {
System.out.println("get resource b");
synchronized (resource_a) {
System.out.println("get resource a");
}
}
}
});
threadA.start();
threadB.start(); }
}
在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b正在等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。同样可以通过jps,jstack证明这种推论:
"Thread-1":
waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String),
which is held by "Thread-1" Java stack information for the threads listed above:
===================================================
"Thread-1":
at learn.DeadLockDemo$2.run(DeadLockDemo.java:34)
- waiting to lock <0x00000007d5ff53a8(a java.lang.String)
- locked <0x00000007d5ff53d8(a java.lang.String)
at java.lang.Thread.run(Thread.java:722)
"Thread-0":
at learn.DeadLockDemo$1.run(DeadLockDemo.java:20)
- waiting to lock <0x00000007d5ff53d8(a java.lang.String)
- locked <0x00000007d5ff53a8(a java.lang.String)
at java.lang.Thread.run(Thread.java:722) Found 1 deadlock.

如上所述,完全可以看出当前死锁的情况。

那么,通常可以用如下方式避免死锁的情况:

  1. 避免一个线程同时获得多个锁;
  2. 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
  3. 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

所以,如何正确的使用多线程编程技术有很大的学问,比如如何保证线程安全,如何正确理解由于JMM内存模型在原子性,有序性,可见性带来的问题,比如数据脏读,DCL等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程中也会让你收获颇丰。

2.3 资源限制的挑战

  • 什么是资源限制

资源限制指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。

硬件资源包括:带宽的上传下载速度、硬盘读写速度和CPU的处理速度等

软件资源包括:线程池大小、数据库的连接数等

  • 资源限制引发的问题

在并发编程中,代码执行速度加快的原则是将代码中的串行部分变成并行执行,但有可能由于资源限制问题,导致程序仍按串行执行,此时程序不仅不会变快,反而更慢,因为增加了上下文切换和资源调度的时间。

  • 如何解决资源限制的问题

对于硬件资源限制:考虑使用集群方式并行执行程序。

对于软件资源限制:考虑使用资源池将资源复用,例如数据库连接池等

  • 资源限制情况下进行并发编程

根据不同的资源限制调整程序的并发度。

3. 应该了解的概念

3.1 同步VS异步

同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

3.2 并发与并行

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

3.3 阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

3.4 临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

4.并发编程的三大特性

并发编程有三大特性:原子性、可见性、有序性。
原子性:是指在一次操作或多次操作中,要么所有的操作都得到执行,要么都不执行。【类似于事务】
  • JMM只保证了基本读取和赋值的原子性操作
  • 多个原子性操作的组合不再是原子性操
  • 可以使用synchronized/lock保证某些代码片段的原子性
  • 对于int等类型的自增操作,可以通过java.util.concurrent.atomic.*保证原子性
可见性:是指一个线程对共享变量进行了修改,其他线程可以立即看到修改后的值。
有序性:是指代码在执行过程中的先后顺序是有序的。【Java编译器会对代码进行优化,执行顺序可能与开发者编写的顺序不同(指令重排)】
并发编程时,保证三大特性的方式有三种:
1、使用volatile关键字修饰变量
  • 当一个变量被volatile关键字修饰时,对于共享变量的读操作会直接在主存中进行,对于共享变量的写操作是先修改本地内存,修改结束后直接刷到主存中。(未被volatile修饰的变量被修改后,什么时候最新值会被刷到主存中是不确定的)
2、使用synchronized关键字修饰方法或代码块
  • synchronized关键字能保证同一时刻只有一个线程获得锁然后执行同步方法,并且确保锁释放之前,会将修改的变量刷入主存。
3、使用JUC提供的显式锁Lock
  • Lock能保证同一时刻只有一个线程获得锁然后执行同步方法,并且确保锁释放之前,会将修改的变量刷入主存。

最后,本文主要对Java并发编程开发需要的知识点作了简单的讲解,这里每一个知识点都可以用一篇文章去讲解,由于篇幅原因不能对每一个知识点都详细介绍,我相信通过本文你会对Java的并发编程会有更近一步的了解。如果您发现还有缺漏或者有错误的地方,可以在评论区补充,谢谢。

Java并发编程入门,看这一篇就够了的更多相关文章

  1. Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)

    摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...

  2. Java并发编程入门(二)

    1.竞态条件 1.1 定义 当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件.换句话说,正确的结果要取决于运气. 最常见的竞态条件类型:先检查后执行(Check-Then-Act)操 ...

  3. Java并发编程入门(一)

    一.为什么要并发? 出现背景:操作系统的出现,使计算机同时运行多个程序成为可能. 1.目的: 资源利用率.某些时候,程序必须等待一些外部操作完成(IO)才能继续运行,在等待时间运行其他程序,可以有效提 ...

  4. java并发编程工具类JUC第一篇&colon;BlockingQueue阻塞队列

    Java BlockingQueue接口java.util.concurrent.BlockingQueue表示一个可以存取元素,并且线程安全的队列.换句话说,当多线程同时从 JavaBlocking ...

  5. java并发编程JUC第十二篇:AtomicInteger原子整型

    AtomicInteger 类底层存储一个int值,并提供方法对该int值进行原子操作.AtomicInteger 作为java.util.concurrent.atomic包的一部分,从Java 1 ...

  6. java并发编程工具类JUC第二篇&colon;ArrayBlockingQueue

    类ArrayBlockingQueue是BlockingQueue接口的实现类,它是有界的阻塞队列,内部使用数组存储队列元素.这里的"有界"是指存储容量存在上限,不能无限存储元素. ...

  7. Spring入门看这一篇就够了

    前言 前面已经学习了Struts2和Hibernate框架了.接下来学习的是Spring框架...本博文主要是引入Spring框架... Spring介绍 Spring诞生: 创建Spring的目的就 ...

  8. Mybatis入门看这一篇就够了

    什么是MyBatis MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为 ...

  9. JSON入门看这一篇就够了

    什么是JSON JSON:JavaScript Object Notation [JavaScript 对象表示法] JSON 是存储和交换文本信息的语法.类似 XML. JSON采用完全独立于任何程 ...

随机推荐

  1. day27&lowbar;反射

    1.反射-概述(掌握) 反射就是在程序运行过程中,通过.class文件动态的获取类的信息(属性,构造,方法),并调用 注意:JAVA不是动态语言,因为动态语言强调在程序运行过程中不仅能获取并调用类里面 ...

  2. PHP实现RESTful风格的API实例(一)

    最近看了一些关于RESTful的资料,自己动手也写了一个RESTful实例,以下是源码 目录详情: restful/ Request.php 数据操作类 Response.php 输出类 index. ...

  3. Codeforces Round &num;364 As Fast As Possible

    二分思想,对所要花费的时间进行二分,再以模拟的形式进行验证是否可行. 使用二分法,可以将一个求最优解的问题转化为一个判定问题,优雅的暴力. #include<cstdio> #includ ...

  4. NPOI设置Excel保护

    有时,我们可能需要某些单元格只读,如在做模板时,模板中的数据是不能随意让别人改的.在Excel中,可以通过“审阅->保护工作表”来完成,如下图:  那么,在NPOI中有没有办法通过编码的方式达到 ...

  5. easyUI 比较时间大小

    //                    function toDate(str){ //                        var sd=str.split("-" ...

  6. leetcode 3Sum python

    # sort the array# loop from i = 0 # then left=i+1 right=len(nums)-1# try nums[i] - ( nums[left]+nums ...

  7. 目标指定法——S&period;M&period;A&period;R&period;T&period;

    一个有效的目标一定要是 具体的(Specific), 可测量的(Measureable), 可实现的(Attainable), 有现实意义的(Realistic), 以及有明确期限的(Time-bas ...

  8. JVM简析

    JVM结构图 1.程序计数器 是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支. ...

  9. eclispe file查找

    今天查找一段js代码时在本页内找不到,所以需要在整个工程下寻找. 过程如下

  10. linux下启动多个php,分别监听不同的端口。

    在工作中,我们可能会遇到,服务器集群的搭建. 这个时候,我们不可能,每一台服务器都是lnmp的环境,我们会把nmp分别放在不同的服务器上,不同的服务器负责不同的功能.比如我们下面要说的php 加入ng ...