JAVA多线程基础篇 6、详解synchronized

synchronized是java语言最常用的加锁指令。

1. Synchronized的使用

在java运行环境中,每个实例都对应有自己的一把锁,不同的锁之间互不影响,相同的锁互相排斥。
通一把锁,在同一时间,只能被一个线程获取,没有获得锁的线程只能等待。

1.1 修饰实例方法

实际上是在this对象上加锁。参照1.2

public void synchronized  dosomething() {
 }

1.2 在代码块里显式给this对象加锁

 synchronized (this) {
 }

1.3 在代码块里显示给对象加锁

建议在这个指定对象上加上final修饰。因为 o 对象如果指向变化了,锁对象就会变化,会导致问题。

final Object o = new Object();
synchronized (o) {
}

1.4 修饰静态方法

实际上是在这个类的class对象上加锁。参照1.5
举例来说,这个类的名称是A,这个方法等同于在A.class这个对象上加锁。

public static synchronized void dosomething() {
} 

1.5 显式锁定class对象

在这里需要注意,在jvm里每个类加载后就会创建一个class对象,比如A类,在jvm里就有且只有一个A.class对象,因此如果对A.class上锁,那么其他争抢A.class对象的线程都会等待与争抢

synchronized(A.class){
}

2.Synchronized的原理

在java中,锁的实现机制是通过以下三个机制协调实现的。

  • 1.锁对象
  • 2.锁对象所关联的Monitor对象。
  • 3.Monitor对象锁关联的同步队列。

其流程如下,线程在获取锁时,会根据锁对象的头信息,查找monitor对象,检查monitor对象是否被其他线程占用。

    1. 如果monitor对象没有被占用,则标记monitor对象为本线程占用。并且monitor内部的锁计数器+1。如果锁方法完成,在解锁时锁计数器-1,并且清除monitor对象的所有者信息为空。 由于锁有可重入机制,如果monitor对象的所有者是自己,那么会允许该线程重复获取锁。
    1. 如果monitor对象被占用,且所有者不是本线程。则会进入monitor关联的同步队列。在monitor对象被解锁时,同步队列上的线程会被唤醒,重新抢锁。注意wait方法也可以进入到这个同步队列。

在java程序中,synchronized方法编译为java字节码,实际上就是用
monitorenter 方法monitorexit 方法来包裹住synchronized方法。

3.Synchronized的优化

在早期jdk版本中,synchronized使用的是重量级锁(悲观锁),效率相比较低。随着jdk升级,synchronized进行了优化,内部会对锁进行逐步升级,因此提升了适应性,效率已经大幅提升了。

3.1 Synchronized加锁膨胀过程

synchronized在加锁的时候一开始是偏向锁,当发生锁冲突的时候,会升级为轻量级锁即CAS,在重试10次后,如果仍然有线程无法获取锁将升级为重量级锁。

  • 偏向锁:在对象头记录所属线程,当新线程访问对象头时会比较对象头记录的线程id,如果不一致,将触发锁升级。
  • 自旋锁:第一次升级为自旋锁。默认10次尝试。
  • 重量级锁:自旋锁尝试多次失败后升级为重量级锁,想要获取锁的线程将进入等待队列。

注意:synchronized加锁的对象不能使用String常量、Integer、Long等基础类型。

3.2 锁消除

public String lockcancel() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

在编译时,编译器就会判断出sb这个对象并不会被这段代码块以外的地方访问到,更不会被其他线程访问到,这时候的加锁就是完全没必要的,编译器就会把这里的加锁代码消除掉,于是,编译器会将程序中的sb对象替换为线程不安全的StringBuilder 类型,以提升效率。

3.3 锁的粗化和细化

  • 锁粗化

如果一系列的连续操作都对同一个对象反复加锁解锁,甚至加锁操作时出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

public void method(String s1, String s2){
    synchronized(this){
        System.out.println("s1");
    }
    synchronized(this){
        System.out.println("s1");
    }
}

会被优化为

public void method(String s1, String s2){
    synchronized(this){
        System.out.println("s1");
        System.out.println("s1");
    }
}
  • 锁细化
    由于加锁后程序进入临界区,为提升系统整理效率,应当减少加锁范围。尽量不要对一整个方法进行加锁,而是抽离出一个方法中的共享数据,只对方法中的这部分数据代码加锁。

总结

Synchronized的运行过程有许多值得了解的细节。

多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。

https://github.com/forestnlp/concurrentlab

如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。

您的支持是对我最大的鼓励。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟空学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值