JMM ialah model memori Java. Oleh kerana terdapat perbezaan tertentu dalam capaian memori di bawah pengeluar perkakasan yang berbeza dan sistem pengendalian yang berbeza, pelbagai masalah akan berlaku apabila kod yang sama dijalankan pada sistem yang berbeza. Oleh itu, Model Memori Java (JMM) melindungi perbezaan akses memori pelbagai perkakasan dan sistem pengendalian untuk mencapai kesan konkurensi yang konsisten untuk program Java pada pelbagai platform.
Model memori Java menetapkan bahawa semua pembolehubah disimpan dalam memori utama, termasuk pembolehubah contoh dan pembolehubah statik, tetapi tidak termasuk pembolehubah tempatan dan parameter kaedah. Setiap benang mempunyai memori kerjanya sendiri Memori kerja benang menyimpan pembolehubah yang digunakan oleh utas dan salinan memori utama Operasi benang pada pembolehubah semuanya dilakukan dalam memori kerja. Benang tidak boleh terus membaca atau menulis pembolehubah dalam ingatan utama.
Benang yang berbeza tidak boleh mengakses pembolehubah dalam memori kerja masing-masing. Pemindahan nilai pembolehubah antara benang perlu diselesaikan melalui ingatan utama.
Memori kerja setiap utas adalah bebas Data operasi benang hanya boleh dilakukan dalam ingatan kerja dan kemudian dibuang semula ke memori utama. Ini ialah cara asas benang berfungsi seperti yang ditakrifkan oleh model memori Java.
Peringatan hangat, sesetengah orang di sini akan salah faham model memori Java sebagai struktur memori Java, dan kemudian menjawab soalan tentang timbunan, tindanan dan pengumpulan sampah GC Pada akhirnya, ia adalah jauh daripada soalan penemuduga ingin bertanya. Malah, apabila ditanya tentang model memori Java, mereka secara amnya ingin bertanya soalan berkaitan multi-threading dan konkurensi Java.
Atomicity
Pewawancara mengambil pen dan menulis sekeping kod bolehkah baris kod berikut menjamin atomicity?
int i = 2; int j = i; i++; i = i + 1;
Ayat pertama ialah operasi penugasan jenis asas, yang mestilah operasi atom.
Ayat kedua membaca nilai i dahulu, dan kemudian menetapkannya kepada j. Operasi dua langkah ini tidak dapat menjamin keatoman.
Ayat ketiga dan keempat sebenarnya setara dengan nilai i, kemudian +1, dan akhirnya tetapkan nilai kepada i Ia adalah operasi tiga langkah dan tidak dapat menjamin atomicity.
JMM hanya boleh menjamin atomicity asas Jika anda ingin memastikan atomicity blok kod, ia menyediakan dua arahan bytecode, monitorenter dan moniterexit, yang merupakan kata kunci yang disegerakkan. Oleh itu operasi antara blok yang disegerakkan adalah atom.
Keterlihatan
Selain kata kunci yang tidak menentu, muktamad dan disegerakkan juga boleh mencapai keterlihatan.
Prinsip disegerakkan ialah selepas pelaksanaan selesai dan sebelum memasuki buka kunci, pembolehubah yang dikongsi mesti disegerakkan ke memori utama.
Medan akhir yang diubah suai, setelah pemulaan selesai, akan kelihatan kepada utas lain jika tiada objek terlepas (bermaksud objek boleh digunakan oleh utas lain selepas pemulaan selesai).
Keteraturan
Kata kunci yang tidak menentu menggunakan halangan memori untuk melarang penyusunan semula arahan untuk memastikan keteraturan.
Prinsip disegerakkan ialah selepas utas dikunci, ia mesti dibuka kunci sebelum utas lain boleh dikunci semula, supaya blok kod yang dibalut disegerakkan dilaksanakan secara bersiri antara berbilang utas.
Tiga dan lapan jenis operasi interaksi memori
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
我再补充一下JMM对8种内存交互操作制定的规则吧:
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
不允许线程将没有assign的数据从工作内存同步到主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
很多并发编程都使用了volatile关键字,主要的作用包括两点:
保证线程间变量的可见性。
禁止CPU进行指令重排序。
volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
volatile保证可见性的流程大概就是这个一个过程:
先说结论吧,volatile不能一定能保证线程安全。
怎么证明呢,我们看下面一段代码的运行结果就知道了:
public class VolatileTest extends Thread { private static volatile int count = 0; public static void main(String[] args) throws Exception { Vector<Thread> threads = new Vector<>(); for (int i = 0; i < 100; i++) { VolatileTest thread = new VolatileTest(); threads.add(thread); thread.start(); } //等待子线程全部完成 for (Thread thread : threads) { thread.join(); } //输出结果,正确结果应该是1000,实际却是984 System.out.println(count);//984 } @Override public void run() { for (int i = 0; i < 10; i++) { try { //休眠500毫秒 Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } count++; } } }
为什么volatile不能保证线程安全?
很简单呀,可见性不能保证操作的原子性,前面说过了count++不是原子性操作,会当做三步,先读取count的值,然后+1,最后赋值回去count变量。需要保证线程安全的话,需要使用synchronized关键字或者lock锁,给count++这段代码上锁:
private static synchronized void add() { count++; }
首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。
为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:
指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。
所以在多线程环境下,就需要禁止指令重排序。
volatile关键字禁止指令重排序有两层意思:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
下面举个例子:
private static int a;//非volatile修饰变量 private static int b;//非volatile修饰变量 private static volatile int k;//volatile修饰变量 private void hello() { a = 1; //语句1 b = 2; //语句2 k = 3; //语句3 a = 4; //语句4 b = 5; //语句5 //... }
变量a,b是非volatile修饰的变量,k则使用volatile修饰。所以语句3不能放在语句1、2前,也不能放在语句4、5后。但是语句1、2的顺序是不能保证的,同理,语句4、5也不能保证顺序。
并且,执行到语句3的时候,语句1,2是肯定执行完毕的,而且语句1,2的执行结果对于语句3,4,5是可见的。
首先要讲一下内存屏障,内存屏障可以分为以下几类:
LoadLoad barrier: Untuk pernyataan seperti Load1, LoadLoad, Load2. Sebelum data yang akan dibaca dalam Load2 dan operasi baca seterusnya diakses, adalah dijamin bahawa data yang akan dibaca dalam Load1 telah dibaca.
Penghalang stor: Untuk kenyataan sedemikian Store1, StoreStore, Store2, sebelum Store2 dan operasi tulis seterusnya dilaksanakan, operasi tulis Store1 dapat dilihat oleh pemproses lain.
Penghalang LoadStore: Untuk penyataan seperti Load1, LoadStore dan Store2, pastikan data yang akan dibaca oleh Load1 dibaca sebelum Store2 dan operasi tulis seterusnya dipadamkan.
Penghalang StoreLoad: Untuk pernyataan seperti Store1, StoreLoad, Load2, sebelum Load2 dan semua operasi baca seterusnya dilaksanakan, adalah dijamin bahawa penulisan ke Store1 boleh dilihat oleh semua pemproses.
Masukkan penghalang LoadLoad selepas setiap operasi bacaan yang tidak menentu dan halangan LoadStore selepas operasi baca.
Masukkan penghalang StoreStore di hadapan setiap operasi tulis yang tidak menentu dan penghalang SotreLoad di bahagian belakang.
Atas ialah kandungan terperinci Analisis Contoh Pengaturcaraan Konkurensi Tinggi JMM di Jawa. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!