多线程的知识点是一个庞大的体现, 对此也是一知半解. 一直想系统的深入的学习多线程的知识, 奈何一直没有找到机会, 好吧, 其实就是懒. 最近在项目中接触到一个多并发的项目, 在项目中踩了无数的坑. 在此下定决心做一个并发的学习笔记.
为什么并发会有安全问题
当两个线程同时对一个共享可变变量进行操作时, 例如:
两个线程对变量 i=1 同时执行 i++ 操作. 执行完毕后 i 可能并不等于 3 而是等于 2. 因为 i++ 不是原子性的操作, i++ 实际上是有三个步骤
第一步: 读取, 从主内存中将 i=1 读取到本地内存中.
第二步: 修改, i 自增.
第三部: 写入, 将 i=2 写会到缓存中.
所以当两个线程同时将 i 读取到工作内存中, 并分别将变量 i 赋值为 2.
原子性
原子性是指一个操作是不可中断的, 要么全部执行成功要么全部执行失败, 有着 "同生共死" 的感觉. 及时在多个线程一起执行的时候, 一个操作一旦开始, 就不会被其他线程所干扰.
可见性
可见性是指当一个线程修改了共享变量后, 其他线程能够立即得知这个修改. 为什么要这样说? 难道一个线程修改了共享变量其他线程不一定会立即得知这个变量的修改? 没错事实确实如此.
简单的举一个例子.
数据 i 是存储在主内存中的, 当一个线程执行 i++ 操作的时候首先将 i 从主内存读取到自己线程的工作内存中(也就是缓冲行), 然后将工作内存的 i 执行 + 1 操作. 如果是单线程程序, 在没有其他写入操作的情况下读取这个值, 首先会读取缓冲行, 缓存命中. 那么总能得到 +1 操作之后的值.
但是多线程环境结果则会违背我们的直觉.
由于操作系统的执行, 我们并不知道工作内存中的值何时才能被写入到主内存中(理由很简单, 我们不可能每次修改了缓存, 操作系统就会将值瞬间刷入到主内存吧? 这样效率会多低呀). 所以如果这之前另一个线程从主内存读取 i 的值到本地工作内存中. 那么他可能并不会感知到另一个线程其实已经修改了 i 的值.
为什么 synchronized 和 volatile 可以实现可见性我们在后续会继续介绍.
如何避免并发问题
1. 不在线程之间共享该状态变量.
2. 将状态变量修改为不可变的变量.
3. 在访问状态变量时使用同步.
Synchronized
在多线程并发中 synchronized 一直是元老级别的角色. 利用 synchronized 来实现同步具体有一下三种表现形式:
对于普通的同步方法, 锁是当前实例对象.
对于静态同步方法, 锁是当前类的 class 对象.
对于同步方法块, 锁是 synchronized 括号里配置的对象.
当一个代码, 方法或者类被 synchronized 修饰以后. 当一个线程试图访问同步代码块的时候, 它首先必须得到锁, 退出或抛出异常的时候必须释放锁. 那么这样做有什么好处呢?
它主要确保多个线程在同一时刻, 只能有一个线程处于方法或者同步块中, 它保证了线程对变量的可见性和排他性.
- int i = 1;
- public sychronized void increment(){
- i++;
- }
在前面介绍过 i++ 并不是原子操作, 所有当多个线程同时操作 i++ 的时候可能会出现多线程并发问题. 而上诉代码块中 i++ 是在 synchronized 修饰的方法中. 其中一个线程进入该方法首先获得当前实例对象的锁, 当另一个线程试图执行该方法的时候, 由于前一个线程并没有执行完毕释放掉锁, 所以该线程挂起等待锁的释放.
通过加锁的方式我们实现了将 i++ 非原子操作的方法变成了原子操作的方法. 从而实现了排他性.
如下图所示, 一个普通的方法会有一个左右摆动的开关, 可以连接到任意一个线程, 如果该方法不是原子性的, 那么可能方法并没有执行完毕就会链接到另一个方法. 而被 synchronized 修饰的方法, 链接到一个线程后, 除非这个线程将方法执行完毕或者抛出异常, 开关才会链接至别的线程. 就这样将一个并行的操作变了穿行
无论 synchronized 关键字加在方法上还是对象上, 如果它作用的对象是非静态的, 则它取得的锁是对象; 如果 synchronized 作用的对象是一个静态方法或一个类, 则它取得的锁是对类, 该类所有的对象同一把锁.
每个对象只有一个锁 (lock) 与之相关联, 谁拿到这个锁谁就可以运行它所控制的那段代码.
实现同步是要很大的系统开销作为代价的, 甚至可能造成死锁, 所以尽量避免无谓的同步控制.
Synchronized 的原理
synchronized 用的锁是存在 java 对象头里的.
锁一共有 4 个状态, 级别从低到高依次是: 无所状态, 偏向锁状态, 轻量级锁状态和重量级锁状态. 对于这四种状态《java 并发编程艺术》讲解的特别好, 有兴趣可以看看这本书的介绍.
总体来说, 通过对象头存储的记录指针, 互斥指正, 标志位等信息来说明当前是对象是被某个线程所使用的, 此时别的线程想要获取这把锁要么 cas 自旋要么线程阻塞.
来源: https://www.cnblogs.com/zhxiansheng/p/10611994.html