石家庄网站建设哪家好,深圳广胜达建设公司,北京智能网站建设平台,网络公司具体是干什么的文章目录 并发编程中的三个问题可见性原子性有序性 了解Java内存模型JMMsynchronized 保证三大特性synchronized 保证原子性synchronized 保证可见性synchronized 保证有序性 synchronized 的特性可重入特性不可中断特性 通过反汇编学习synchronized原理当修饰代码块时当修饰方… 文章目录 并发编程中的三个问题可见性原子性有序性 了解Java内存模型JMMsynchronized 保证三大特性synchronized 保证原子性synchronized 保证可见性synchronized 保证有序性 synchronized 的特性可重入特性不可中断特性 通过反汇编学习synchronized原理当修饰代码块时当修饰方法时 通过JVM源码学习synchronizedmonitor 监视器锁monitor竞争monitor等待monitor释放 并发编程中的三个问题
可见性
是指一个线程对共享变量进行修改另一个先立即得到修改后的最新值
演示可见性问题 实际效果线程 1 并没有退出循环说线程二修改了flag的值而线程一并没有立即得到修改后的值。 原子性
原子性Atomicity在一次或多次操作中要么所有的操作都执行并且不会受其他因素干扰而中断要么所有的操作都不执行。
演示原子性问题 执行几次后的实际结果 证明
number 实际上是一个复合操作且是非原子性的它包含三个步骤
读取 number 的当前值。将读取的值加1。将结果写回到 number。
再通过反汇编说明number的问题
javap -p -v .\AtomicityDemo
其中对于number 而言number 为静态变量实际会产生如下的 JVM 字节码指令 number 一共由于4条命令构成
getstatic获取到当前 number 的值iconst_1准备常量 1iadd当前值与 iconst_1 做加法putstatic将结果赋值给 number
以上多条指令在一个线程的情况下是不会出问题的但是在多线程环境下就可能会出现问题。比如一个线程在执行 13: iadd 时另一个线程又执行 9: getstatic。会导致两次 number实际上只加了1。
有序性
是指程序代码在执行过程中的先后顺序由于Java在编译期以及运行期的优化导致了代码的执行顺序未必就是开发者编写代码时的顺序。
举个例子
int a 1; // 操作1
int b 2; // 操作2// 重排序后
int b 2; // 操作2
int a 1; // 操作1出现可见性问题的两个前提至少有两个线程、有个共享变量
public class Ordering {private int num 0;private boolean flag false;private int x;public void action1() {if (flag) {x num num;} else {x 1;}// x的可能结果System.out.println(x);}public void action2() {num 2;flag true;}
}x的可能结果
结果1线程1执行 action1()此时 flagfalsex的结果为1结果2线程2先执行了 action2()线程1再执行 action1()此时 flagtrue, num2x的结果为4结果3java在编译和运行时会对代码进行优化action2()的执行顺序变成了如下此时线程2更改flag值之后CPU切换到线程1执行num为初始化的值0x的结果为0
public void action2() { // 因为第2行和第3行代码并没有逻辑关系 Java在编译期以及运行期的优化 可能会将其改变顺序flag true;num 2;
}上面的结果3就是有序性产出的并发问题
了解Java内存模型JMM
Java内存模型Java Memory Model, JMM是Java虚拟机(JVM)定义的一种规范用于描述多线程程序中变量包括实例字段、静态字段和数组元素如何在内存中存储和传递的规则。规范了线程何时会从主内存中读取数据、何时会把数据写回主内存。
JMM 抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中。
JMM 的核心目标是确保多线程环境下的可见性、有序性和原子性从而避免由于硬件和编译器优化带来的不一致问题。
可见性确保一个线程对变量的修改能及时被其他线程看到。关键字 volatile 就是用来保证可见性的它强制线程每次读写时都直接从主内存中获取最新值。有序性指线程执行操作的顺序。JMM允许某些指令重排序以提高性能但会保证线程内的操作顺序不会被破坏并通过 happens-before 关系保证跨线程的有序性。原子性是指操作不可分割线程不会在执行过程中被中断。例如synchronized 关键字能确保方法或代码块的原子性。
从 JMM 了解可见性线程 1 将 a 的值拷贝一份并修改了 a 的值然后同步给主内存的值而线程 2 一直用的是副本的值并不知道主内存的值已被修改了所以线程 1 修改的值对于线程 2 来说是不可见的。
synchronized 保证三大特性
synchronized能够保证在同一时刻最多只有一个线程执行该段代码以达到保证并发安全的效果。
synchronized (锁对象) {
// 受保护资源;
}synchronized 保证原子性
synchronized保证原子性的原理synchronized保证只有一个线程拿到锁能够进入同步代码块。
案例演示:5个线程各执行1000次 i;
public class Test01Atomicity {private static int number 0;public static void main(String[] args) throws InterruptedException {Runnable increment () - {for (int i 0; i 1000; i) {synchronized (Test01Atomicity.class) {number;}}};ArrayListThread ts new ArrayList();for (int i 0; i 50; i) {Thread t new Thread(increment);t.start();ts.add(t);}for (Thread t : ts) {t.join();}System.out.println(number number);}
}对number;增加同步代码块后保证同一时间只有一个线程操number;。就不会出现安全问题。
synchronized 保证可见性
案例演示一个线程根据boolean类型的标记flag while循环另一个线程改变这个flag变量的值另一个线程并不会停止循环。
public class Test01Visibility {// 多个线程都会访问的数据我们称为线程的共享数据private static boolean run true;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {while (run) {// 增加对象共享数据的打印println是同步方法System.out.println(run run);}});t1.start();Thread.sleep(1000);Thread t2 new Thread(() - {run false;System.out.println(时间到线程2设置为false);});t2.start();}
}点进去println本质还是加了 synchronized 关键字 synchronized 保证有序性
为什么要重排序
为了提高程序的执行效率编辑器和cpu会对程序中的代码进行重排序
as-if-serial语义
as-if-serial语义的意思是不管编译器和CPU如何重排序必须保证在单线程情况下程序的结果是正确的。 以下数据有依赖关系不能重排序。 写后读 int a 1;
int b a;写后写 int a 1;
int a 2;读后写 int a 1;
int b a;
int a 2;synchronized 后虽然进行了重排序保证只有一个线程会进入同步代码块也能保证有序性。
synchronized 保证有序性的原理加s ynchronized 后依然会发生重排序只不过我们有同步代码块可以保证只有一个线程执行同步代码中的代码。保证有序性。
synchronized 的特性
可重入特性
synchronized内部维护了一个计数器 recursions 变量记录线程是第几次获取锁在执行完同步代码块时计数器的数量会 - 1 直到计时器的数量为 0就释放这个锁。 好处1可以避免死锁 2可以让我们更好的来封装代码
不可中断特性
不可中断特性一个线程获得锁后另一个线程想要获得锁必须处于阻塞或等待状态如果第一个线程不释放锁第二个线程会一直阻塞或等待不可被中断。
public class Demo02_Uninterruptible {private static final Object obj new Object();public static void main(String[] args) throws InterruptedException {// 1.定义一个RunnableRunnable run () - {// 2.在Runnable定义同步代码块synchronized (obj) {String name Thread.currentThread().getName();System.out.println(name 进入同步代码块);// 保证不退出同步代码块try {Thread.sleep(888888);} catch (InterruptedException e) {e.printStackTrace();}}};// 3.先开启一个线程来执行同步代码块Thread t1 new Thread(run);t1.start();Thread.sleep(1000);// 4.后开启一个线程来执行同步代码块(阻塞状态)Thread t2 new Thread(run);t2.start();// 5.停止第二个线程System.out.println(停止线程前);t2.interrupt();System.out.println(停止线程后);System.out.println(t1.getState());System.out.println(t2.getState());}
}执行结果
t1 线程不释放锁t2 线程会一直阻塞或等待既不可被中断。 通过反汇编学习synchronized原理
它的实现原理依赖与 JVM 中的 Monitor监视器锁和对象头Object Header。
当修饰代码块时
会在代码块的前后插入monitorenter和monitorexit字节码指令可以把monitorenter理解为加锁monitorexit理解为解锁。
第二次出现 monitorexit 可以理解为出现异常的情况也需要解锁。 monitorenter
synchronized 的锁对象会关联一个 monitor这个 monitor 不是我们主动创建的是 JVM 的线程执行到这个同步代码块发现锁对象没有monitor 就会创建 monitormonitor 内部有两个重要的成员变量owner: 拥有 这把锁的线程recursions 会记录线程拥有锁的次数当一个线程拥有 monitor 后其他线程只能等待。
monitorexit
能执行 monitorexit 指令的线程一定是拥有当前对象的 monitor 的所有权的线程。执行 monitorexit 时会将 monitor 的进入数减 1。当 monitor 的进入数减为 0 时当前线程退出 monitor不再拥有 monitor 的所有权此时其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor的所有权。
当修饰方法时 方法的常量池会添加一个 ACC_SYNCHRONIZED 标识当某个线程访问这个方法时会检查是否有ACC_SYNCHRONIZED标识若有则需要获取监视器锁才可以执行方法此时就保证了方法的同步。
通过JVM源码学习synchronized
monitor 监视器锁
在 HotSpot 虚拟机中 monitor 是通过 ObjectMonitor 实现的底层是 c 实现的其数据结构和解释如下 monitor竞争
通过CAS尝试把 monitor 的 owner 字段设置为当前线程。如果设置之前的 owner 指向当前线程说明当前线程再次进入monitor即重入锁执行 recursions 记录重入的次数。如果当前线程是第一次进入该 monitor设置 recursions 为 1_owner 为当前线程该线程成功获 得锁并返回。如果获取锁失败则等待锁的释放。
monitor等待
当前线程被封装成ObjectWaiter对象node状态设置成ObjectWaiter::TS_CXQ。在for循环中通过 CAS 把 node 节点 push 到 _cxq 列表中同一时刻可能有多个线程把自己的 node 节点 push 到_cxq列表中。node节点push到_cxq列表之后通过自旋尝试获取锁如果还是没有获取到锁则通过park将当 前线程挂起等待被唤醒。当该线程被唤醒时会从挂起的点继续执行通过ObjectMonitor::TryLock 尝试获取锁。
monitor释放
当某个持有锁的线程执行完同步代码块时会进行锁的释放给其它线程机会执行同步代码在 HotSpot中通过退出monitor的方式实现锁的释放并通知被阻塞的线程。
退出同步代码块时会让_recursions 减 1当 recursions 的值减为0时说明线程释放了锁。根据不同的策略由QMode指定从cxq或EntryList中获取头节点通过 ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程唤醒操作最终由unpark完成。