Variabel di Java dibagi menjadi dua kategori: variabel lokal dan variabel kelas. Variabel lokal mengacu pada variabel yang didefinisikan dalam suatu metode, seperti variabel yang didefinisikan dalam metode yang dijalankan. Untuk variabel-variabel ini, tidak ada masalah berbagi antar thread. Oleh karena itu, mereka tidak memerlukan sinkronisasi data. Variabel kelas adalah variabel yang didefinisikan dalam suatu kelas, dan cakupannya adalah seluruh kelas. Variabel tersebut dapat dibagikan oleh banyak thread. Oleh karena itu, kita perlu melakukan sinkronisasi data pada variabel-variabel tersebut.
Sinkronisasi data berarti hanya satu thread yang dapat mengakses variabel kelas yang disinkronkan secara bersamaan. Setelah thread saat ini mengakses variabel tersebut, thread lain dapat terus mengaksesnya. Akses yang disebutkan di sini mengacu pada akses dengan operasi tulis. Jika semua thread yang mengakses variabel kelas adalah operasi baca, sinkronisasi data umumnya tidak diperlukan. Lalu apa jadinya jika sinkronisasi data tidak dilakukan pada variabel kelas bersama? Mari kita lihat dulu apa yang terjadi dengan kode berikut:
Copy kode kodenya sebagai berikut:
tes paket;
kelas publik MyThread memperluas Thread
{
int statis publik n = 0;
menjalankan kekosongan publik()
{
ke dalam m = n;
menghasilkan();
m++;
n = m;
}
public static void main(String[] args) memunculkan Pengecualian
{
Benang Saya Benang Saya = Benang Saya baru();
Utas utas[] = Utas baru[100];
for (int i = 0; i < benang.panjang; i++)
benang[i] = Benang baru(Benang saya);
for (int i = 0; i < benang.panjang; i++)
benang[i].mulai();
for (int i = 0; i < benang.panjang; i++)
utas[i].join();
System.out.println("n = " + Benang Saya.n);
}
}
Kemungkinan hasil dari mengeksekusi kode di atas adalah sebagai berikut:
Copy kode kodenya sebagai berikut:
n=59
Banyak pembaca mungkin terkejut melihat hasil ini. Program ini jelas memulai 100 thread, dan kemudian setiap thread menambah variabel statis n sebanyak 1. Terakhir, gunakan metode join untuk membuat 100 thread berjalan, lalu keluarkan nilai n. Biasanya, hasilnya harus n = 100. Namun hasilnya kurang dari 100.
Faktanya, penyebab dari hasil ini adalah “data kotor” yang sering kita sebutkan. Pernyataan hasil() dalam metode run adalah inisiator dari "data kotor" (tanpa menambahkan pernyataan hasil, "data kotor" juga dapat dihasilkan, tetapi tidak akan begitu jelas. Hanya dengan mengubah 100 ke angka yang lebih besar akan hal ini sering terjadi. Menghasilkan "data kotor", memanggil hasil dalam contoh ini adalah untuk memperkuat efek "data kotor"). Fungsi dari metode hasil adalah untuk menghentikan sementara thread, yaitu membuat thread yang memanggil metode hasil untuk sementara menyerahkan sumber daya CPU, sehingga memberikan kesempatan kepada CPU untuk mengeksekusi thread lainnya. Untuk mengilustrasikan bagaimana program ini menghasilkan "data kotor", mari kita asumsikan hanya dua thread yang dibuat: thread1 dan thread2. Karena metode awal thread1 dipanggil terlebih dahulu, metode run dari thread1 biasanya akan dijalankan terlebih dahulu. Ketika metode run dari thread1 berjalan ke baris pertama (int m = n;), nilai n ditugaskan ke m. Ketika metode hasil dari baris kedua dijalankan, thread1 akan berhenti mengeksekusi untuk sementara. Ketika thread1 dijeda, thread2 mulai berjalan setelah mendapatkan sumber daya CPU (thread2 telah dalam keadaan siap sebelumnya). m = n;), karena n masih 0 ketika thread1 dijalankan untuk menghasilkan, maka nilai yang diperoleh m pada thread2 juga adalah 0. Hal ini menyebabkan nilai m dari thread1 dan thread2 sama-sama mendapat 0. Setelah mereka menjalankan metode hasil, mereka semua memulai dari 0 dan menambahkan 1. Oleh karena itu, tidak peduli siapa yang mengeksekusinya terlebih dahulu, nilai akhir dari n adalah 1, tetapi n ini diberi nilai masing-masing oleh thread1 dan thread2. Mungkin ada yang bertanya, jika hanya ada n++, apakah "data kotor" akan dihasilkan? Jawabannya adalah ya. Jadi n++ hanyalah sebuah pernyataan, jadi bagaimana cara menyerahkan CPU ke thread lain selama eksekusi? Faktanya, ini hanyalah fenomena dangkal. Setelah n++ dikompilasi ke dalam bahasa perantara (juga disebut bytecode) oleh kompiler Java, itu bukanlah sebuah bahasa. Mari kita lihat bahasa perantara Java apa yang akan dikompilasi ke dalam kode Java berikut.
Copy kode kodenya sebagai berikut:
menjalankan kekosongan publik()
{
n++;
}
Kode bahasa perantara yang dikompilasi
Copy kode kodenya sebagai berikut:
menjalankan kekosongan publik()
{
memuat_0
dup
dapatkan lapangan
ikon_1
ia menambahkan
lapangan put
kembali
}
Anda dapat melihat bahwa hanya ada pernyataan n++ dalam metode run, tetapi setelah kompilasi, ada 7 pernyataan bahasa perantara. Kita tidak perlu mengetahui apa fungsi dari pernyataan-pernyataan tersebut, lihat saja pernyataan pada baris 005, 007, dan 008. Baris 005 adalah getfield, menurut arti bahasa inggrisnya kita tahu bahwa kita ingin mendapatkan nilai tertentu. Karena di sini hanya ada satu n, maka tidak diragukan lagi kita ingin mendapatkan nilai n. Tidak sulit untuk menebak bahwa iadd pada baris 007 adalah menambahkan 1 pada nilai n yang diperoleh. Saya rasa Anda mungkin sudah menebak arti putfield pada baris 008. Ini bertanggung jawab untuk memperbarui n setelah menambahkan 1 kembali ke variabel kelas n. Ngomong-ngomong soal ini, Anda mungkin masih ragu. Saat menjalankan n++, cukup tambahkan n dengan 1 saja. Kenapa repot sekali? Faktanya, ini melibatkan masalah model memori Java.
Model memori Java dibagi menjadi tempat penyimpanan utama dan tempat penyimpanan kerja. Area penyimpanan utama menyimpan semua instance di Java. Artinya, setelah kita menggunakan new untuk membuat objek, objek dan metode internalnya, variabel, dll. disimpan di area ini, dan n di kelas MyThread disimpan di area ini. Penyimpanan utama dapat digunakan bersama oleh semua thread. Area penyimpanan yang berfungsi adalah tumpukan thread yang kita bicarakan sebelumnya. Di area ini, variabel yang ditentukan dalam metode run dan metode yang dipanggil oleh metode run disimpan, yaitu variabel metode. Ketika sebuah thread ingin mengubah variabel di tempat penyimpanan utama, ia tidak mengubah variabel tersebut secara langsung, tetapi menyalinnya ke tempat penyimpanan kerja dari thread saat ini. Setelah modifikasi selesai, nilai variabel ditimpa dengan nilai yang sesuai nilai di tempat penyimpanan utama.
Setelah memahami model memori Java, tidak sulit untuk memahami mengapa n++ bukan operasi atom. Harus melalui proses copy, tambah 1 dan timpa. Proses ini mirip dengan yang disimulasikan di kelas MyThread. Seperti yang dapat Anda bayangkan, jika thread1 terputus karena alasan tertentu saat getfield dijalankan, situasi yang mirip dengan hasil eksekusi kelas MyThread akan terjadi. Untuk menyelesaikan masalah ini sepenuhnya, kita harus menggunakan beberapa metode untuk menyinkronkan n, yaitu hanya satu thread yang dapat mengoperasikan n pada waktu yang sama, yang disebut juga operasi atom pada n.