Java采用synchronized关键字、以互斥同步的方式的解决线程安全问题,那么什么是线程安全呢?这里引用《Java并发编程实战》作者Brian Goetz给出的定义:
一、synchronized的使用 (一)使用方法“当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调 *** 作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。”—— Brian Goetz
// 修饰实例方法
public synchronized void test1(){
}
// 修饰代码块
public void test2(){
synchronized(new Test()){
}
}
// 修饰静态方法
public static synchronized void test3(){
}
1、synchronized作用于实例方法
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 输出结果:
* 2000000
*/
}
两个线程 *** 作同一个共享资源即变量 i ,由于i++; *** 作并不具备原子性,该 *** 作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取 i 的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1 *** 作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance。
因此,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁。
2、synchronized作用于静态方法public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发 *** 作。当synchronized作用于静态方法时,其锁就是当前类的class对象锁。
如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
3、synchronized作用于代码块public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗时 *** 作....
//使用同步代码块对变量i进行同步 *** 作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++; *** 作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
总结:
(二)字节码分析无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // here
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/easy/helloworld/Test
3: dup
4: invokespecial #3 // Method "":()V
7: dup
8: astore_1
9: monitorenter // here
10: aload_1
11: monitorexit // here
12: goto 20
15: astore_2
16: aload_1
17: monitorexit // here
18: aload_2
19: athrow
20: return
public static synchronized void test3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // here
可以观察到:
- 修饰实例方法:隐式调用moniterenter、moniterexit
- 修饰代码块:通过moniterenter、moniterexit 关联到到一个monitor对象,进入时设置Owner为当前线程,计数+1、退出-1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。
- 修饰静态方法:隐式调用moniterenter、moniterexit
moniterenter和moniterexit这两个jvm指令,主要是基于 Mark Word
和Object monitor
来实现的。其中Mark Word在对象头中,对象头的具体结构如下:
Monitor被翻译成监视器或管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word就被设置为指向Monitor对象的指针。
(这部分后续补充)
二、轻量级锁引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁使用的 *** 作系统互斥量带来的开销,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的,也就是没有竞争,那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized。
进入轻量级锁的流程:
- 获取对象的Mark Word,判断标识位是否为001,无锁状态且不可偏向。
- 如果标识位是001,创建锁记录(Lock Record)对象。每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
- 让锁记录中的Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。
- 如果CAS替换成功,对象头中存储了锁记录的机制和状态00,表示由该线程给对象加轻量级锁。
- 如果是锁重入情况,则再添加一条锁记录,并设置Mark Word=null。
- 当退出synchronized代码块时(解锁),如果锁记录取值不为null,使用CAS将Mark Word的值恢复给对象头,成功则解锁成功,反之说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,需要进入重量级锁解锁流程。
如果在尝试加轻量级锁的过程中,CAS *** 作无法成功,一种情况就是由其它线程为次对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当thread1进行轻量级加锁时,thread0已经对该对象加了轻量级锁
- 这时thread1加轻量级锁失败,进入锁膨胀流程:
- 为Object对象申请Monitor锁,让Object指向重量级锁的地址,
- 然后自己进入Monitor的EntryList中阻塞。
- 当thread0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中阻塞的线程。
重量级锁竞争的时候,还可以使用自旋在进行优化,如果当前线程自旋成功,也就是这时候持锁线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞。
五、偏向锁经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS *** 作,耗时)的代价而引入偏向锁。
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步 *** 作,即获取锁的过程,这样就省去了大量有关锁申请的 *** 作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
六、撤销 (一)调用对象的hashCode调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销,因为偏向锁对象MarkWord里没有hashCode。
- 轻量级锁会在锁记录中记录hashCode
- 重量级锁会在Monitor中记录hashCode
有偏向锁会使用偏向锁,如果有错开的其它线程使用这个对象,就撤销偏向锁,变成轻量级锁。如果这时有竞争发生(并没有错开),锁就会膨胀为重量级锁。
七、批量重偏向如果对象虽然被多个线程访问,但没有竞争,这时偏向了thread1的对象仍有机会重新偏向thread2,重偏向会重置对象的threadID。当撤销偏向锁的次数达到超过阈值20次之后,jvm会认为偏向错误,于是给这些对象加锁时重新偏向至加锁线程。
假设有30个对象,这30个对象都偏向threadA,后来由于threadB来使用这些对象,在使用前20个对象的时候,会先撤销它们的偏向,再改为轻量级锁。循环20次后,jvm批量修改后面的对象,让它们偏向threadB。
八、批量撤销当撤销偏向锁阈值超过40次后,jvm会觉得这个类竞争过于激烈,根本不该偏向,于是整个类的所有对象都变为不可偏向,新建的对象也都是不可偏向的。
参考https://blog.csdn.net/javazejian/article/details/72828483
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)