前言
- Java 中 volatile、synchronized 和 final 实现可见性。
- Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
synchronized实现线程同步
synchonzied同步机制是为了实现同步多线程对相同资源的并发访问控制。
同步的主要目的是保证多线程间的数据共享。同步会带来巨大的性能开销,
所以同步操作应该是细粒度的(对象中的不同元素使用不同的锁,而不是整个对象一个锁)。
普通同步方法(实例方法):
锁是当前实例对象 ,进入同步方法前要获得当前实例的锁(this)
静态同步方法:
锁是当前类的class对象 ,进入同步方法前要获得当前类对象的锁
同步代码块:
锁是自定义对象,进入同步代码块前要获得自定义对象(obj)
同步是一种高开销的操作,因此应该尽量减少同步的内容:
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
new Thread() {
@Override
public void run() {
synchronized (this) {
for (int i = 'a'; i <= 'g'; i++) {
System.out.print((char) i);
}
}
}
}.start();
synchronized特点
synchronized具有可重入性,是重量级锁。
synchronized不支持中断:
如果一个线程在等待锁,那么结果只有两种,要么它获得锁继续执行,要么它就等待,即使调用中断线程的方法,也不会生效。
阻塞的代价
java的线程是映射到操作系统原生线程之上的:
如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源。
如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间。
如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁。
为了缓解上述上锁的性能问题,JVM引入了偏向锁,轻量锁,自旋锁,他们都属于乐观锁。
锁的状态总共有四种:
无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级
偏向锁
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
无竞争,只有一个申请锁的线程:
如果一个线程获得了锁,那么锁就进入偏向模式,
当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,
这样就省去了大量有关锁申请的操作,减少加锁/解锁的一些CAS操作,从而也就提供程序的性能
偏向锁运行在只有一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,
以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,
只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。
轻量级锁
无实际竞争,多个线程交替使用锁,允许短时间的锁竞争:
获取轻量级锁时:
如果获取成功,对象处于轻量级锁定状态;
否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),
当前线程便尝试使用自旋来获取锁,等待轻量锁的线程不会阻塞,它会一直自旋等待锁,这就是自旋锁。
在若干个自旋后,如果还没有获得锁,则才被挂起。它就会修改markword,修改为重量级锁,表示该进入重量锁了。
轻量级锁适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁
如果持有锁的线程能在很短时间内释放锁资源:
那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,自旋线程会停止自旋进入阻塞状态。
重量锁(synchronized)
有实际竞争,且锁竞争时间长
wait和notify实现线程间通信
wait():
使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():
使一个正在运行的线程处于睡眠状态,是一个静态方法。并不会释放锁。
调用此方法要捕捉InterruptedException异常。
notify():
唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,
并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():
唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
/**
* 线程交替打印案例
* ab1cd2ef3
*/
//对象锁
Object obj = new Object();
new Thread() {
@Override
public void run() {
//同步代码块
synchronized (obj) {
int a = 2;
for (int i = 'a'; i <= 'g'; i++) {
if (a == 0) {
try {
a = 2;
//把别人唤醒,自己再睡(自己先碎了就不能唤醒别人了)
obj.notifyAll();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print((char) i);
a--;
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
for (int i = 1; i <= 10; i++) {
System.out.print(i);
try {
//把别人唤醒,自己再睡(自己先碎了就不能唤醒别人了)
obj.notifyAll();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();