学生网站建设首页,建设内部网站目的,龙口网站设计,网站扫码怎么做这里是Themberfue 在上一节的最后#xff0c;我们讨论两个线程同时对一个变量累加所产生的现象 在这一节中#xff0c;我们将更加详细地解释这个现象背后发生的原因以及该如何解决这样类似的现象 线程安全问题
public class Demo15 {private static int count 0;public … 这里是Themberfue · 在上一节的最后我们讨论两个线程同时对一个变量累加所产生的现象 · 在这一节中我们将更加详细地解释这个现象背后发生的原因以及该如何解决这样类似的现象 线程安全问题
public class Demo15 {private static int count 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int i 0; i 50000; i) {count;}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {count;}});// 线程安全问题// 会照成 线程不安全问题// t1.start();// t2.start();// t1.join();// t2.join();// 改为串行执行t1.start();t1.join();t2.start();t2.join();System.out.println(count);}
} · 我们先回顾上述代码如果两个线程并发执行逻辑同时累加 count 变量 100,000 次后得到的结果是一个随机值且这个随机值一定小于 100,000 · 如果改为串行执行就是 t1 执行完后t2 再度执行那么 count 的结果就为 100,000 · 为什么会产生这样的现象 · 我们先从一行代码入手count有的人就会问这有什么好分析的这不就是一个count 1的操作吗没错的确是这样 · 众所周知CPU中央处理器执行的是一系列指令这些指令定义了它需要执行的逻辑操作这些指令的集合统称为指令集指令集有两种一种是..... 再讲就串台了这是计算机组成原理的知识哦 · 常见的指令就有逻辑指令算术指令等那么一个 count 其实分为三个指令操作因为它还设计到变量的修改而不是单纯地加法 · 我们都知道把大象放进冰箱分三步把把冰箱门打开把大象装进去再把冰箱门关上 · count 也分为三步操作 1. load把内存中 count 的值加载到 cpu 寄存器 2. add把寄存器中的 count 的值 1 3. save把寄存器中的 count 的值保存到内存中 PS寄存器就是CPU处理日常任务的小工具用来存放临时信息 · 操作系统对线程的调度的是随机的所以在执行这三条指令时可能不是一口气全部执行完毕而是执行了一半就不执行了而后又执行了 · 比如执行 指令1 后被调度走调度回来后执行 指令2 指令3 · 比如执行 指令1 指令2 后被调度走调度回来后执行 指令3 · 比如执行 指令1 后被调度走调度回来后执行 指令 2 后被调度走调度回来后执行指令3 · 多线程的随机调度是造成这个bug出现的原因 · 上述为简单模拟了一遍两次 count 的大概流程 · 这是最为理想的情况就是三条指令一次性执行完毕后再去执行下三条指令但实际情况却不能保证每次发生这种理想的情况 · 上述情况才是经常发生的也是导致bug的主要原因 · 尽管执行了两次 count 操作但内存中保存的值为1结果只增值了一次 · 产生上述问题的原因就是线程安全问题 · 根本原因就是操作系统对于线程的调度是随机的也就是抢占式执行这个策略在最初诞生多线程任务操作系统时就诞生了是非常伟大的发明后世的操作系统都是这个策略 · 第二个原因就是多个线程修改同一个变量 如果是一个线程修改一个变量不会产生上述问题 如果是多个线程修改不同变量同样的 如果是多个线程不是同时修改同一个变量同样的 如果是多个线程同时读取一个变量同样的 · 第三个原因就是修改的操作不是原子的如果是 count 是一条指令就可以执行完毕那么认为该操作就是原子的 · 内存可见性问题 · 指令重排序 · 后续再讨论其细节 加锁 · Java中解决线程安全问题的最主要的方案就是给代码块加锁通过加锁可以让不是原子的操作打包成一个原子的操作 · 计算机中的锁操作和生活中的加锁区别不大都是互斥排他。例如你上厕所对当前这个厕所间加锁那么别人就不能进这个厕所间了你出厕所门时此时就是解锁 · 通过使用锁对先前的 count 操作就可以将其变为原子的在加上锁后count 的三个指令就会完整执行完毕后才可能被调度走 · 加锁操作不是讲线程锁死到CPU上禁止这个线程被调度走是禁止其他线程重新加这个锁避免其他线程的操作在这个线程执行过程中插队 · 加锁和解锁这个操作本身是操作系统提供的 api但是很多语言都对其单独进行了封装大多数的封装风格都是采取这两个函数 Object.lock();// 执行的代码逻辑Object.unlock(); · 但是这样写的弊端也很大不能保证每次加上锁后都会记住去解锁所以 Java 提供了一种更为简洁的方式去给某个代码块上锁 synchronized {// 执行的代码逻辑} · 只要进入了代码块进入 { 后就会加上锁只要出了代码块出去 } 后就会自动解锁 · 但在上述伪代码中synchronized 的使用并不正确单纯地加锁但是此时另一个线程又要加锁我们要怎么判断这个锁有没有被使用锁又不止一个 · 所以应该这样使用 synchronized (Object) {// 执行的代码逻辑} · 没错括号里填写的就是用来加锁的对象这个对象一般称为锁对象作为锁的作用去使用 · Object 表示一个类Java 的所有对象都可以作为锁对象 Object locker new Object();synchronized (locker) {count;
} public class Demo16 {private static int count 0;public static void main(String[] args) throws InterruptedException {// Java中任何一个对象都可以作为锁Object locker new Object();Thread t1 new Thread(() - {for (int i 0; i 50000; i) {// 对count的操作进行上锁// loadaddsave操作执行完才会调度走synchronized (locker) {count;}}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {synchronized (locker) {count;}}});// 只有两个线程针对一个对象加锁才会产生互斥效果// 一个线程被上了锁另一个线程得阻塞等待直到第一个线程解锁t1.start();t2.start();t1.join();t2.join();System.out.println(count count);}
} · 单独对一个线程上一个锁是不能发挥锁的作用的 · 只有两个线程针对一个对象加锁才会产生互斥效果 · 一个线程被上了锁另一个线程就得阻塞等待直到那个线程解锁才会继续向下执行 · 运行上述代码count 的结果恒为 100,000不可能出现其他值也就解决了该代码逻辑的线程安全问题 · 通过加锁操作count 操作的三个指令相当于合并成了一个指令保证每个线程从内存中获取到的值是正确的 · 并不是加上了 synchronized 就一定保证线程安全得要正确地使用锁在该使用的时候使用锁在对的地方使用锁 · 比如在这个案例中不是对 count 操作操作而是在 for 循环开始前就上锁 synchronized (locker) {for (int i 0; i 50000; i) {count;}
} · 这样虽然也是上了锁但是没什么意义就相当于等到 for 循环逻辑全部结束后再解锁另一个线程停止等待拿到锁 · 因为这两个线程就这一个相同逻辑所以这么写就相当于变成了串行执行不是并发了 · 采取 synchronized 的加锁方式就可以确保一定会释放锁不会遇到加锁后但是没有解锁的情况 · 除此之外synchronized 还可以修饰方法对这个方法加锁 class Counter {private int count;// 使用 synchronized 对方法进行上锁就相当于是针对this上锁synchronized public void addCount() {// synchronized (this) {this.count;// }}// 使用 synchronized 对静态方法进行上锁就相当于是针对类对象上锁反射public synchronized static void func () throws ClassNotFoundException {synchronized (Class.forName(Counter)) {System.out.println(func);}}public synchronized static void fuc () {synchronized (Counter.class) {System.out.println(fuc);}}public int getCount() {return count;}
}public class Demo17 {public static void main(String[] args) throws InterruptedException {Counter counter new Counter();Thread t1 new Thread(() - {for (int i 0; i 50000; i) {counter.addCount();}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {counter.addCount();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count counter.getCount());}
} · 我们如果查看 StringBuffer 类的方法也可以看到类似的操作 · 下一节我们会更加深入多线程了解到死锁等相关概念 · 毕竟不知后事如何且听下回分解~~