java中的锁

java中的锁

Posted by bulingfeng on July 29, 2024

锁的简介

java中有两种锁synchronizedlock;原来synchronized被认为是低效的,但是经过一些优化之后和lock的性能已经相差无几了。

synchronized

synchronized可以用于方法和方法内的局部代码块。

synchronized使用的是Monitor锁,而Monitor锁是跟着对象走的。

作用与方法

add和substract都不能进行并发,因为使用的是同一个对象;

1
2
3
4
5
6
7
8
9
10
11
public class Counter {
  private int count = 0;

  public synchronized void add(int value) {
    count += value;
  }

  public synchronized void substract(int value) {
    count -= value;
  }
}

作用于代码块

如果认为synchronized作用在方法上造成锁的颗粒度太大,那么可以在方法内的代码块进行执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Counter {
  private int count = 0;

  public void add(int value) {
    ...
    synchronized (this) {
      count += value;
    }
    ...
  }

  public void substract(int value) {
    ...
    synchronized (this) {
      count -= value;
    }
    ...
  }
}

类锁和对象锁

如果使用类锁的话,那么该类下的所有对象都不能并发。

1
2
3
4
5
6
7
8
9
10
11
12
public class Wallet {
  private int balance;

  public void transferTo(Wallet targetWallet, int amount) {
    synchronized (Wallet.class) {
      if (this.balance >= amount) {
        this.balance -= amount;
        targetWallet.balance += amount;
      }
    }
  }
}

当然也可以在static方法上直接加上synchronized这个关键字。

1
2
3
4
5
6
7
public class Counter {
  private static int count = 0;

  public synchronized static void add(int value) {
      count += value;
  }
}

底层实现原理

下图为对象的存储结构

Monitor加锁的步骤

1)多个线程竞争获取锁;

2)没有获取到锁的线程排队等待锁;

3)锁释放之后会通知排队等待锁的线程去竞争锁;

4)没有获取锁的线程会阻塞,并且对应的内核线程不再分配时间片;

5)阻塞线程获取到锁之后取消阻塞,并且对应的内核线程恢复分配时间片。

Java对synchronized的优化

在JDK1.6版本中,Java对synchronized做了较大的优化,引入了偏向锁、轻量级锁、锁粗化、锁消除等优化手段。

偏向锁

设计偏向锁和轻量级锁的目的就是考虑到一下情况:

  1. 虽然我们添加synchronized关键字是为了防止多线程造成的安全问题,但是大多数情况下可能只有一个线程或者多个线程交替来执行(交替执行互不影响)的情况。所以偏向锁和轻量级锁诞生了。

Mark Word的图示

锁的获取过程

当发现threadId=0的时候,说明这个偏向锁还没有被使用过,然后这个线程就会采用CAS的方式来竞争这个偏向锁。

需要注意的是当偏向锁执行完的时候,并不会把Mark Wold中的threadId设置为0;这样做的目的是为了提高效率,因为当同一个线程再执行同步代码的时候,只需要判断threadId是否和原来一样可以了,无需再加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
  private static Object obj = new Object();
  private static int count = 0;
  public static void main(String[] args) {
    synchronized(obj) { //处于偏向锁状态
      count++;
    }
    ...
    synchronized(obj) { //再次请求偏向锁
      count--;
    }
  }
}

异常情况

当一个线程持有偏向锁,而另一个线程没有竞争到偏向锁,或者当一个线程持有偏向锁,而另一个线程想去获取到偏向锁的时候,这个时候就涉及到了锁升级。

在锁升级的过程中,会先把获取到偏向锁的线程给停止掉,然后再给锁进行升级。

锁升级图

轻量级锁升级重量级锁的时候是这样的:

  1. 如果一个线程持有轻量级锁,另一个线程尝试去获取轻量级锁,那么这个时候就需要进行锁升级。
  2. 满足以上的条件,并不是里面就进行锁升级,未获取到轻量级锁的线程而是进行cas方式看看是否能够获取到轻量级锁,如果不能获取到就进行锁升级。cas自旋的次数为10次,但是这个参数也可以通过JVM进行修改。

锁消除

1
2
3
4
5
6
7
8
9
// 因为StringBuffer是线程安全的,并且还是局部变量,不存在线程安全问题,所以会直接去除掉锁
public class Demo {
  public String concat(String s1, String s2) {
    StringBuffer strBuffer = new StringBuffer();
    strBuffer.append(s1);
    strBuffer.append(s2);
    return strBuffer.toString();
  }
}

锁粗化

1
2
3
4
5
6
7
8
9
10
// StringBuffer是线程安全的,并且还是公共变量,而进行for循环的时候每次都加锁,影响效率,从而把锁加在reproduce方法这里,从而避免频繁获取锁和释放锁,从而提高效率
public class Demo35_4 {
  private StringBuffer strBuffer = new StringBuffer();

  public void reproduce(String s) {
    for (int i = 0; i < 10000; ++i) {
      strBuffer.append(s);
    }
  }
}