一 、进程&线程&基本的线程机制#
- 进程是运行在自己的地址空间内的自包容程序(一个程序至少包含一个进程,而一个进程至少包含一条线程),多任务操作系统周期性的将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: 多线程的中重排序问题#
举个例子,假设多个线程并发访问下面两个方法
boolean 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于该线程的任意后续操作
int a=1; // 1 int b =2; //2 int c =3; //3
在上面代码中 1happens-before 2 3
- 监视器规则
- 对于同一个锁的解锁,总是 happens-before于 随后对这个锁的加锁
private ReentrantLock lock = new ReentrantLock(); public void read(){ lock.lock(); //do something ... lock.unlock(); //1 解锁 } public void write(){ lock.lock(); // 加锁 //do something ... lock.unlock();
如上, 1的解锁,后跟着2的加锁
- volatile变量规则
- 对一个volatile域的写,happens-before于任意后续对这个变量的读
- 传递性
- A happens-before B ; B happens-before C; 那么 A happens-before C
- Start 规则(线程启动规则)
- 如果ThreadA里面 启动了ThreadB,那么ThreadB.start() happens-before于线程B中的任意操作
- Join 规则(线程终止规则)
- 和Start相反,线程中的所有操作,都优先发生于 对线程的终止检验, Thread.jion(); Thread.isAlive()
- 线程的中断规则:
- interrupt()优先发生于 被中断线程的代码,检测到中断事件的发生
- 对象终结规则
- 一个对象的初始化完成,先行于它的finalize()方法
happens-before 和 重排序的区别与联系#
两个操作具有happens-before关系,并不意味着前一个操作一定要在后一个操作之
前执行,(假如两个操作没有数据依赖那么可能会被编译器处理器进行指令的重排序),他只是要求前一个操作的执行结果对后一个操作是可见的(也就是前一个操作不一定先开始,但是它一定要比后一个操作先结束)让其他线程看到结果,前一个操作肯定要把执行的结果,从他自己的缓存中刷回到内存
三. 什么是锁#
- 锁是工具,是作为并发共享数据,保证数据一致性的工具
前言:锁的内存语义#
- 锁的获取与释放建立的happens-before关系
锁存在于对象的哪里?#
存在于对象头中
对象头中的信息
Mark Word:存储对象的hashset值,锁信息
Class MetaData Address:存储对象所属类的位置
Array Length : 数组对象特有的标记数组的长度
1 内置锁#
java中每一个对象都可被当作同步的锁,这些锁就叫做内置锁,例如 Synchronized修饰方法,获取到this对象锁,修饰静态方法,获取到Class类锁,同步代码块里面可以设置this对象锁,非this对象锁等等
2 互斥锁(排它锁)和共享锁#
前者指该锁一次性只能被一条线程占有,后者表示该锁一次性可以被多条线程占有synchronized和ReentarntLock都是互斥的,而ReentrantReadWriteLock中的读锁,是共享的,读读共享
3 偏向锁/轻量级锁/重量级锁#
这三种锁是指锁的状态,并且是针对synchronized,java5通过引入锁的升级来实现高效的synchronized,这三种锁的状态,是通过对象监视器在对象头中的字段来区分
- 偏向锁是指一段同步代码块一直被一条线程锁访问,那么以后该线程就会自动的获取锁,来降低获取锁的代价
- 轻量级锁是指,在偏向锁的基础上,出现其他线程来访问此代码,偏向锁升级为轻量级锁,其他线程通过自旋,尝试获取锁
运行流程:
当前线程获取到锁,修改锁对象头里面的Mark Word里面的锁标志位,然后去执行同步代码体,这时候,其他的线程也对象头信息复制到虚拟机栈,企图去更改锁标志位,但是上一个线程没有释放,他就不停的尝试去修改,直到对象锁被释放了为止
- 当锁是轻量级锁的时候,其他线程来访问代码,会自旋,但是当它自旋到一定次数之后还是没有获取到锁,就阻塞.轻量级锁也就转换成重量级锁
4. 重入锁#
可重入锁,有叫递归锁,就是说某一个线程运气比较好,拿到锁之后,在释放之前,再次拿到了这个锁,而且不会被锁阻塞
- ReentrantLock就是一把可重入的锁
/* * 经典的验证 锁的重复问题 ,在单一的线程下, a() 想在 未释放锁 的前提下 调用b(),前提就是可冲入锁 * */ public void a() { lock.lock(); System.out.println("a"); b(); lock.unlock(); } public void b() { lock.lock(); System.out.println("b"); lock.unlock(); }
实例二: synchronized也是一把重入锁
public class safe{ synchronized public void method01(){ .... method02(); } synchronized public void method02(){ .... } }
- method01() 和 method02 () 都是线程安全的,假如当前线程拿到对象锁后,在执行method01()时,碰到了method(),他可以重复拿到锁,而不会被阻塞!
- 容易和Synchronized方法,或者代码块的特性混淆:两条线程分别竞争执行method01()和method02() ,无论哪条线程正在执行 Synchronized方法也好,同步代码块也好,另一条线程都不能执行其他任意Synchronized方法或者代码块
- 其中Synchronized 和 locked 都是可重入锁!
- 如过多个线程使用多个锁对象,一定是一部执行,锁不住线程!
5. 自旋锁#
- 所谓自旋锁,实际上就是在空转cup的时间片,while(true) 抢到cup的执行权,却不做任何事,while(true){},等着其他线程把cup的执行权抢走!
6. 死锁#
- 当一个线程永远持有一把锁还不释放,其他线程一直在等待...
public class siSuo { Object a = new Object(); Object b = new Object(); public void method01(){ synchronized (a){ try{ Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } synchronized(b){ System.out.println("method1执行了"); } } } public void method02(){ synchronized (b){ synchronized(a){ System.out.println("method2执行了"); } } } public static void main(String[] args) { siSuo siSuo = new siSuo(); new Thread(()->{ siSuo.method01(); }).start(); new Thread(()->{ siSuo.method02(); }).start(); } }
7. 公平锁#
Lock锁分为公平锁和非公平锁,所谓公平锁,就是表示线程获取锁的顺序,是按照线程加锁的顺序来实现的,也就是FIFO的顺序先进先出的顺序,而非公平锁描述的则是一种锁的随机抢占机制,还可能会导致一些线程根本抢不着锁而被饿死,结果就是不公平了
8. 乐观锁和悲观锁#
- 就像生活中乐观的人,什么事都往好处想,因此它每次拿到数据之后呢,都认为别人不会来修改它的值,也就是读写不互斥,但是为了安全,他需要在更新数据之前判断一下,有没有人修改过,java.util.Concurrent.atomic包下的原子类,就是乐观锁的实现方法cas完成的
- 就像生活中悲观的人,什么事都往坏处想,因此它在每次拿数据的时候,都会加上锁阻塞住其他的线程,因为它总是想其他线程肯定回来修改它拿到的数据,传统的关系型数据库中就大量的使用了悲观锁,如行锁,表锁,读锁,写锁等等,Synchronized 和 ReentrantLock都是悲观锁思想的实现
9. 分段锁#
分段式锁是一种设计理念分段锁的设计目的就是细化锁的颗粒度,当操作不需要操作整个数组的时候,仅仅获取它想要的那一段数据的锁就可以,而其他线程仍然可以获取其他段的锁的数据的内容
- ConcurrentHashMap就是通过分段式锁来实现的高效并发Map集合,它的分段式锁叫segment(分片),每一段内部有一个Entry数组,数组中的每一个元素即是一个链表,也是一个ReentrantLock,因此当我们往里面put的时候,只需要获取到此段的锁就可以,实现了并行put,同时gut()无锁