java线程同步的作用_java线程同步机制

1.Sychronized

sychronized有三种使用方式修饰实例方法

修饰类方法

修饰代码块

1.1修饰实例方法

这种情况下加锁的对象是实例对象,也就是说同一个对象调用方法时才会产生互斥效果,看下例子

public class Test {

public static void main(String [] ar){

SychronizedMethods methods1 = new SychronizedMethods();

SychronizedMethods methods2 = new SychronizedMethods();

new Thread(new Runnable() {

@Override

public void run() {

methods1.print();

}

}).start();

methods2.print();

}

}

public class SychronizedMethods {

synchronized public void print() {

for (int i = 0; i < 10; i++)

System.out.println("当前线程:" + Thread.currentThread().![在这里插入图片描述](https://img-blog.csdnimg.cn/20200530120451234.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JyaW4yMzM=,size_16,color_FFFFFF,t_70)getName() + " " + i);

}

}

从日志看出,两个线程的执行是互不干扰的。

将以上代码改一下

public class Test {

public static void main(String [] ar){

SychronizedMethods methods1 = new SychronizedMethods();

SychronizedMethods methods2 = new SychronizedMethods();

new Thread(new Runnable() {

@Override

public void run() {

methods1.print();

}

}).start();

methods1.print();

}

}

​ 日志可以看出主线程执行完后释放锁,子线程才能调用print方法

1.2修饰类方法

​ 如果Sychronized修饰的是静态类方法,则无论是哪个实例,都会产生互斥效果,我们将上述代码中的方法改为静态的看下效果

可以看出改成修饰类方法后即使通过不同的实例对象调用,也会产生互斥。

1.3修饰代码块

public class SychronizedMethods {

public void print() {

synchronized (this){

for (int i = 0; i < 10; i++)

System.out.println("当前线程:" + Thread.currentThread().getName() + " " + i); }

}

}

任何Object对象都可以当作锁对象。

1.4 实现细节

​ 我们查看一下SychronizedMethods编译后的字节码,可以看出当使用同步代码块时,会在字节码中添加monitorenter,monitorexit指令:

而修饰方法则会在字节码文件中,该方法的flags属性中添加ACC_SYCHRONIZED标志。当JVM访问到这个标志时,会在方法的开始和结束分别添加monitorenter和monitorexit。

接下来我们看看monitor究竟是什么

修饰实例方法时,monitor是对象实例

修饰类方法时,monitor就是这个类的class

修饰代码块时,monitor为括号中所跟的对象实例

注:Sychronized是可重入的,也就是持有锁的线程可以重复进入同步方法时,不用再去获取锁。

每一个对象都是一个monitor,其中包含计数器和线程指针:

计数器:当前线程访问锁的次数。

线程指针:指向持有该锁的线程。

当执行到monitorenter指令时,线程会去尝试获取该monitor的所有权

如果monitor的计数器值为0,则线程得到锁,将计数器值+1,并将线程指针指向自己。

如果该线程已经持有该锁,则直接将计数器值+1。

如果已有其他线程持有monitor,则线程进入阻塞状态,知道monitor计数器为0,再重新尝试获取该锁。

当执行monitorexit时,monitor的计数-1,如果-1后计数器的值为0,则该线程释放锁,不再是monitor持有者。

2.ReentrantLock

​ 使用ReentrantLock也可以实现同步操作,但是需要手动加锁和释放锁。使用方式

public ReentrantLock reentrantLock = new ReentrantLock();

public void printReenter(){

try {

reentrantLock.lock();

for (int i = 0; i < 10; i++)

System.out.println("currentThread:" + Thread.currentThread().getName() + " " + i);

}catch (Exception e){

e.printStackTrace();

}finally {

reentrantLock.unlock();

}

}

2.1公平锁

​ Sychronized和ReentrantLock默认都是非公平锁,也就是说当一个锁被释放,其他被阻塞的线程重新竞争该锁,不存在先来后到的情况。而公平锁就是通过同步队列实现多个线程按照顺序获取锁。

public ReentrantLock reentrantLock = new ReentrantLock(true);//创建公平锁

3.同步原理

3.1 对象头

​ 在讲原理前,先来看下Java对象的组成,对象在内存中由三部分组成:对象头

实例数据

对齐填充

对象头又包含Mark Word,指向类元数据的指针

3.1.1 Mark Word

​ Mark Word用于存储对象自身运行时数据,sychronized的所有锁状态都与Mark Word有关

​ 以32位虚拟机为例,Mark Word的构成如下:

这是无锁状态下的对象Mark Word组成。

​ 事实上,Mark Word的组成结构并不是固定的,根据状态的不同可能出现以下几种情况:

根据是否偏向锁和锁标志位可以分出以下5种不同的锁状态。

3.2 Monitor

​ Java中任何一个对象都有一个对应的Monitor,保存在对象头中,是一个ObjectMonitor对象,数据结构如下

ObjectMonitor() {

_header = NULL;

_count = 0; // 计数器,用于记录该线程获取锁的个数

_waiters = 0,

_recursions = 0; // 重入次数

_object = NULL;

_owner = NULL; // 指向持有该Monitor的线程

_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet

_WaitSetLock = 0 ;

_Responsible = NULL ;

_succ = NULL ;

_cxq = NULL ;

FreeNext = NULL ;

_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表

_SpinFreq = 0 ;

_SpinClock = 0 ;

OwnerIsThread = 0 ;

}

当有多个线程访问同步代码时,这些线程会先进入_EntryList中,当某个线程竞争到monitor时,会将 _owner指向自身,然后将 _count加1。

如果获得锁的线程调用了wait()方法,将释放当前的monitor,将 _owner置为null, _count减1,然后进入 _waitSet中等待唤醒。

如果线程执行完毕也会释放锁。

示意图如下:

1.线程2通过竞争获取到锁

2.线程2将owner指向自身,并且count值+1。

3.当线程2调用wait()方法后,会释放锁,将owner置null,count值减1,并进入到WaitSet中等待唤醒。

4.线程1再通过竞争获取锁,并且在执行过程中调用了notify()方法唤醒线程2,则线程2将转移到EntryList重新开始竞争。注意:调用notify方法并不会让线程1释放锁。

4.锁优化

​ Java 6开始,虚拟机对sychronized进行了多方面优化,主要包括:自旋锁、轻量级锁、偏向锁 等

4.1 自旋锁

​ 线程的阻塞和唤醒需要CPU从用户态转向核心态,是比较重的操作,频繁的阻塞、唤醒会对CPU造成很大的负担。虚拟机的优化都是针对如何避免线程切换带来的开销,所谓的自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环(自旋)检测锁是否被释放,而不是进入线程挂起或睡眠状态。

​ 缺点

​ 自旋锁适用于锁占用时间较短的场景,也就是等待的线程只需短时间循环等待就可以获取到锁。由于自旋操作就是执行毫无意义的循环,也是会占用CPU的,对于锁占用时间长,竞争激烈的场景,锁自旋反而会白白浪费CPU资源,造成性能浪费,因此这种场景下应该禁用自旋锁。

4.2 适应性自旋锁

​ 自旋锁在JDK 1.4就引入,默认关闭。可以通过-XX:+UseSpinning开启,JDK 1.6默认开启,默认自旋次数为10次。JDK 1.6中还引入了适应性自旋锁。

​ 所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

当有线程自旋成功获取锁,那么下次自旋的次数就会增加,因为虚拟机认为既然上次自旋成功,那么这次自旋成功的可能性也比较大,所以允许增加自旋等待的次数。反之,如果一个锁很少有自旋成功的,说明锁的占用时间比较长,那么就会降低获取这个锁的自旋次数,避免造成CPU资源浪费。

4.3 偏向锁

​ 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

​ 如果线程得到偏向锁,如果接下来一段时间没有其他线程来请求该锁,那么持有锁的线程再次进入退出同一同步代码时,不需要抢占锁和释放锁操作。如果有其他线程来竞争锁,则偏向锁一定会膨胀为轻量级锁或者重量级锁,而撤销操作也是比较重的行为。

​ JDK1.6偏向锁默认开启,并发量大的情况下可以使用参数-XX:-UseBiasedLocking来禁止偏向锁。

4.4 轻量级锁

​ 当关闭偏向锁或者多个线程在不同时间段交替执行一个同步代码,则会开启轻量级锁。

​ 轻量级锁工作流程:

​ 1.当线程进入同步代码时,如果锁状态为无锁(锁标志位为01,是否偏向锁位1),虚拟机将在当前线程的栈帧中开辟一个Lock Record 锁记录空间,用于记录锁对象的Mark Word。

2. 然后虚拟机将尝试使用CAS(Compare And Swap)操作将Mark Word中的线程栈帧中的Lock Record指针指向当前线程的Lock Record,并将Lock Record中的owner指向该Object 的Mark Word。

3. 如果更新成功了,那么当前线程就得到锁,并且将锁标志位置为00,代表该对象处于轻量级锁状态。

4. 如果更新失败了,则虚拟机会先检查Object Mark Word中的锁记录是否指向当前线程的栈帧,如果是,代表已经拥有该锁,直接进入同步代码。否则进入自旋,自旋失败后,轻量级锁膨胀为重量级锁,当前线程进入阻塞状态。

4.5 锁消除

​ 锁消除是基于对象逃逸分析的,在某些情况下代码是不需要加锁的,但是却执行了加锁操作,虚拟机检测不到竞争情况,会将同步锁消除。

例如我们平时使用到的StringfBuffer.append();

@Override

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

return this;

}

从源码看出是一个同步方法,如果确实没必要加锁,虚拟机会将锁消除。

4.6 锁粗化

​ 通常情况下,我们希望同步代码作用范围尽可能小,仅在共享数据的作用域内加锁。但是有些时候会造成频繁的加锁,解锁操作,这时就需要锁粗化。例如

public void doSomethingMethod(){

for(int i = 0; i < 10; i++){

synchronized(lock){

//do some thing

}

}

}

for循环内是个同步代码块,每次循环都需要加锁,虚拟机会将连续加锁,解锁操作合并为一个更大范围的,即吧加锁、解锁移到for循环外。

5.总结

加锁、释放锁都是需要进行线程切换的操作,成本非常高。重量级锁是通过对象内部的Monitor实现。Java对锁的优化也是围绕着减少真正的执行加锁、释放锁的操作,避免线程切换导致CPU用户态和内核态的切换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值