synchronized锁的是对象
- 不是锁的代码
- 作用在方法上时相当于synchronized(this),即锁的是当前对象本身。
- 如果修饰的是静态方法,那就是类对象锁了。
synchronized获得的锁是可重入的
- 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。一个同步方法里面可以调用另外一个同步方法(前提是锁对象一样),否则会发生死锁。
- 如下例子
//如果m1执行时,调用m2还需要再获得锁,但锁对象它已占用,所以直接可以调用,避免了死锁发生
public class T {
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
new T().m1();
}
}
程序在执行过程中,如果出现异常,默认情况sync锁会被释放
- 一个线程中抛出异常,其他线程就会进入同步代码区,注意有可能会访问到异常产生时的数据。
synchronized底层实现
- sync锁的是对象,这个对象头有两位标识当前锁的类型。
为什么锁有多种类型呢?是为了提高加锁解锁的效率 - jdk早期,sync是重量级锁(加锁需要调用操作系统内核,非常影响效率)
- 后来有了改进,有了锁升级的概念:偏向锁–>自旋锁–>重量级锁
- 偏向锁:synchronized(obj) 在obj对象头记录当前获得这个锁的线程ID,如果下次又是这个线程需要加锁了,直接根据ID认出他,加锁就行了。
- 自旋锁:如有多个线程来争用这把锁,就升级为自旋锁。一个线程在使用锁,另一个线程类似while循环,在等待着这把锁。(自旋锁特点是占CPU,但不访问内核态,是用户态执行。加锁解锁比在内核态的效率要高)
- 重量级锁:默认如果自旋了10次,还是没有得到锁,则锁升级为重量级锁,同时这个等待的线程是不自旋了,进入等待序列,不占CPU了。
AtomicXXX,lock等是自旋锁,那么什么时候用自旋锁,什么时候用重量级锁(系统锁)?
由以上分析,可以得出:
- 加锁代码部分,执行时间短,线程数少,用自旋 (AtomicXXX,lock锁)
自旋时间短,很快会得到锁,没有太耗费CPU.同时也避免了使用重量级锁。 - 加锁代码部分, 执行时间长,线程数多,用系统锁 (用sync锁)
防止大量线程自旋等待消耗CPU.
延申:什么是内核态,什么是用户态
- 比如linux操作系统有两区域内存,一块是内核的,一个是用户的。有时用户区执行时需要内核来提高服务,内核的执行需要耗费更多资源。比如线程的启动,切换,关闭需要内核的参与。