锁的简介
java中有两种锁synchronized
和lock
;原来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做了较大的优化,引入了偏向锁、轻量级锁、锁粗化、锁消除等优化手段。
偏向锁
设计偏向锁和轻量级锁的目的就是考虑到一下情况:
- 虽然我们添加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--;
}
}
}
异常情况
当一个线程持有偏向锁,而另一个线程没有竞争到偏向锁,或者当一个线程持有偏向锁,而另一个线程想去获取到偏向锁的时候,这个时候就涉及到了锁升级。
在锁升级的过程中,会先把获取到偏向锁的线程给停止掉,然后再给锁进行升级。
锁升级图
轻量级锁升级重量级锁的时候是这样的:
- 如果一个线程持有轻量级锁,另一个线程尝试去获取轻量级锁,那么这个时候就需要进行锁升级。
- 满足以上的条件,并不是里面就进行锁升级,未获取到轻量级锁的线程而是进行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);
}
}
}