作者:老男孩标兄_164 | 来源:互联网 | 2024-09-24 18:10
内容多有疏漏,有问题欢迎提出
目录
- java内存模型的概念
- 原子性(Atomicity)
- 可见性(Visibility)
- 有序性(Ordering)
- 总结
java内存模型的概念
java并发程序开发比串行程序开发复杂的多,其中重要的一点就是要保证数据的正确性。对于串行来讲,输入是1,输出一定是1,这个很好保证,但是对于并行开发来讲,中间如果没有做好数据在多线程中的控制,很有可能导致输入是,输出可能是1,也可能是111。java内存模型(JMM)就是为了保证数据在多线程中正确性的一种协议。
其中,JMM最关键的技术点都是围绕着多线程的原则性、可见性和有序性来建立。下面,我们重点来讨论多线程中的这三大特性。
原子性(Atomicity)
原子性指一个操作是不可中断的,即要么全部执行成功要么全部执行失败,不接受一半成功一半失败的情况。
int a = 10; //1
a++; //2
以上两种场景,只有第一种是原则性操作,第二种虽然看起来是一步执行完成,但是实际上他执行了
- 读取a的值
- 对a进行加1的操作
- 把加1后的值赋给变量
在开发中,常用的保证原子性的操作即是加上sychronized的锁,加锁之后的效果就是把锁住的类、方法、对象作为一个整体,在该模块处理完之前,不允许其他操作调用该模块。
另外一种锁--volatile只能可见性和有序性,不能保证原子性,所以在下面例子中,使用volatile进行数据一致性的操作是有问题的:
public class VolatileExample {private static volatile int counter &#61; 0;public static void main(String[] args) {for (int i &#61; 0; i <10; i&#43;&#43;) {Thread thread &#61; new Thread(new Runnable() {&#64;Overridepublic void run() {for (int i &#61; 0; i <10000; i&#43;&#43;)counter&#43;&#43;;}});thread.start();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(counter);}
}
如果volatile可以保证原子性的话&#xff0c;实际输出结果应该是100000&#xff0c;但是真实的结果远小于100000。
然而想要保证上面例子的结果输出正确的话&#xff0c;我们可以做这样的修改&#xff1a;
public class VolatileExample {private static volatile int counter &#61; 0;public static void main(String[] args) {for (int i &#61; 0; i <10; i&#43;&#43;) {Thread thread &#61; new Thread(new Runnable() {&#64;Overridepublic void run() {for (int i &#61; 0; i <10000; i&#43;&#43;)//在counter&#43;&#43;操作时加上synchronized锁synchronized (VolatileExample.class){counter&#43;&#43;;}}});thread.start();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(counter);}
}
或者使用AtomicInteger这样线程安全的类进行变量赋值的操作。
import java.util.concurrent.atomic.AtomicInteger;public class Atomicity {//使用保证的原子性的AtomicInteger类进行counter变量的修改操作static AtomicInteger counter &#61; new AtomicInteger();private static void count() {Atomicity atomicity &#61; new Atomicity();for (int i &#61; 0; i <10; i&#43;&#43;) {Thread thread &#61; new Thread(new Runnable() {&#64;Overridepublic void run() {for (int i &#61; 0; i <10000; i&#43;&#43;)//addAndGet是线程安全的修改方法counter.addAndGet(1);}});thread.start();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(atomicity.counter);}public static void main(String[] args) {count();}
}
这样写的话&#xff0c;实际的输出结果就是100000。
可见性&#xff08;Visibility&#xff09;
可见性是指当一个线程修改了某一个共享变量的值时&#xff0c;其他的线程是否能够立即知道这个修改。
即当CPU1一个线程修改了一个全局变量a&#xff0c;并将其缓存在cache中或者寄存器里&#xff0c;在此同时CPU2上的某一线程也对全局变量a进行修改&#xff0c;也将其及存到cache或寄存器里&#xff0c;这样&#xff0c;两个线程对于全局变量a的值可能存在不一样的情况&#xff0c;从而导致接下来的数据处理可能会异常。
对此&#xff0c;sychronized和volatile关键字都可以保证操作的可见性&#xff0c;即当某一全局变量一旦被修改&#xff0c;就会立刻被刷新到主内存中去&#xff0c;保证多线程中数据的一致性。
有序性&#xff08;Ordering&#xff09;
在多线程执行过程中&#xff0c;一般理解代码会按顺序从前到后依次执行&#xff0c;但是出于某种原因&#xff0c;在某些情况下&#xff0c;程序会对指令重新排序&#xff0c;导致程序操作结果异常。在单一线程中&#xff0c;指令执行的顺序一定是一致的&#xff08;否则程序无法正常工作&#xff09;&#xff0c;只有在并行的程序中&#xff0c;才会发生指令重排序的情况。但是总结性的来讲&#xff0c;指令的重排序不是无意义的&#xff0c;他的目的是为了尽量少的中断流水线&#xff0c;提高程序的性能。
对于哪些指令不能重拍&#xff0c;也有既定的规则&#xff0c;即Happen-Before规则&#xff1a;
- 程序顺序原则&#xff1a;一个县城内保证语义的串行性&#xff1b;
- volatile规则&#xff1a;volatile变量的写咸鱼度发生&#xff0c;这保证了volatile变量的可见性&#xff1b;
- 锁规则&#xff1a;解锁&#xff08;unlock&#xff09;必然发生在随后加锁&#xff08;lock&#xff09;之前&#xff1b;
- 传递性&#xff1a;A先于B&#xff0c;B先于C&#xff0c;那么A必然先于C&#xff1b;
- 线程的start&#xff08;&#xff09;方法先与他的每一个动作&#xff1b;
- 线程的所有操作先于线程的终结&#xff08;Thread.join()&#xff09;&#xff1b;
- 线程的中断&#xff08;interrupt()&#xff09;先于被中断线程的代码&#xff1b;
- 对象的构造函数的执行&#xff0c;结束先于finalize()方法&#xff1b;
总结
本章说明了了java内存模型&#xff08;JMM&#xff09;是保证多线程中数据正确性的基础协议&#xff0c;并对多线程中原子性、可见性、有序性这三大特性进行了分析。下一章的内容&#xff0c;我们会从java线程讲起&#xff0c;阐述线程从创建到终止的一些列操作。