说明:本文是之前在HR写的,想用来培训新员工的,没有写完就离开了。文中其实是说并发安全的,进程线程、线程安全并发安全等概念有点混着说了。还没有润色。
---------------
一、企业应用现状,和我们的工作
我们的客户,中石油,中石化,大型的集团企业,央企,国企,民企。
信息化已经渗透到企业经营管理的方方面面,经营管理,生产管理,内部控制,外部数据交换。
有些还是企业的核心业务系统,比如资金管理系统,容不得半点从出错,即便出了错误,也希望能尽快定位错误,修正错误。
企业信息系统的本质是什么?处理数据。所有的应用都围绕一个核心:资源,其中最主要的是数据,来运行。
作为开发人员,我们怕的是什么?
系统出错;
出错不可怕,我们可以查找原因,再现错误,尽快修正它!
比系统出错更可怕的是什么?
是偶尔出错,不忙时不出错,越忙越出错,还找不到原因(不能再现错误,没法调试,没法跟踪)
我下面要讲的这个问题,如果处理不好的话,就会导致比出错更可怕的错误!
错误不是必然的,是偶尔的,很难再现,根本无法调试。
只能凭经验静态分析查找可能的错误代码:因为多线程、并发处理不当,导致的问题。
一、多线程的概念
这里虽然叫线程,其实从这次讲的内容来看,交准确讲叫多并发,多任务更合适。从我们的关注点看,多线程 和并发是一个意思。多线程会导致并发。
所谓多线程,就是程序的多段代码,在多个线程里,同时,或者分时段运行。而不是单一执行顺序。
1,以前单cpu的,单线程;后来发展了多线程,但是因为是单cpu,其本质上还是串行的;
2,后来,出现了多cpu,多内核,出现了真正的并行计算
这里加上图,说明多cpu,单cpu。
线程T1,T2,…,Tn
UniProcessor System
++++++++++++++++++++++++++++++++++时间
T1----- CPU1
T2------- CPU1
……
Tn------ CPU1
===================================
DualProcessor system
T1------ CPU1
T2------ CPU2
Tn------ CPU1
多线程问题其实是单个程序内的并发问题,是单个程序内的多任务。
如果我们从整个企业的全局看,对企业资源的存取访问是多应用间的并发问题。
所以我们放大来看
==========================================
系统 S1,S2,…,Sn
+++++++++++++++++++++++++++++++++++时间
S1------
S2------
Sn------
多线程,多应用是个好东西,但是面临着资源争用的挑战。
说到这里,估计很多人就会理解我下面要讲的内容了。
二、多线程的好处
更有效利用资源;提高系统性能;简化程序
举例:
1,现实中的例子:烧水涮杯子的例子(高中应用文),有效利用时间
T1:烧水
T2:涮杯子
串行:T1----------------------------T2--------------
+++++0++++++++++++++++++18++++++++30
并行:T1-----------------------------
T2--------------
2,软件开发中的例子:
数据库读写的离职,读速度要快于写。
我要把1000条数据从数据库A复制到数据库B
读100条,需要1s,写100条,需要2s
如果单线程的话,应该是 10s 读 +20s 写 = 30 s
如果多线程(3个线程),1个读线程R,2个写线程W1,W2
R--R--R--R--R------------------R->|
--1--2--3--4--5--6--7--8--9--10--11--12--
W-----W-----W-----W-----W-----
W-----W-----W-----W------W------>|
一共需要12S。当然这里没有考虑线程切换的成本。
三、线程安全
但是:多线程的挑战:线程安全!
线程安全的概念:编写的代码能在多线程环境下,正确运行。
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染,并且为了避免数据污染,采取的措施得当,不会导致系统死锁,假死等问题。
线程安全的5个类型:不可变;线程安全;有条件的线程安全;线程兼容;线程对立。
为什么出现线程不安全的问题?
根源:出现了对资源的争用。
副作用:为了处理资源争用,采取了不当的措施,到时系统出现死锁,假死。
因为多个程序或线程都访问同一个资源,会导致出现线程不安全的的问题。
从本质上讲,即便不是多线程的,但是如果存在多个程序访问同一资源的问题,都会出现并发安全的问题。
问:我不写多线程程序,也会有线程安全的问题吗
回答是肯定的。
从单个应用看:
现在的应用系统,尤其是企业应用系统,肯定不是单机系统,是多用户系统。既然是多用户系统,如果多个用户同时访问系统的话,就会出现多线程的运行时情景,你写的程序可能会在多个线程中同时运行。这是就一个系统而言。
从企业全局看:
如果企业里有多个系统同时访问相同的资源,即便每个系统都是单用户的,不是多线程的,也存在类似的问题。
原子操作:automic operation,需要硬件支持。
四、线程不安全的几个表现:
1,得到非期望的结果(数据污染)
2,死锁
3,假死,等待的线程永远不会被唤醒。
举例说明:
1,数据污染
这是最主要的一个问题。
他有各种表现和各种场景。
public class myCounter{
private long count = 0;
public long inc(){
return ++count ;
}
}
++ 操作不是原值操作,分3步,
(1)将值取到寄存器,
(2)寄存器的值+1,
(3)将寄存器的值写回内存,
汇编指令:
(1)mov eax,dword ptr [count]
(2)add eax,1
(3)mov dword ptr [count],eax
T1:……(1)(2)(3) (1)(2)(3)
----------------------- -----------------------------
T2:……(2)(3) (2)(3)
count: 0 1 1
这三步不是原子操作(atomic operation)
画出计算机系统的内存模型:CPU寄存器->缓存->内存(栈)
如果以上类的一个实例 myCounter counter= new myCounter被多个线程引用,那么如果:
画出出错的运行步骤,
线程安全的类:<列举几个安全的列,比如 LinkedBlockQueue等>
非线程安全的类:
很多类都不是线程安全的,比如ArrayList,Hashtable等等。
那岂不是很危险,都不是线程安全的。别怕,好在我们极少有同一个类实例在不同的线程被引用的情况。大多的情况是,不同的线程会创建自己的类实例(instance),这样一般不会产生以上所说的脏数据。
再举个我们比较熟悉的例子
个人借款:
业务逻辑:如果员工的当前借款超过了50000,则不允许借款
我要借款1000
(1)取当前借款余额loan_balance=3000
select balance from ar_personal where emp = '16'
(2)判断是否满足借款条件:
如果loan_balance >=50000-1000 goto (4)
(3)借款
loan_balance+=1000
update ar_personal set balance=balance+1000 where emp= '16'
update ar_personal set balance=4000 where emp='16'
(4)退出
提问:以上的代码有没有问题??
五、怎么才能做并发安全的程序
1,时刻想着你的程序,不是1个人在战斗,很多人都在竞争资源,即便你的程序是单线程程序。
2,采取行动
(1), 临界区同步
只允许一个线程进入临界区代码段。
如果只有一个类。
(2)临界资源锁定
什么是临界资源(共享资源)
(1)内存变量,类实例
(2)文件
(3)数据库记录
(4)IO 端口/打印机/声卡/显卡(显示区域)/
(4)服务(service)
非临界资源的例子
基础类型的局部变量
未逃逸的非基础类型的局部变量
总之,不会被多个线程(类实例)共享的变量
(2)锁的类型
内存变量,信号量,应用自定义锁,
文件系统的文件锁,数据库锁
锁操作的基本要求:原子性(atomicity)
具体实现:
比较常见的操作:检查记录X是否存在,如果不存在,插入X
(3),避免死锁
加锁不当,会带来死锁。
问题是:要在编码阶段识别出所有的锁(共享资源);实际情况是,只能是尽可能识别出有限的锁。
如果要对多个资源加锁,必须按照相同的顺序加锁(比如,我们规定按照数据名称的A-Z顺序加锁)。
举例:
A 占用 R1,请求 R2
B 占用 R2, 请求 R1
买家:拿着钱,要货
卖家:拿着货,要钱
如果互不相让,陷入僵局(死锁)
死锁的场景在交通拥堵的时候比较常见
开车的同学,应该见过有待单位门口或者路口有矩形的黄色禁入区。