最近复习一下基础,查缺补漏,输出倒逼一下输入. 以后忘了再来翻翻,看看自己输出的怎么样,反复卤煮好吧!
1 首先是几个基本特性:
-
一把锁只能被一个线程获取, 一旦被获取后, 其他的线程只能等待,锁释放后才可重新获取.
-
每个实例对象都有各自的锁, 不同实例对象互补影响,除非两种特殊情况,一种是直接锁在类上(*.class),或者是用synchronized修饰的static静态方法,这时的对象就是共用一把锁
-
synchronized修饰的方法, 不管是正常流程结束,还是抛异常,都会释放锁.
然后是敲的一些代码例子
public class SynchronizedObjectLock implements Runnable{
//静态对象
static SynchronizedObjectLock instance1 =new SynchronizedObjectLock();
public void run1() {
//对象锁, 使用对象时被抢占,只能等
//如果不锁就各跑各的了
synchronized (this){
System.out.println("我是线程"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束");
}
}
Object block1=new Object();
Object block2=new Object();
public void run2() {
// 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行
synchronized (block1){
System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
}
//如果两块代码块用的是同一把锁,跑到这里时,如果锁也是block1
//如果另外一个线程在上面占用了block1锁住了,那么这一块就得等另外一个线程把上面跑完释放锁,才能走
synchronized (block2){
System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");
}
}
static SynchronizedObjectLock instance2 =new SynchronizedObjectLock();
public void run3() {
method();
}
@Override
//类锁
public void run() {
// 所有线程需要的锁都是同一把
synchronized(SynchronizedObjectLock.class){
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
//修饰普通方法 锁的是当前对象this
//线程0 和线程1 都是走各自对象的方法,所以互不影响
private synchronized void method1() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
//修饰静态方法,锁的是这个class类
//所以哪怕用不同的对象,还是会被锁住
//这里线程1 就得等线程0跑完
private synchronized static void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
//使用同一个对象
/*Thread thread0 = new Thread(instance1);
Thread thread1 = new Thread(instance1);*/
Thread thread0 = new Thread(instance1);
Thread thread1 = new Thread(instance2);
thread0.start();
thread1.start();
}
}
2 synchronized原理分析
2.1 加锁和释放锁的原理
现象、时机(内置锁this)、深入JVM看字节码(反编译看monitor指令)
public class SynchronizedDemo2 {
Object object = new Object();
public void method1() {
synchronized (object) {
}
method2();
}
private static void method2() {
}
}
反编译后:
主要关注一下monitorenter和monitorexit
拆开来看就是monitor enter 和exit 进入和退出锁
进入时锁+1 退出时锁-1
每一个对象在同一个时间上只能和一个monitor关联
一个monitor在同一个时间上只能被一个线程获得
当一个对象在尝试获得这个关联的monitor时, monitorenter会发生以下三种情况之一
-
当monitor的计数器为0时, 线程A进来会给monitor加1,并记录一下进来的是我线程A
-
当A进来时,发现monitor上面的计数器为1, 上次的记录是我线程A,我又进来了,我是老客户,计数器再加1,变成了2,再进来再加1,因为他是可重入锁.
-
当B进来了,发现monitor上次是被其他线程获取了,我就得等等,等着锁释放干净以后再来抢,因为他是不公平锁.
这里会出现2和3两种情况就是因为可重入锁的便利之处,如果是A继续,就不用释放锁,继续跑就行了,如果是B就得释放锁.
就像去酒店,A进来502睡了一晚,搞了两朵大红花贴墙上,老板记下来,今晚A来睡过一次,他出去了走了.
第二天又想来继续睡,可以不用大整,稍微收拾一下卫生就行了.
然后第三天,B来了,我觉得不行,这两朵大红花算什么玩意啊,给我贴个米老鼠,所以就得等酒店人员把A的一些布置清理掉,给B贴个米老鼠才行 ,这样B才能住的舒服满意.
然后B住进来了,之前A的连续居住记录就没用了,清掉,换成记B居住1天,这样明天还是B的话,米老鼠就不用拆掉了!
继续继续
2.2 保持可见性原理
内存模型和happens-before规则
有点懵逼先跳过
3 JVM中锁优化
3.1 锁的类别
从java se 1.6开始 引入了一些锁的优化,并对锁进行了一些分类,让锁不是那么的重.
锁的膨胀方向:
无锁 => 偏向锁 => 轻量级锁 => 重量级锁(此过程不可逆)
锁的优化
- 锁粗化
- 锁消除
- 轻量级锁
- 偏向锁
- 自旋锁
3.2 自旋锁
引入背景: 其实提到synchronized ,第一个印象就是重, 尤其是在锁优化前.
当多线程在竞争锁时,一个线程取得锁以后,其他的线程都将被阻塞,对性能造成了极大的影响.
挂起线程和恢复线程的动作都需要转入到内核状态中完成,对系统的并发能力造成了很大的压力.
同时HotSpot团队注意到, 共享数据的锁定状态只会保持很短的一段时间,为了这么点时间去挂起和恢复线程很不值得.
如今在多处理器的环境下,完全可以让另外一个没用获取到锁的线程在门外等一会儿(自旋 ) ,但不放弃CPU的执行时间.
如果锁很快就被释放,那么另外一个线程就可以直接进入,节省了挂起和恢复的动作.
而这个自旋, 只需要让线程去执行一个忙循环,也就是自旋, 这就是自旋锁的由来, 类似while(true)
3.3 自适应自旋锁
在JDK1.6中引入的, 前面自旋锁的自旋时间是固定的,不够智能.
自适应自旋锁会根据上一个 自旋锁做判定, 比如上一个自旋锁等待成功获取了锁,且这个获取到锁的线程正在运行中.
那么JVM就会认为获取自旋锁的概率很高, 可以再加点自旋的次数,比如多循环100次.
但如果自旋很少成功获得到锁, 那么JVM甚至可以减少,甚至放弃自旋,来避免浪费处理器资源.
3.4 锁消除
参考文档: 传送门