- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章Java多线程:从基本概念到避坑指南由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
多核的机器,现在已经非常常见了。即使是一块手机,也都配备了强劲的多核处理器。通过多进程和多线程的手段,就可以让多个CPU同时工作,来加快任务的执行.
多线程,是编程中一个比较高级的话题。由于它涉及到共享资源的操作,所以在编码时非常容易出现问题。Java的concurrent包,提供了非常多的工具,来帮助我们简化这些变量的同步,但学习应用之路依然充满了曲折.
本篇文章,将简单的介绍一下Java中多线程的基本知识。然后着重介绍一下初学者在多线程编程中一些最容易出现问题的地方,很多都是血泪经验。规避了这些坑,就相当于规避了90%凶残的多线程bug.
1.1 轻量级进程 。
在JVM中,一个线程,其实是一个轻量级进程(LWP)。所谓的轻量级进程,其实是用户进程调用系统内核,所提供的一套接口。实际上,它还要调用更加底层的内核线程(KLT).
实际上,JVM的线程创建销毁以及调度等,都是依赖于操作系统的。如果你看一下Thread类里面的多个函数,你会发现很多都是native的,直接调用了底层操作系统的函数.
下图是JVM在Linux上简单的线程模型.
可以看到,不同的线程在进行切换的时候,会频繁在用户态和内核态进行状态转换。这种切换的代价是比较大的,也就是我们平常所说的上下文切换(Context Switch).
1.2 JMM 。
在介绍线程同步之前,我们有必要介绍一个新的名词,那就是JVM的内存模型JMM.
JMM并不是说堆、metaspace这种内存的划分,它是一个完全不同的概念,指的是与线程相关的Java运行时线程内存模型.
由于Java代码在执行的时候,很多指令都不是原子的,如果这些值的执行顺序发生了错位,就会获得不同的结果。比如,i++的动作就可以翻译成以下的字节码.
这还只是代码层面的。如果再加上CPU每核的各级缓存,这个执行过程会变得更加细腻。如果我们希望执行完i++之后,再执行i--,仅靠初级的字节码指令,是无法完成的。我们需要一些同步手段.
上图就是JMM的内存模型,它分为主存储器(Main Memory)和工作存储器(Working Memory)两种。我们平常在Thread中操作这些变量,其实是操作的主存储器的一个副本。当修改完之后,还需要重新刷到主存储器上,其他的线程才能够知道这些变化.
1.3 Java中常见的线程同步方式 。
为了完成JMM的操作,完成线程之间的变量同步,Java提供了非常多的同步手段.
从上面的描述可以看出,多线程编程要学的东西可实在太多了。幸运的是,同步方式虽然千变万化,但我们创建线程的方式却没几种.
第一类就是Thread类。大家都知道有两种实现方式。第一可以继承Thread覆盖它的run方法;第二种是实现Runnable接口,实现它的run方法;而第三种创建线程的方法,就是通过线程池.
其实,到最后,就只有一种启动方式,那就是Thread。线程池和Runnable,不过是一种封装好的快捷方式罢了.
多线程这么复杂,这么容易出问题,那常见的都有那些问题,我们又该如何避免呢?下面,我将介绍10个高频出现的坑,并给出解决方案.
2.1. 线程池打爆机器 。
首先,我们聊一个非常非常低级,但又产生了严重后果的多线程错误.
通常,我们创建线程的方式有Thread,Runnable和线程池三种。随着Java1.8的普及,现在最常用的就是线程池方式.
有一次,我们线上的服务器出现了僵死,就连远程ssh,都登录不上,只能无奈的重启。大家发现,只要启动某个应用,过不了几分钟,就会出现这种情况。最终定位到了几行让人啼笑皆非的代码.
有位对多线程不太熟悉的同学,使用了线程池去异步处理消息。通常,我们都会把线程池作为类的静态变量,或者是成员变量。但是这位同学,却将它放在了方法内部。也就是说,每当有一个请求到来的时候,都会创建一个新的线程池。当请求量一增加,系统资源就被耗尽,最终造成整个机器的僵死.
这种问题如何去避免?只能通过代码review。所以多线程相关的代码,哪怕是非常简单的同步关键字,都要交给有经验的人去写。即使没有这种条件,也要非常仔细的对这些代码进行review.
2.2. 锁要关闭 。
相比较synchronized关键字加的独占锁,concurrent包里面的Lock提供了更多的灵活性。可以根据需要,选择公平锁与非公平锁、读锁与写锁.
但Lock用完之后是要关闭的,也就是lock和unlock要成对出现,否则就容易出现锁泄露,造成了其他的线程永远了拿不到这个锁.
如下面的代码,我们在调用lock之后,发生了异常,try中的执行逻辑将被中断,unlock将永远没有机会执行。在这种情况下,线程获取的锁资源,将永远无法释放.
正确的做法,就是将unlock函数,放到finally块中,确保它总是能够执行.
由于lock也是一个普通的对象,是可以作为函数的参数的。如果你把lock在函数之间传来传去的,同样会有时序逻辑混乱的情况。在平时的编码中,也要避免这种把lock当参数的情况.
2.3. wait要包两层 。
Object作为Java的基类,提供了四个方法wait wait(timeout) notify notifyAll ,用来处理线程同步问题,可以看出wait等函数的地位是多么的高大。在平常的工作中,写业务代码的同学使用这些函数的机率是比较小的,所以一旦用到很容易出问题.
但使用这些函数有一个非常大的前提,那就是必须使用synchronized进行包裹,否则会抛出IllegalMonitorStateException。比如下面的代码,在执行的时候就会报错.
类似的方法,还有concurrent包里的Condition对象,使用的时候也必须出现在lock和unlock函数之间.
为什么在wait之前,需要先同步这个对象呢?因为JVM要求,在执行wait之时,线程需要持有这个对象的monitor,显然同步关键字能够完成这个功能.
但是,仅仅这么做,还是不够的,wait函数通常要放在while循环里才行,JDK在代码里做了明确的注释.
重点:这是因为,wait的意思,是在notify的时候,能够向下执行逻辑。但在notify的时候,这个wait的条件可能已经是不成立的了,因为在等待的这段时间里条件条件可能发生了变化,需要再进行一次判断,所以写在while循环里是一种简单的写法.
带if条件的wait和notify要包两层,一层synchronized,一层while,这就是wait等函数的正确用法.
2.4. 不要覆盖锁对象 。
使用synchronized关键字时,如果是加在普通方法上的,那么锁的就是this对象;如果是加载static方法上的,那锁的就是class。除了用在方法上,synchronized还可以直接指定要锁定的对象,锁代码块,达到细粒度的锁控制.
如果这个锁的对象,被覆盖了会怎么样?比如下面这个.
上面的代码,由于在逻辑中,强行给锁listeners对象进行了重新赋值,会造成锁的错乱或者失效.
为了保险起见,我们通常把锁对象声明成final类型的.
或者直接声明专用的锁对象,定义成普通的Object对象即可.
2.5. 处理循环中的异常 。
在异步线程里处理一些定时任务,或者执行时间非常长的批量处理,是经常遇到的需求。我就不止一次看到小伙伴们的程序执行了一部分就停止的情况.
排查到这些中止的根本原因,就是其中的某行数据发生了问题,造成了整个线程的死亡.
我们还是来看一下代码的模板.
在loop函数中,执行我们真正的业务逻辑。当执行到某个task的时候,发生了异常。这个时候,线程并不会继续运行下去,而是会抛出异常直接中止。在写普通函数的时候,我们都知道程序的这种行为,但一旦到了多线程,很多同学都会忘了这一环.
值得注意的是,即使是非捕获类型的NullPointerException,也会引起线程的中止。所以,时刻把要执行的逻辑,放在try catch中,是个非常好的习惯.
2.6. HashMap正确用法 。
HashMap在多线程环境下,会产生死循环问题。这个问题已经得到了广泛的普及,因为它会产生非常严重的后果:CPU跑满,代码无法执行,jstack查看时阻塞在get方法上.
至于怎么提高HashMap效率,什么时候转红黑树转列表,这是阳春白雪的八股界话题,我们下里巴人只关注怎么不出问题.
网络上有详细的文章描述死循环问题产生的场景,大体因为HashMap在进行rehash时,会形成环形链。某些get请求会走到这个环上。JDK并不认为这是个bug,虽然它的影响比较恶劣.
如果你判断你的集合类会被多线程使用,那就可以使用线程安全的ConcurrentHashMap来替代它.
HashMap还有一个安全删除的问题,和多线程关系不大,但它抛出的是ConcurrentModificationException,看起来像是多线程的问题。我们一块来看看它.
上面的代码会抛出异常,这是由于HashMap的Fail-Fast机制。如果我们想要安全的删除某些元素,应该使用迭代器.
2.7. 线程安全的保护范围 。
使用了线程安全的类,写出来的代码就一定是线程安全的么?答案是否定的.
线程安全的类,只负责它内部的方法是线程安全的。如我我们在外面把它包了一层,那么它是否能达到线程安全的效果,就需要重新探讨.
比如下面这种情况,我们使用了线程安全的ConcurrentHashMap来存储计数。虽然ConcurrentHashMap本身是线程安全的,不会再出现死循环的问题。但addCounter函数,明显是不正确的,它需要使用synchronized函数包裹才行.
这是开发人员常踩的坑之一。要达到线程安全,需要看一下线程安全的作用范围。如果更大维度的逻辑存在同步问题,那么即使使用了线程安全的集合,也达不到想要的效果.
2.8. volatile作用有限 。
volatile关键字,解决了变量的可见性问题,可以让你的修改,立马让其他线程给读到.
虽然这个东西在面试的时候问的挺多的,包括ConcurrentHashMap中队volatile的那些优化。但在平常的使用中,你真的可能只会接触到boolean变量的值修改.
千万不要把它用在计数或者线程同步上,比如下面这样.
这段代码在多线程环境下,是不准确的。这是因为volatile只保证可见性,不保证原子性,多线程操作并不能保证其正确性.
直接用Atomic类或者同步关键字多好,你真的在乎这纳秒级别的差异么?
2.9. 日期处理要小心 。
很多时候,日期处理也会出问题。这是因为使用了全局的Calendar,SimpleDateFormat等。当多个线程同时执行format函数的时候,就会出现数据错乱.
为了改进,我们通常将SimpleDateFormat放在ThreadLocal中,每个线程一份拷贝,这样可以避免一些问题。当然,现在我们可以使用线程安全的DateTimeFormatter了.
2.10. 不要在构造函数中启动线程 。
在构造函数,或者static代码块中启动新的线程,并没有什么错误。但是,强烈不推荐你这么做.
因为Java是有继承的,如果你在构造函数中做了这种事,那么子类的行为将变得非常魔幻。另外,this对象可能在构造完毕之前,出递到另外一个地方被使用,造成一些不可预料的行为.
所以把线程的启动,放在一个普通方法,比如start中,是更好的选择。它可以减少bug发生的机率.
wait和notify是非常容易出问题的地方, 。
编码格式要求非常严格。synchronized关键字相对来说比较简单,但同步代码块的时候依然有许多要注意的点。这些经验,在concurrent包所提供的各种API中依然实用。我们还要处理多线程逻辑中遇到的各种异常问题,避免中断,避免死锁。规避了这些坑,基本上多线程代码写起来就算是入门了.
许多java开发,都是刚刚接触多线程开发,在平常的工作中应用也不是很多。如果你做的是crud的业务系统,那么写一些多线程代码的时候就更少了。但总有例外,你的程序变得很慢,或者排查某个问题,你会直接参与到多线程的编码中来.
我们的各种工具软件,也在大量使用多线程。从Tomcat,到各种中间件,再到各种数据库连接池缓存等,每个地方都充斥着多线程的代码.
即使是有经验的开发,也会陷入很多多线程的陷阱。因为异步会造成时序的混乱,必须要通过强制的手段达到数据的同步。多线程运行,首先要保证准确性,使用线程安全的集合进行数据存储;还要保证效率,毕竟使用多线程的目标就是如此.
希望本文中的这些实际案例,让你对多线程的理解,更上一层楼.
小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道.
原文链接:https://mp.weixin.qq.com/s/jyrUtHrZtg3KQQmprHax9w 。
最后此篇关于Java多线程:从基本概念到避坑指南的文章就讲到这里了,如果你想了解更多关于Java多线程:从基本概念到避坑指南的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我将 Bootstrap 与 css 和 java 脚本结合使用。在不影响前端代码的情况下,我真的很难在css中绘制这个背景。在许多问题中,人们将宽度和高度设置为 0%。但是由于我的导航栏,我不能使用
我正在用 c 编写一个程序来读取文件的内容。代码如下: #include void main() { char line[90]; while(scanf("%79[^\
我想使用 javascript 获取矩阵数组的所有对 Angular 线。假设输入输出如下: input = [ [1,2,3], [4,5,6], [7,8,9], ] output =
可以用pdfmake绘制lines,circles和other shapes吗?如果是,是否有documentation或样本?我想用jsPDF替换pdfmake。 最佳答案 是的,有可能。 pdfm
我有一个小svg小部件,其目的是显示角度列表(参见图片)。 现在,角度是线元素,仅具有笔触,没有填充。但是现在我想使用一种“内部填充”颜色和一种“笔触/边框”颜色。我猜想line元素不能解决这个问题,
我正在为带有三角对象的 3D 场景编写一个非常基本的光线转换器,一切都工作正常,直到我决定尝试从场景原点 (0/0/0) 以外的点转换光线。 但是,当我将光线原点更改为 (0/1/0) 时,相交测试突
这个问题已经有答案了: Why do people write "#!/usr/bin/env python" on the first line of a Python script? (22 个回
如何使用大约 50 个星号 * 并使用 for 循环绘制一条水平线?当我尝试这样做时,结果是垂直(而不是水平)列出 50 个星号。 public void drawAstline() { f
这是一个让球以对角线方式下降的 UI,但球保持静止;线程似乎无法正常工作。你能告诉我如何让球移动吗? 请下载一个球并更改目录,以便程序可以找到您的球的分配位置。没有必要下载足球场,但如果您愿意,也可以
我在我的一个项目中使用 Jmeter 和 Ant,当我们生成报告时,它会在报告中显示 URL、#Samples、失败、成功率、平均时间、最短时间、最长时间。 我也想在报告中包含 90% 的时间线。 现
我有一个不寻常的问题,希望有人能帮助我。我想用 Canvas (android) 画一条 Swing 或波浪线,但我不知道该怎么做。它将成为蝌蚪的尾部,所以理想情况下我希望它的形状更像三角形,一端更大
这个问题已经有答案了: Checking Collision of Shapes with JavaFX (1 个回答) 已关闭 8 年前。 我正在使用 JavaFx 8 库。 我的任务很简单:我想检
如何按编号的百分比拆分文件。行数? 假设我想将我的文件分成 3 个部分(60%/20%/20% 部分),我可以手动执行此操作,-_-: $ wc -l brown.txt 57339 brown.tx
我正在努力实现这样的目标: 但这就是我设法做到的。 你能帮我实现预期的结果吗? 更新: 如果我删除 bootstrap.css 依赖项,问题就会消失。我怎样才能让它与 Bootstrap 一起工作?
我目前正在构建一个网站,但遇到了 transform: scale 的问题。我有一个按钮,当用户将鼠标悬停在它上面时,会发生两件事: 背景以对 Angular 线“扫过” 按钮标签颜色改变 按钮稍微变
我需要使用直线和仿射变换绘制大量数据点的图形(缩放图形以适合 View )。 目前,我正在使用 NSBezierPath,但我认为它效率很低(因为点在绘制之前被复制到贝塞尔路径)。通过将我的数据切割成
我正在使用基于 SVM 分类的 HOG 特征检测器。我可以成功提取车牌,但提取的车牌除了车牌号外还有一些不必要的像素/线。我的图像处理流程如下: 在灰度图像上应用 HOG 检测器 裁剪检测到的区域 调
我有以下图片: 我想填充它的轮廓(即我想在这张图片中填充线条)。 我尝试了形态学闭合,但使用大小为 3x3 的矩形内核和 10 迭代并没有填满整个边界。我还尝试了一个 21x21 内核和 1 迭代,但
我必须找到一种算法,可以找到两组数组之间的交集总数,而其中一个数组已排序。 举个例子,我们有这两个数组,我们向相应的数字画直线。 这两个数组为我们提供了总共 7 个交集。 有什么样的算法可以帮助我解决
简单地说 - 我想使用透视投影从近裁剪平面绘制一条射线/线到远裁剪平面。我有我认为是使用各种 OpenGL/图形编程指南中描述的方法通过单击鼠标生成的正确标准化的世界坐标。 我遇到的问题是我的光线似乎
我是一名优秀的程序员,十分优秀!