Apakah prinsip dan proses peningkatan kunci Java Synchronized?
Penyediaan alat
Sebelum kita bercakap secara rasmi tentang prinsip penyegerakan, mari kita bercakap tentang Kunci putaran, kerana Kunci putaran memainkan peranan yang sangat penting dalam pengoptimuman daripada kesan yang besar. Untuk memahami kunci putaran, pertama sekali kita perlu memahami apa itu keatoman.
Apa yang dipanggil atomicity bermaksud setiap operasi sama ada tidak dilakukan atau melakukan semua bermakna ia tidak boleh diganggu semasa operasi, contohnya, untuk pembolehubah data
Untuk melakukan operasi kenaikan, terdapat tiga langkah berikut:
Muatkan
data
dari memori ke daftar.Tambahkan satu pada nilai
data
.Tulis hasilnya kembali ke ingatan.
Atomicity bermaksud bahawa apabila thread melakukan operasi kenaikan, ia tidak boleh diganggu oleh thread lain Hanya apabila thread ini melengkapkan ketiga-tiga proses ini, thread lain boleh memanipulasi data >. data
untuk melaksanakan operasi atom pada data integer: AtomicInteger
import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { public static void main(String[] args) throws InterruptedException { AtomicInteger data = new AtomicInteger(); data.set(0); // 将数据初始化位0 Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 对数据 data 进行原子加1操作 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 对数据 data 进行原子加1操作 } }); // 启动两个线程 t1.start(); t2.start(); // 等待两个线程执行完成 t1.join(); t2.join(); // 打印最终的结果 System.out.println(data); // 200000 } }
bersamaan dengan 0, dan nilai awal data
diperolehi oleh kedua-dua utas adalah sama dengan 0. data
, dan kemudian utas satu menyegerakkan nilai data
kembali ke memori utama Data dalam keseluruhan memori berubah sebagai berikut: data
menambah satu, dan kemudian menyegerakkan nilai data
kembali ke memori utama (menimpa nilai asal memori utama) : data
menjadi data
selepas perubahan di atas, tetapi benang dua menimpa nilai kami, jadi dalam situasi berbilang benang, keputusan akhir kami akan menjadi lebih kecil. 2
untuk data
adalah atom dan tidak boleh dibahagikan, dan benang lain tidak boleh beroperasi pada +1
. Inilah kelebihan yang dibawa oleh data
atomicity.
di atas dilaksanakan melalui +1
kunci putaran Kita boleh lihat pada kod sumber : AtomicInteger
public final int addAndGet(int delta) { // 在 AtomicInteger 内部有一个整型数据 value 用于存储具体的数值的 // 这个 valueOffset 表示这个数据 value 在对象 this (也就是 AtomicInteger一个具体的对象) // 当中的内存偏移地址 // delta 就是我们需要往 value 上加的值 在这里我们加上的是 1 return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
UnSafe
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); // 从对象 o 偏移地址为 offset 的位置取出数据 value ,也就是前面提到的存储整型数据的变量 } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
offset
!compareAndSwapInt(o, offset, v, v + delta)
Fungsi utama pernyataan ini adalah untuk membandingkan sama ada data objek
bersamaan dengan o
Jika ya, offset
akan menetapkan data dengan alamat offset v
kepada v
Jika pernyataan ini berjaya dilaksanakan, ia akan kembali offset
jika tidak kembalikan v + delta
. Inilah yang sering kami panggil true
CAS< di Jawa 🎜>. false
Melihat perkara ini, anda sepatutnya mendapati bahawa apabila pernyataan di atas gagal dilaksanakan, operasi gelung sementara akan diteruskan sehingga operasi itu berjaya tidak berjaya, ia akan terus "berpusing" "Di sini, operasi seperti ini ialah Pusing
Pusing ini dipanggil Kunci Putaran. Susun atur memori objek
Dalam JVM, terdapat tiga bahagian memori utama untuk objek Java: Pengepala objek, yang mengandungi dua bahagian data , masing-masing- Tandakan perkataan
- dan penunjuk taip (
Penunjuk Kclass).
Data instance ialah pelbagai data yang kami takrifkan dalam kelas. - Padding penjajaran Apabila dilaksanakan, JVM memerlukan saiz memori yang diduduki oleh setiap objek perlu menjadi gandaan integer 8 bait jika saiz memori diduduki oleh data objek tidak mencukupi Jika ia adalah gandaan integer 8 bait, maka ia perlu diisi kepada 8 bait Contohnya, jika stesen objek ialah 60 bait, ia akhirnya akan diisi kepada 64 bait.
- Berkait rapat dengan prinsip peningkatan kunci yang disegerakkan yang akan kita bincangkan ialah Perkataan tanda
Dalam mesin maya Java 32-bit, perkataan Mark mempunyai 4 bait dan sejumlah 32 bit kandungannya adalah seperti berikut:
我们在使用synchronized时,如果我们是将synchronized用在同步代码块,我们需要一个锁对象。对于这个锁对象来说一开始还没有线程执行到同步代码块时,这个4个字节的内容如上图所示,其中有25个比特用来存储哈希值,4个比特用来存储垃圾回收的分代年龄(如果不了解可以跳过),剩下三个比特其中第一个用来表示当前的锁状态是否为偏向锁,最后的两个比特表示当前的锁是哪一种状态:
如果最后三个比特是:001,则说明锁状态是没有锁。
如果最后三个比特是:101,则说明锁状态是偏向锁。
如果最后两个比特是:00, 则说明锁状态是轻量级锁。
如果最后两个比特是:10, 则说明锁状态是重量级锁。
而synchronized锁升级的顺序是:无????->偏向????->轻量级????->重量级????。
在Java当中有一个JVM参数用于设置在JVM启动多少秒之后开启偏向锁(JDK6之后默认开启偏向锁,JVM默认启动4秒之后开启对象偏向锁,这个延迟时间叫做偏向延迟,你可以通过下面的参数进行控制):
//设置偏向延迟时间 只有经过这个时间只有对象锁才会有偏向锁这个状态 -XX:BiasedLockingStartupDelay=4 //禁止偏向锁 -XX:-UseBiasedLocking //开启偏向锁 -XX:+UseBiasedLocking
我们可以用代码验证一下在无锁状态下,MarkWord的内容是什么:
import org.openjdk.jol.info.ClassLayout; import java.util.concurrent.TimeUnit; public class MarkWord { public Object o = new Object(); public synchronized void demo() { synchronized (o) { System.out.println("synchronized代码块内"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } public static void main(String[] args) throws InterruptedException { System.out.println("等待4s前"); System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); TimeUnit.SECONDS.sleep(4); MarkWord markWord = new MarkWord(); System.out.println("等待4s后"); System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); Thread thread = new Thread(markWord::demo); thread.start(); thread.join(); System.out.println(ClassLayout.parseInstance(markWord.o).toPrintable()); } }
上面代码输出结果,下面的红框框住的表示是否是偏向锁和锁标志位(可能你会有疑问为什么是这个位置,不应该是最后3个比特位表示锁相关的状态吗,这个其实是数据表示的大小端问题,大家感兴趣可以去查一下,在这你只需知道红框三个比特就是用于表示是否为偏向锁和锁的标志位):
从上面的图当中我们可以分析得知在偏向延迟的时间之前,对象锁的状态还不会有偏向锁,因此对象头中的Markword当中锁状态是01,同时偏向锁状态是0,表示这个时候是无锁状态,但是在4秒之后偏向锁的状态已经变成1了,因此当前的锁状态是偏向锁,但是还没有线程占有他,这种状态也被称作匿名偏向,因为在上面的代码当中只有一个线程进入了synchronized同步代码块,因此可以使用偏向锁,因此在synchronized代码块当中打印的对象的锁状态也是偏向锁。
上面的代码当中使用到了jol包,你需要在你的pom文件当中引入对应的包:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
上图当中我们显示的结果是在64位机器下面显示的结果,在64位机器当中在Java对象头当中的MarkWord和Klcass Pointer内存布局如下:
其中MarkWord占8个字节,Kclass Pointer占4个字节。JVM在64位和32位机器上的MarkWord内容基本一致,64位机器上和32位机器上的MarkWord内容和表示意义是一样的,因此最后三位的意义你可以参考32位JVM的MarkWord。
锁升级过程
偏向锁
假如你写的synchronized代码块没有多个线程执行,而只有一个线程执行的时候这种锁对程序性能的提高还是非常大的。他的具体做法是JVM会将对象头当中的第三个用于表示是否为偏向锁的比特位设置为1,同时会使用CAS操作将线程的ID记录到Mark Word当中,如果操作成功就相当于获得????了,那么下次这个线程想进入临界区就只需要比较一下线程ID是否相同了,而不需要进行CAS或者加锁这样花费比较大的操作了,只需要进行一个简单的比较即可,这种情况下加锁的开销非常小。
可能你会有一个疑问在无锁的状态下Mark Word存储的是哈希值,而在偏向锁的状态下存储的是线程的ID,那么之前存储的Hash Code不就没有了嘛!你可能会想没有就没有吧,再算一遍不就行了!事实上不是这样,如果我们计算过哈希值之后我们需要尽量保持哈希值不变(但是这个在Java当中并没有强制,因为在Java当中可以重写hashCode方法),因此在Java当中为了能够保持哈希值的不变性就会在第一次计算一致性哈希值(Mark Word里面存储的是一致性哈希值,并不是指重写的hashCode返回值,在Java当中可以通过 Object.hashCode()或者System.identityHashCode(Object)方法计算一致性哈希值)的时候就将计算出来的一致性哈希值存储到Mark Word当中,下一次再有一致性哈希值的请求的时候就将存储下来的一致性哈希值返回,这样就可以保证每次计算的一致性哈希值相同。但是在变成偏向锁的时候会使用线程ID覆盖哈希值,因此当一个对象计算过一致性哈希值之后,他就再也不能进行偏向锁状态,而且当一个对象正处于偏向锁状态的时候,收到了一致性哈希值的请求的时候,也就是调用上面提到的两个方法,偏向锁就会立马膨胀为重量级锁,然后将Mark Word 储在重量级锁里。
下面的代码就是验证当在偏向锁的状态调用System.identityHashCode
函数锁的状态就会升级为重量级锁:
import org.openjdk.jol.info.ClassLayout; import java.util.concurrent.TimeUnit; public class MarkWord { public Object o = new Object(); public synchronized void demo() { System.out.println("System.identityHashCode(o) 函数之前"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.identityHashCode(o); System.out.println("System.identityHashCode(o) 函数之后"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } public static void main(String[] args) throws InterruptedException { TimeUnit.SECONDS.sleep(5); MarkWord markWord = new MarkWord(); Thread thread = new Thread(markWord::demo); thread.start(); thread.join(); TimeUnit.SECONDS.sleep(2); System.out.println(ClassLayout.parseInstance(markWord.o).toPrintable()); } }
轻量级锁
轻量级锁也是在JDK1.6加入的,当一个线程获取偏向锁的时候,有另外的线程加入锁的竞争时,这个时候就会从偏向锁升级为轻量级锁。
在轻量级锁的状态时,虚拟机首先会在当前线程的栈帧当中建立一个锁记录(Lock Record),用于存储对象MarkWord的拷贝,官方称这个为Displaced Mark Word。然后虚拟机会使用CAS操作尝试将对象的MarkWord指向栈中的Lock Record,如果操作成功说明这个线程获取到了锁,能够进入同步代码块执行,否则说明这个锁对象已经被其他线程占用了,线程就需要使用CAS不断的进行获取锁的操作,当然你可能会有疑问,难道就让线程一直死循环了吗?这对CPU的花费那不是太大了吗,确实是这样的因此在CAS满足一定条件的时候轻量级锁就会升级为重量级锁,具体过程在重量级锁章节中分析。
当线程需要从同步代码块出来的时候,线程同样的需要使用CAS将Displaced Mark Word替换回对象的MarkWord,如果替换成功,那么同步过程就完成了,如果替换失败就说明有其他线程尝试获取该锁,而且锁已经升级为重量级锁,此前竞争锁的线程已经被挂起,因此线程在释放锁的同时还需要将挂起的线程唤醒。
重量级锁
所谓重量级锁就是一种开销最大的锁机制,在这种情况下需要操作系统将没有进入同步代码块的线程挂起,JVM(Linux操作系统下)底层是使用pthread_mutex_lock
、pthread_mutex_unlock
、pthread_cond_wait
、pthread_cond_signal
和pthread_cond_broadcast
这几个库函数实现的,而这些函数依赖于futex
系统调用,因此在使用重量级锁的时候因为进行了系统调用,进程需要从用户态转为内核态将线程挂起,然后从内核态转为用户态,当解锁的时候又需要从用户态转为内核态将线程唤醒,这一来二去的花费就比较大了(和CAS自旋锁相比)。
在有两个以上的线程竞争同一个轻量级锁的情况下,轻量级锁不再有效(轻量级锁升级的一个条件),这个时候锁为膨胀成重量级锁,锁的标志状态变成10,MarkWord当中存储的就是指向重量级锁的指针,后面等待锁的线程就会被挂起。
因为这个时候MarkWord当中存储的已经是指向重量级锁的指针,因此在轻量级锁的情况下进入到同步代码块在出同步代码块的时候使用CAS将Displaced Mark Word替换回对象的MarkWord的时候就会替换失败,在前文已经提到,在失败的情况下,线程在释放锁的同时还需要将被挂起的线程唤醒。
Atas ialah kandungan terperinci Apakah prinsip dan proses peningkatan kunci Java Synchronized?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

AI Hentai Generator
Menjana ai hentai secara percuma.

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas



Panduan Nombor Sempurna di Jawa. Di sini kita membincangkan Definisi, Bagaimana untuk menyemak nombor Perfect dalam Java?, contoh dengan pelaksanaan kod.

Panduan untuk Penjana Nombor Rawak di Jawa. Di sini kita membincangkan Fungsi dalam Java dengan contoh dan dua Penjana berbeza dengan contoh lain.

Panduan untuk Weka di Jawa. Di sini kita membincangkan Pengenalan, cara menggunakan weka java, jenis platform, dan kelebihan dengan contoh.

Panduan untuk Nombor Smith di Jawa. Di sini kita membincangkan Definisi, Bagaimana untuk menyemak nombor smith di Jawa? contoh dengan pelaksanaan kod.

Dalam artikel ini, kami telah menyimpan Soalan Temuduga Spring Java yang paling banyak ditanya dengan jawapan terperinci mereka. Supaya anda boleh memecahkan temuduga.

Java 8 memperkenalkan API Stream, menyediakan cara yang kuat dan ekspresif untuk memproses koleksi data. Walau bagaimanapun, soalan biasa apabila menggunakan aliran adalah: bagaimana untuk memecahkan atau kembali dari operasi foreach? Gelung tradisional membolehkan gangguan awal atau pulangan, tetapi kaedah Foreach Stream tidak menyokong secara langsung kaedah ini. Artikel ini akan menerangkan sebab -sebab dan meneroka kaedah alternatif untuk melaksanakan penamatan pramatang dalam sistem pemprosesan aliran. Bacaan Lanjut: Penambahbaikan API Java Stream Memahami aliran aliran Kaedah Foreach adalah operasi terminal yang melakukan satu operasi pada setiap elemen dalam aliran. Niat reka bentuknya adalah

Panduan untuk TimeStamp to Date di Java. Di sini kita juga membincangkan pengenalan dan cara menukar cap waktu kepada tarikh dalam java bersama-sama dengan contoh.

Kapsul adalah angka geometri tiga dimensi, terdiri daripada silinder dan hemisfera di kedua-dua hujungnya. Jumlah kapsul boleh dikira dengan menambahkan isipadu silinder dan jumlah hemisfera di kedua -dua hujungnya. Tutorial ini akan membincangkan cara mengira jumlah kapsul yang diberikan dalam Java menggunakan kaedah yang berbeza. Formula volum kapsul Formula untuk jumlah kapsul adalah seperti berikut: Kelantangan kapsul = isipadu isipadu silinder Dua jumlah hemisfera dalam, R: Radius hemisfera. H: Ketinggian silinder (tidak termasuk hemisfera). Contoh 1 masukkan Jejari = 5 unit Ketinggian = 10 unit Output Jilid = 1570.8 Unit padu menjelaskan Kirakan kelantangan menggunakan formula: Kelantangan = π × r2 × h (4
