拉呱:这是第一篇并发的博客,在后续的并发博文中,我会尽力整理出较全的关于并发的知识点,先却分开两个概念,并发与高并发就是多线程操作相同资源时如何保证数据安全,线程安全以及合理利用资源.和它仅一字只差的是高并发,高并发是指服务能够处理很多请求,比如12306的抢票,处理不好,会降低用户的体验度,甚至是服务器宕机
一 进程&线程&基本的线程机制
- 进程是运行在自己的地址空间内的自包容程序(一个程序至少包含一个进程,而一个进程至少包含一条线程),多任务操作系统周期性的将CPU在进程之间切换,来实现同时运行多个程序,尽管进程的运行歇歇停停,但是CPU的运算速度太快了,以至于给人一种进程一直运行而没有停的假象.
- windows系统中的一个 .exe 的程序,实际上就是一个进程
- 线程就是进程中的一个单一的顺序执行流,因此单个进程可以拥有多个线程并发执行任务(底层的实现机制是切分CPU的时间片段),CPU给每个任务轮流的分配其执行的时间,以至于每个任务都觉得自己一直占用CPU.
- 线程一个理解为进程中的一个子任务
QQ就是一个进程,和好友视频聊天可以理解成一个线程
- 线程一个理解为进程中的一个子任务
当然,如果程序确实运行在多核的机器上,那么有可能真的是在同时运行
- 好处: 可以使我们从单个线程这个层次抽身出来,而多任务和多线程也是使用多处理器系统的最合适的方式
尽管JAVASE5在并发中做出了显著的改进,但是仍然没有编译器验证和检查型异常(?)
二 线程带来的风险
2.1活跃性问题:
死锁
哲学家问题,当哲学家们都不肯把手里的筷子借给其他人,最后的结果就是全部饿死
饥饿
排队打饭,假设所有人都来这一个窗口排队打饭,打完饭也不走,可能就会导致比较瘦弱的女生吃不上饭而饥饿(反应在线程的优先级问题上)
- 高优先级吞噬所有低优先级的时间片
- 设置线程的优先级 setPrioriry(int newPriority)
线程被永久的堵塞在进入同步块的状态
等待的线程永远不会被唤醒
- 活锁
独木桥问题,相互谦让,导致最后谁都过不去
2.2安全性问题
- 原子性:访问互斥,同一时刻只允许一个线程对它进行操作为线程安全
- 可见性:一条线程对主内存的修改可以及时的被其他线程看到
- 有序性:一个线程观察其他线程中指令的执行顺序,由于指令重排序的存在,一般它们看到的结果都是杂乱无序的
非线程安全&线程安全
多个线程对同一个实例对象中的实例变量进行并发访问,产生的后果就是脏读(读取到了被更改的数据)–存在非线程安全问题
获取到的对象的实例是经过同步处理的,不会出现脏读的现象–线程安全
说到线程的安全性问题,和重排序和happens-before法则是紧密相关的
2.3性能问题
多线程速度一定会快吗?
关于性能,是具有多面性的,多线程不一定快,单核的处理器也可以实现多线程,就像烤烧饼,CPU分配给各个线程的时间片很短,但是来回的切换是有成本的但是并发通常是提高运行在单核处理器上的程序的性能,表面上看CPU在多个线程上进行切换很浪费时间,但是阻塞是这个问题变得不同,大多数情况下是因为IO或者进行过一项很复杂的计算,如果没有并发,整个程序都将会停止下来
什么是重排序?
1:重排序的定义
重排序就是编译器,处理器,在不改变程序执行结果的前提下,重新排序指令的执行顺序,以达到最佳的运行效果
2:分类
- 编译器重排序
- 处理器重排序
3: 什么是数据依赖
数据依赖指的是,某些指令存在某种先后关系,比如相邻的两行执行都访问通同一个变量,并且其中一个指令执行了写操作,那么,这两行指令就存在数据依赖,换言之,执行的顺序不能改变,否者得出错误的结果
因此,编译器和处理器仅仅对没有数据依赖的指令进行重排序
指令 | 实例 |
---|---|
读后写 | a=b ; b=1; |
写后读 | a=1; b=a; |
写后写 | a=1; a=2; |
4: 什么是as-if-serial?
在单线程的开发中程序员不需要知道指令是如何进行重排序的,只是简单认为程序是按顺序执行就行,故 意为:貌似是串行的
5: 多线程的中重排序问题
举个例子,假设多个线程并发访问下面两个方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16boolean flag;
private int a;
public void read(){
a=1;
flag=true;
}
public void write(){
if(flag){
int b = a+1;
System.out.println("b=="+b);
}
}
上面的代码,a=1,flag=ture;显然没有依赖关系,因此可能会被重排序成flag=true; a=1;这时候就会出现问题,当执行到fl,ag=ture,cpu的执行权被另一个线程抢去执行write(),write()里面的输出语句输出的不再是2,而是其他意想不到的值
6: 多线程中重排序问题的解决方法
- 同步,给上面那两个方法加上锁,同一时刻,只允许一个持有该锁线程去访问同步方法,等它执行完释放锁后,其他线程才能去访问
什么是happens-before?
1: 定义:
- happens-before 用来指定两个操作之间的执行顺序,提供跨线程的内存可见性
- 在java的内存模型中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必然存在happens-before的关系
规则
- 程序顺序规则
- 单个线程中的每个操作,总是前一个操作happens-before于该线程的任意后续操作
1 | int a=1; // 1 |
在上面代码中 1happens-before 2 3
- 监视器规则
- 对于同一个锁的解锁,总是 happens-before于 随后对这个锁的加锁
1 | private ReentrantLock lock = new ReentrantLock(); |
实例二: synchronized也是一把重入锁
1 | public class safe{ |
method01() 和 method02 () 都是线程安全的,假如当前线程拿到对象锁后,在执行method01()时,碰到了method(),他可以重复拿到锁,而不会被阻塞!
容易和Synchronized方法,或者代码块的特性混淆:两条线程分别竞争执行method01()和method02() ,无论哪条线程正在执行 Synchronized方法也好,同步代码块也好,另一条线程都不能执行其他任意Synchronized方法或者代码块
其中Synchronized 和 locked 都是可重入锁!
如过多个线程使用多个锁对象,一定是一部执行,锁不住线程!
5. 自旋锁
- 所谓自旋锁,实际上就是在空转cup的时间片,while(true) 抢到cup的执行权,却不做任何事,while(true){},等着其他线程把cup的执行权抢走!
6. 死锁
- 当一个线程永远持有一把锁还不释放,其他线程一直在等待…
1 | public class siSuo { |
7. 公平锁
Lock锁分为公平锁和非公平锁,所谓公平锁,就是表示线程获取锁的顺序,是按照线程加锁的顺序来实现的,也就是FIFO的顺序先进先出的顺序,而非公平锁描述的则是一种锁的随机抢占机制,还可能会导致一些线程根本抢不着锁而被饿死,结果就是不公平了
8. 乐观锁和悲观锁
就像生活中乐观的人,什么事都往好处想,因此它每次拿到数据之后呢,都认为别人不会来修改它的值,也就是读写不互斥,但是为了安全,他需要在更新数据之前判断一下,有没有人修改过,java.util.Concurrent.atomic包下的原子类,就是乐观锁的实现方法cas完成的
就像生活中悲观的人,什么事都往坏处想,因此它在每次拿数据的时候,都会加上锁阻塞住其他的线程,因为它总是想其他线程肯定回来修改它拿到的数据,传统的关系型数据库中就大量的使用了悲观锁,如行锁,表锁,读锁,写锁等等,Synchronized 和 ReentrantLock都是悲观锁思想的实现
9. 分段锁
分段式锁是一种设计理念分段锁的设计目的就是细化锁的颗粒度,当操作不需要操作整个数组的时候,仅仅获取它想要的那一段数据的锁就可以,而其他线程仍然可以获取其他段的锁的数据的内容
ConcurrentHashMap就是通过分段式锁来实现的高效并发Map集合,它的分段式锁叫segment(分片),每一段内部有一个Entry数组,数组中的每一个元素即是一个链表,也是一个ReentrantLock,因此当我们往里面put的时候,只需要获取到此段的锁就可以,实现了并行put,同时gut()无锁