Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立。下面逐个看下哪些操作实现这三个特性:
由Java内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store和 write 这六个,我们可以大致认为,基本数据类型的访问、读写都是具备原子性(例外就是 long 和 double 的非原子性协定),当然如果应用场景需要更大的范围来保证原子性,可以使用 synchronized 关键字,在 synchronized 块之间的操作也具备原子性。
所谓的可见性就是指当一个线程修改了共享变量的值时,其他线程能够立马知道这个修改。Java内存模型是通过在变量修改后将新值台同步回主内存,在变量读取之前从主内存刷新变量值这种依赖主内存作为传递中介的方式来实现可见性!不论是普通变量还是 volatile 变量都是一样。然后 volatile 和普通变量的区别在于,volatile 变量可以保证新值能立刻同步主内存,每次使用都是拿到最新的值。
另外 synchronized 和 final 也可以实现可见性。
如果在同一个线程内,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指:线程内表现为串行的语义;后半句:是指 指令重排序和工作内存与主内存同步延迟现象。
先来看一段线程不安全的代码
class Counters{
public int count = 0;
public void add(){
count++;
}
public int getCount(){
return count;
}
}
public class Demo22 {
public static void main(String[] args) throws InterruptedException {
Counters counters = new Counters();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
counters.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
counters.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counters.getCount());
}
}
class Counter{
public int count = 0;
synchronized public void add(){
count++;
}
public int getCount(){
return count;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
counter.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
此时输出结果为20000,此时保证了,当t1线程在访问synchronzed方法时,t2线程并不能访问。这就保证了操作的原子性! 另外要注意,当一个线程正在访问一个对象的synchronized实例方法,那么其他线程并不能访问该对象的其他synchronized方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,但是其他线程可以访问该实例对象的非synchronized方法。
相当于给类加锁,会作用于这个类的所有对象实例
当一个线程A调用实例对象的非静态 synchronized方法,而线程B调用实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥对象!
因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用锁是当前实例对象的锁
class Counter {
public static int count = 0;
synchronized public static void add() {
count++;
}
public int getCount() {
return count;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Counter counter1 = new Couter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
counter.add();
System.out.println(counter.count);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
couter1.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
class Counter {
Object locker = new Object();
public int count = 0;
public void add() {
synchronized (locker) {
count++;
}
}
public int getCount() {
return count;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
修饰代码块时候,可以在()里面填写任意对象!由于在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,但是需要同步的代码块又是一小部分,所以此时就可以用修改代码块的方式加锁!
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的代码块或者方法在任意时刻都只能有一个线程执行。
先看一段代码:
public class Demo23 {
public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("循环结束! t1 结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
public class Demo23 {
volatile public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("循环结束! t1 结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
总结:volatile 不保证原子性! 适用的场景,一个线程读,一个线程写!
可以看一段伪代码
Map configOptions;
char[] configText;
// 此变量必须定义为 volatile
volatile boolean initialized = fasle;
// 假设以下代码在线程A中执行
// 模拟读取配置信息,当读取完成后
// 将initialized设置为true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;
// 假设以下代码在线程B执行
// 等待initialized为true,代表线程A已经把配置信息初始化
while(!initialized){
sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();
可以试想一下,如果定义的 initialized 没有被 volatile修饰,就可能会因为指令重排序的优化,导致线程A中最后一行代码被提前执行,这样在线程B中使用配置信息就会可能出现错误,而volatile关键字就可以避免此类情况发生!
如果有错误,请留言指正~~
本文参考资料:
《深入理解Java虚拟机》
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- kqyc.cn 版权所有 赣ICP备2024042808号-2
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务