目录
状态
线程的意义
多线程带来的风险——线程安全✅
线程安全的概念
线程不安全的原因
抢占式执行,随机性调度
修改共享数据
原子性->加
可见性
指令重排序
解决线程不安全问题(学完线程再总结)
synchronized关键字——监视器锁monitor lock编辑
互斥
使用示例
可重入
Java标准库中线程安全的类
死锁
1.死锁是什么
2.死锁的三个典型情况
3.死锁的四个必要条件编辑
4.如何破除死锁?
状态是针对当前的线程调度情况进行描述的,所以认为线程是程序调度的基本单位(后面再谈状态都是考虑线程的状态了);
public class ThreadState { public static void main(String[] args) { for (Thread.State state : Thread.State.values()) { System.out.println(state); } } }
1. NEW : 创建Thread对象,但是还没调用start(内核还没创建对应的PCB);
2. TERMINATED : 内核中的PCB已经创建完毕了,但是Thread对象还在;
3. RUNNABLE:可运行的;(RUNNABLE是正在CPU上执行的,RUNNING是在就绪队列上的,可以去CPU上执行的);
4. WAITING
5. TIMED_WAITING
6. BLOCKED
上述三个都是阻塞,表示线程 PCB 正在阻塞队列中,但是原因不同
RUNNABLE表示就绪状态,包含正在CPU上运行,或者在就绪队列中排队 ;
当有for循环里有sleep的时候,t运行中的状态大部分是TIMED_WAITING, 为了让时间均衡,可以给 t 线程中添加更多的计算逻辑。
如果线程代码中全是for循环里计算,比较大小之类的操作,此时这个线程就不会阻塞,始终是RUNNABLE'状态;
所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着 的。
程序分成:
CPU密集:包含大量的加减乘除运算符;
IO密集:涉及到读写文件,读写控制台,读写网络的操作;
多线程可以更充分的利用到多核心cpu的资源
起源于多线程的抢占式执行带来的随机性。如果没有多线程,程序的执行顺序就是固定的;调度的源头来自于操作系统的内核实现
多个线程同时对某个共享资源进行访问导致的原子性,可见性,有序性的问题,这些问题导致共享资源存在一个不可预测性,使执行结果出现不可预期的结果。
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线 程安全的。
多个线程同时修改一个变量
string都是不可变对象,所以他线程安全
是不可拆分的基本单位,针对线程安全问题最主要的的手段就是通过加锁将非原子的转换为“原子”的,我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。一条 java 语句不一定是原子的,也不一定只是一条指令比如 n++ ,其实是由三步操作组成的:1. 从内存把数据读到 CPU2. 进行数据更新3. 把数据写回到 CPU不保证原子性会给多线程带来什么问题如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是 错误的。这点也和线程的抢占式调度密切相关 . 如果线程不是 " 抢占 " 的 , 就算没有原子性 , 也问题不大 .
如果一个线程读,一个线程改,也可能出现问题。
编译器对于指令重排序的前提是 " 保持逻辑不发生变化 ". 这一点在单线程环境下比较容易判断 , 但是在多线程环境下就没那么容易了 , 多线程的代码执行复杂程度更高 , 编译器很难在编译阶段对代码的执行效果进行预测 , 因此激进的重排序很容易导致优化后的逻辑和之前不等价 .
- 同步代码块
- 同步方法
- 锁
- 当两个线程同时对一个对象进行加锁的时候,会出现锁冲突/锁竞争,一个线程可以获取到锁另一个线程出现线程阻塞的情况 ,直到那个线程解锁,它才取锁成功。
- 两个线程,一个加锁,一个不加锁,不存在锁竞争;
- 两个线程针对不同对象加锁,俩线程获取到各自的锁,此时不存在锁竞争;
同一个对象 synchronized 就会 阻塞等待 .进入 synchronized 修饰的代码块 , 相当于 加锁退出 synchronized 修饰的代码块 , 相当于 解锁synchronized 用的锁是存在 Java 对象头里的。
synchronized关键字
修饰方法(普通方法-加到this对象上,静态方法-加到类对象上)
代码块-手动指定加到某个对象上
ctrl+alt+t->选中代码块包裹
一个线程针对同一个对象,连续加锁两次如果没有问题, 就叫可重入的,否则不可重入(第二次加锁就会出现阻塞等待的情况,这种情况线程就僵住了,出现死锁现象)。
这里的死锁(死锁的一种情况)如何理解?
灵异事件:滑稽老铁去厕所,锁上了门,结果时空错乱,它出来了,忘记了去厕所这件事,去厕所发现门一直都是锁的。
为了避免上述死锁,java将synchronized设定为可重入的。
上述图片可以理解为:
我追到男神,追的过程中给他表白了一次,追到手后再表白了一次,就是“可重入的”,不会出现阻塞等待等。
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
eg:
static class Counter { public int count = 0; synchronized void increase() { count++; } synchronized void increase2() { increase(); } }
在可重入锁的内部 , 包含了 " 线程持有者 " 和 " 计数器 " 两个信息 .1. 如果某个线程加锁的时候 , 发现锁已经被人占用 , 但是恰好占用的正是自己 , 那么仍然可以继续获取 到锁, 并让计数器自增 .2. 解锁的时候计数器递减为 0 的时候 , 才真正释放锁 . ( 才能被别的线程获取)
多个线程操作同一个集合类,就要考虑到线程安全问题。
还有的类不涉及加锁,但仍然是线程安全的,因为不涉及修改操作:String;
1.死锁是什么
死锁是两个或两个以上的线程在执行过程中,去争夺同一个共享资源导致的互相等待的过程,在没有外部干预的情况下,线程会一直处于阻塞状态,无法向下执行。
死锁比较隐蔽,一旦写了死锁,会出现严重的bug
2.死锁的三个典型情况
- 一个线程,一把锁,连续锁两次,如果锁是不可重入性锁,就会死锁;Java里的synchronized和ReentrantLock都是可重入性锁;
- 循环等待:两个线程,两把锁,t1,t2先各自针对锁A,锁B进行加锁,再尝试获取对方的锁。线程1在获取B的时候等待线程2释放B,线程2在获取A的时候等待线程1释放A,造成blocked;(线程1拿到A锁,再尝试获取锁B,A这把锁还是继续保持的,不会因为要去获取B就把A释放了)
public class demo33 { public static void main(String[] args) { Object jiangyou=new Object(); Object cu=new Object(); Thread tanglaoshi=new Thread(()->{ synchronized(jiangyou){ try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (cu){ System.out.println("汤老师拿到了酱油和醋"); } } }); Thread shiniang=new Thread(()->{ synchronized(cu){ try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou){ System.out.println("师娘拿到了酱油和醋"); } } }); } } //用jconsole定位到这两个线程的第二个操作出现了BLOCKED操作;
- 多个线程,多把锁 当他们都拿起左手的筷子就会出现极端情况。
3.死锁的四个必要条件
4.如何破除死锁?
去破坏死锁中的必要条件中的任意一个(除了互斥条件),给锁编号,然后制定一个固定的顺序(从小到大)来加锁,任意线程加多把锁的时候,都让线程遵循上述循序,此时循环等待自然破除。eg:对于上面的代码就可以通过把锁的内容换一下 。
还有解决死锁的银行家算法。
对于死锁,我们需要借助jconsole这样的工具来定位。 在问到死锁时,记得再解释下可重入,不可重入