深圳网站设计哪家公司好,视觉网络网站,专科网页设计实训报告,谷歌英文网站推广文章目录 多线程带来的风险-线程安全线程不安全的举例分析产出线程安全的原因#xff1a;1.线程是抢占式的2. 多线程修改同一个变量#xff08;程序的要求#xff09;3. 原子性4. 内存可见性5. 指令重排序 总结线程安全问题产生的原因解决线程安全问题1. synchronized关键字… 文章目录 多线程带来的风险-线程安全线程不安全的举例分析产出线程安全的原因1.线程是抢占式的2. 多线程修改同一个变量程序的要求3. 原子性4. 内存可见性5. 指令重排序 总结线程安全问题产生的原因解决线程安全问题1. synchronized关键字的介绍监视器锁 monitor locka.锁的概念b.synchronized的特性c.synchronized的用法d.针对上述问题加synchronied解决问题分析底层逻辑e.关于synchronized的总结f.不同锁对象的情况g.如何判断多个线程竞争的是不是同一把锁h.可重入锁I.锁对象 Java 标准库中的线程安全类不安全类安全类 volatile 关键字解决线程安全的问题内存可见性实例CPU层面保证可见性Java层面保证指令顺序从而保证内存可见性总结 wait() 和 notify()wait和notify的基础知识wait()方法notify()⽅法notifyAll()⽅法wait 和 sleep和join的对⽐⾯试题wait和notify的总结 多线程带来的风险-线程安全
线程不安全的举例 场景 用两个线程对同一个变量分别自增5万次预期结果和自增结果是一个累加和10万次。 public static int count;public static void main(String[] args) {Counter counter new Counter();Thread t1 new Thread(()-{for(int i0;i5_0000;i){counter.count();}});Thread t2 new Thread(()-{for(int i0;i5_0000;i){counter.count();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Count: counter.count);}public void count(){{count1;}} } 执行结果 程序运行得到的结果与预期的结果值不一样而且是一个错误的结果而且我们程序的逻辑是正确的这个现象所表现的问题称为线程安全问题 分析产出线程安全的原因
1.线程是抢占式的 线程是抢占执行的执行顺序是随机的 由于线程的执行顺序无法为人控制抢占式执行是造成线程安全问题的主要罪魁祸首而且我们解决不了完全是CPU自己调度而且和CPU的核数有关 2. 多线程修改同一个变量程序的要求 单个线程修改同一个变量不会产生线程安全问题 多个线程修改不同的变量不会产生线程安全问题 多个线程修改同一个变量会产生线程安全问题 3. 原子性 什么是原⼦性 我们把⼀段代码想象成⼀个房间每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证A进⼊房间之后还没有出来B 是不是也可以进⼊房间打断 A 在房间⾥的隐私。这个就是不具备原⼦性那我们应该如何解决这个问题呢是不是只要给房间加⼀把锁A 进去就把⻔锁上其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。 有时也把这个现象叫做同步互斥表⽰操作是互相排斥的。⼀条 java 语句不⼀定是原⼦的也不⼀定只是⼀条指令 比如上面的count对应的是多条CPU指令 1从内存或者寄存器读取count值 LOAD 2执行自增 ADD 3把计算结果写回寄存器或者内存 STORE不保证原⼦性会给多线程带来什么问题 如果⼀个线程正在对⼀个变量操作中途其他线程插⼊进来了如果这个操作被打断了结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原⼦性, 也问题不⼤. 4. 内存可见性 什么是内存可见性 一个线程对共享变量进行了修改其他线程能感知到变量修改后的值。Java内存模型JMM java虚拟机规范定义了Java内存模型 ⽬的是屏蔽掉各种硬件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到⼀致的并发效果. 分析Java内存模型 1.工作内存和线程之间是一一对应的 2.java的共享变量都在主内存里面java线程线程首先从主内存读取变量的值到自己的工作内存 3.每个线程都有自己的工作内存且线程工作内存直接是相互隔离的 4.线程在工作内存修改完变量的值后又从工作内存把变量的值刷回主内存里面。 5.在以上执行count操作由于两个线程在执行每个线程都有自己的工作内存且相互不可见最终导致了线程安全问题。线程对共享变量的修改线程之间相互感知不到注意 为什么整这么多内存 实际并没有这么多 “内存”. 这只是 Java 规范中的⼀个术语, 是属于 “抽象” 的叫法所谓的 “主内存” 才是真正硬件⻆度的 “内存”. ⽽所谓的 “⼯作内存”, 则是指 CPU 的寄存器和⾼速缓存 为啥要这么⿇烦的拷来拷去? 因为 CPU 访问⾃⾝寄存器的速度以及⾼速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是⼏千倍, 上万倍).那为什么不全部用寄存器原因很简单太贵了。关于JMM内存模型的面试题JMM规定 1.所以线程不直接修改主内存中的共享变量 2.如果修改共享变量需要把这个变量从主内存复制到自己的工作内存中修改完之和再刷回主内存 3.各个线程之间不能相互通信做到了内存级别的线程隔离。 5. 指令重排序 1.什么是指令重排序 我们写的代码在编译之后可能与代码对应的指令顺序不同这个过程就是指令重排序JVM层面可能重排序CPU执行指令也可能重排序 1.一段代码是这样的 a.代阳去教室取英语书 b.代阳去食堂吃饭 c.代阳去教室去数学书 在单线程情况下,JVM,CPU指令集会对其优化执行顺序按a–c–b的方式执行也是没有问题可以少跑一次教室这就叫指令重排序 编译器对于指令重排序的前提是 “保持逻辑不发⽣变化”. 这⼀点在单线程环境下⽐较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执⾏复杂程度更⾼, 编译器很难在编译阶段对代码的执⾏效果进⾏预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价 总结线程安全问题产生的原因
线程是抢占式执行的
CPU的调度问题硬件层面我们解决不了
多个线程修改同一个变量
在真实业务场景中使用多线程就是为了提升效率在并发编成中这个需求是满足的
原子性
指令是在CPU上执行怎么才能让CPU在执行时实现原子性这个可能可以解决
内存可见性
java层面应该可以解决进程之间可以进行通信那那么在线程中应该也有这样的机制让线程在内存中也可以彼此感知
指令重排序
对于代码来说谁的优先级高我们可以通过某种方式告诉编译器不要对我的代码进行重排序
总结以上12我们不能改变但是345我们可以进行改变只要满足345中的一条或者多条线程安全问题就可以解决
解决线程安全问题
1. synchronized关键字的介绍监视器锁 monitor lock
a.锁的概念
比如线程A拿到了锁别的线程如果要执行被锁住的代码那就要等到线程A释放锁之后如果A没有释放锁那么别的线程只能阻塞等待这个状态就是BLOCK
b.synchronized的特性
互斥synchronized会引起互斥效果某个线程执行到某个对象的synchronized时其他线程如果也执行到同一个对象sychronized就会阻塞等待保证了原子性通过加锁实现保证了内存可见性通过串行执行实现不保证有序性
c.synchronized的用法
修饰方法
修饰非静态方法默认锁对象是this当前对象修饰静态方法默认锁对象是本身类 修饰代码块 可以充当锁对象的是实例对象new出来的对象类对象this
d.针对上述问题加synchronied解决问题分析底层逻辑 public synchronized void count(){ // 修饰代码块加锁synchronized(this){ // count1; // }synchronized(this){count1;}} } 如果修饰方法其实把方法进行了串行化处理 如果修饰的是代码块其实把修饰代码块的内容进行了串行话处理。对于部分类似这种要修改共享变量的情况进行串行话其他代码模块继续并行执行这样就可以提高效率 画图分析 注意的点 t1释放锁之后也可能第二次还是t1先于t2拿到锁因为线程是抢占式执行的不一定是t2 由于线程在执行逻辑之前要拿到锁当拿到锁时上一个线程已经执行完所有的指令并把修改的值刷新会主内存所有当前线程永远读到的是上一个线程执行完后的值 synchronized保证了原子性 因为当前线程永远拿到的是前一个线程修改后的值所有这样也现象上实现了内存可见性但是并没有真正对内存可见性做出技术上的处理。 synchronized没有保证有序性不会禁止指令重排序 e.关于synchronized的总结
被synchronized修身的代码块会编成串行执行synchronized可以修饰方法或者代码块被修饰的代码并不是一次性在CPU执行完而是中途可能会被CPU调度走当所有指令执行完后才会释放锁只给一个线程加锁也会出现线程安全
f.不同锁对象的情况
同·一个引用调用静态和非静态两个方法一个用synchronized修饰一个不用synchronized不修饰只加一把锁
public static int count;public static void main(String[] args) {Counter_Demo1 counter new Counter_Demo1();Thread t1 new Thread(()-{for(int i0;i5_0000;i){counter.count();}});Thread t2 new Thread(()-{for(int i0;i5_0000;i){counter.count1();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Count: counter.count);}public void count(){//修饰非静态代码块synchronized(this){count1;}}public void count1(){{count1;}}
//修饰非静态方法
// public void synchronized count(){
// {
// count1;
// }
// }
// public void count1(){
// {
// count1;
// }
// }
修饰静态的方法和代码块
// public static void count(){
// //修饰代码块
// //静态方法里面不能用this
// synchronized(Counter_Demo1.class){
// count1;
// }
// }
// public static void count1(){
// {
// count1;
// }
// }
// public synchronized static void count(){
// //修饰代码块
// //静态方法里面不能用this
// {
// count1;
// }
// }
// public static void count1(){
// {
// count1;
// }
// }执行结果 都不符合预期
两个引用调用同一个方法锁对象是实例对象new public static int count;public static void main(String[] args) {Counter_Demo1 counter1 new Counter_Demo1();Counter_Demo1 counter2 new Counter_Demo1();Thread t1 new Thread(()-{for(int i0;i5_0000;i){counter1.count();}});Thread t2 new Thread(()-{for(int i0;i5_0000;i){counter2.count();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Count: counter1.count);}Object objectnew Object();public void count(){synchronized (object){count1;}}执行结果不符合预期 用类对象来加锁
static Object object new Object();public void count(){synchronized (object){count1;}}执行结果符合预期 结论
只要一个线程A获得锁没有锁竞争线程A和线程B共同抢一把锁谁先拿到锁就先执行谁另一个线程就要阻塞等待等到持有锁的线程释放锁之后再竞争锁线程A与线程B抢的不是同一把锁它们之间没有竞争关系分别去拿到自己的锁不存在锁关系
g.如何判断多个线程竞争的是不是同一把锁
实例对象new出来的对象每个都是单独存在类中的属性类没有用static修饰的变量每个实例对象都是不同的类中的静态成员变量用static修饰属于类对象全局唯一类对象:.class文件加载jvm之后的对象全局唯一线程之间是否存在锁竞争关键是看访问的是不是同一个锁对象如果是则存在锁竞争如果不是则不存在锁竞争
h.可重入锁 对同一个锁对象和同一个线程如果可以重复加锁称之为不互斥称之为可重入。对同一个锁对象和同一线程如果不可以重复加锁称之为互斥就会形成死锁。已经获取锁对象的线程如果再多次进行加锁操作不会产生互斥现象 I.锁对象
锁对象记录了获取锁的线程信息任何对象都可以做锁对象java中每个对象都是由以下几个部分组成 – 1.markword – 2.类型指针 – 3.实例数据 – 4.对齐填充 – 5对象默认带线啊哦是16byte
Java 标准库中的线程安全类
不安全类
ArraylistLinkedListHashMapTreeMapHashSetTreeSetStringBuilder
安全类
-Vector不推荐 HashTable不推荐 CocurrentHashMap StringBuffer String 虽然没有加锁但是不涉及修饰仍然是线程安全的
volatile 关键字
解决线程安全的问题
真正意义上解决了内存可见性解决了指令重排序禁止指令重排序问题没有解决原子性问题
内存可见性
我们都知道实际工作时候访问的数据都是工作内存里面的数据这样是为了保证效率但是这样有时候会产生安全问题。但是加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了代码在写⼊ volatile 修饰的变量的时候 – 改变线程⼯作内存中volatile变量副本的值 – 将改变后的副本的值从⼯作内存刷新到主内存代码在读取volatile修改的变量时候 –从主内存中读取volatile变量的最新值到线程的⼯作内存中 –从⼯作内存中读取volatile变量的副本
实例
static class Counter {public volatile int flag 0;}public static void main(String[] args) {Counter counter new Counter();Thread t1 new Thread(() - {while (counter.flag 0) {// do nothing}System.out.println(循环结束!);});Thread t2 new Thread(() - {Scanner scanner new Scanner(System.in);System.out.println(输⼊⼀个整数:);counter.flag scanner.nextInt();});t1.start();t2.start();}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug)
//static class Counter {
// public volatile int flag 0;
//}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束.对于线程t1来说只是比较flag这个变量的值从来都没有修改过所有认为这个值永远也不会改变从而也不会重新从主内存中读取值cpu为了提升高运行效率这个值一般存在寄存器或者cpu的缓存中 在多线程环境下就会出现出现这个问题一个线程修改了另一个线程无法感知到的变量
CPU层面保证可见性
MESI缓存 一致协议可以理解是一种通知机制
Java层面保证指令顺序从而保证内存可见性
内存屏障作用是保证指令执行的顺序从而保证内存可见性 volatile写 volatile读 有序性用volatile 修改过的变量由于前后有内存屏障保证了指令的执行顺序也可以理解为告诉编译器不要进行指令重排序。
总结
volatile不保证原子性
public static volatile int count;public synchronized static void main(String[] args) {Counter counter new Counter();Thread t1 new Thread(()-{for(int i0;i5_0000;i){counter.count();}});Thread t2 new Thread(()-{for(int i0;i5_0000;i){counter.count();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Count: counter.count);}public void count(){count1;}volatile保证可见性MESI缓存 一致协议可以理解是一种通知机制 volatile保证有序性内存屏障作用是保证指令执行的顺序从而保证内存可见性
wait() 和 notify()
wait和notify的基础知识
wait() 和notify(),notifyAll()是object方法wait()/wait(long timeout):让线程进入等待的线程notify()/notifyAll():唤醒在当前对象上等待的线程
wait()方法
wait做的事情 – 使当前执行代码的线程进行等待把线程放到等待队列中 – 释放当前锁 – 满足一定条件被唤醒尝试重新获得这个锁 – wait要搭配sychronized来使用脱离sychronized使用wait会之间抛出异常wait 结束等待的条件 – 其他线程调用 调用该对象的notify方法 – wait等待时间超时(wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间) – 其他线程调用该等待线程的interrupted方法导致wait抛出InterruptedException 异常.wait()方法代码使用
public static void main(String[] args) throws InterruptedException {Object object new Object();System.out.println(等待中);synchronized (object) {object.wait(1000);}System.out.println(等待结束);}这样在执⾏到object.wait()之后就⼀直等待下去那么程序肯定不能⼀直这么等待下去了。这个时候就
需要使⽤到了另外⼀个⽅法唤醒的⽅法notify()。notify()⽅法
notify ⽅法是唤醒等待的线程. – 方法notify()也要在同步方法或者同步代码块中执行该方法是用来通知哪些可能等待该对象的对象锁的其他线程对其发出通知并使它们重新获取该对象对象锁 – 如果由多个线程等待则有线程调度器随机挑选出一个呈现wait状态的线程。并没有 “先来后到”) – 在notify()方法后当前线程不会立马释放该对象锁需要等到notify方法线程将程序执行完也就是退出同步代码块之后才会释放锁对象。使用notify()方法唤醒线程
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker locker;}Overridepublic void run() {synchronized (locker){try {System.out.println(等待开始) ;locker.wait();System.out.println(等待结束);} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker locker;}Overridepublic void run() {synchronized (locker) {System.out.println(notify 开始);locker.notify();System.out.println(notify 结束);}}}public static void main(String[] args) throws InterruptedException {Object locker new Object();Thread t1 new Thread(new WaitTask(locker));Thread t2 new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();}notifyAll()⽅法
notify⽅法只是唤醒某⼀个等待线程. 使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程. static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker locker;}Overridepublic void run() {synchronized (locker) {try {System.out.println(等待开始);locker.wait();System.out.println(等待结束);} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker locker;}Overridepublic void run() {synchronized (locker) {System.out.println(notify 开始);locker.notifyAll();System.out.println(notify 结束);}}}public static void main(String[] args) throws InterruptedException {Object locker new Object();Thread t1 new Thread(new WaitTask(locker));Thread t3 new Thread(new WaitTask(locker));Thread t4 new Thread(new WaitTask(locker));Thread t2 new Thread(new NotifyTask(locker));t1.start();t3.start();t4.start();sleep(1000);t2.start();}}
注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执⾏, ⽽仍然是有先有后的执⾏
wait 和 sleep和join的对⽐⾯试题
wait需要搭配synchronized使用 sleepjoin不需要wait是Object的方法sleep是Thread的静态方法join是类中的方法实例方法一个是用于线程之间的通信的两个是让线程阻塞一段时间相同点可以让线程放弃执行一段时间
wait和notify的总结 join和wait是两个不同的操作 –join是Thread类中的方法 – wait和notify是Object类中的方法 – join状态主线程要等待子线程的结果 – wait是等待另一个线程的资源wait和notify必须跟synchronized一起使用并且使用同一个对象 – 否则会报错 – wait的线程进入阻塞状态调用wait的线程会释放自己持有的锁不再占有cpu资源notify()和notifyAll() – notify随机唤醒一个线程notifyAll唤醒所有线程唤醒后的线程需要重新去竞争锁拿到锁之后wait位置的代码才会继续执行。使用小结 – wait和notify必须搭配synchronized一起使用 – wait和notify使用的锁对象必须是同一个 – notify执行多少次都没有关系及时没有wait类似老板把包子做好空喊了一声举例 – 现实举例 – 指令举例