1、synchronized的三种应用方式
- 修饰实例方法
作用于当前实例加锁,进入同步代码前要获得当前实例的锁 - 修饰静态方法
作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 - 修饰代码块
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
以上3个都说明sync是一种对象锁,它锁住的是对象,
1:类锁, 如果在在类的static方法上加synchronized关键字,那么锁住的就是类对象
比如:
public class Test extends Thread{
@Override
public void run() {
synClass();
}
//类锁,实际是锁类的class对象
private static synchronized void synClass(){
System.out.println("synClass going...");
System.out.println("synClass end");
}
public static void main(String[] args) {
//新建2个实例,但run()方法里面调用static sync方法,锁住的是类对象,拿到锁的线程执行完后,才会执行第二个线程
Test synClzAndInst = new Test ();
Thread t1 = new Thread(synClzAndInst);
Test synClzAndInst1 = new Test ();
Thread t2 = new Thread(synClzAndInst1);
t2.start();
t1.start();
}
}
2:如果在非static 方法上加锁,那么必须锁住同一个实例对象才有效
@Override
public void run() {
instance(); //非staic 方法
}
public static void main(String[] args) {
Test synClzAndInst = new Test ();
Thread t1 = new Thread(synClzAndInst);
Thread t2 = new Thread(synClzAndInst);
t2.start();
t1.start();
}
synchronized作用于实例方法
public class Sy1 implements Runnable {
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void add(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
add();
}
}
public static void main(String[] args) throws InterruptedException {
Sy1 instance=new Sy1();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果 2000000
上述代码开启了2个线程,但传的都是同一个实例对象,当前线程的锁便是实例对象instance。
还有一点需要注意:
当一个线程正在访问一个对象的synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法。
synchronized作用于静态方法
public class Sy2 implements Runnable {
//共享资源(临界资源)
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
*/
public static synchronized void add(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void add1(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
add();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Sy2());
Thread t2=new Thread(new Sy2());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出 2000000
synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的add1方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同。
synchronized作用于代码块
public class Sy3 implements Runnable {
static Sy3 instance=new Sy3();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
//this,当前实例对象锁,和实列一相同
/*synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}*/
//class对象锁,和实列二相同
/*synchronized(Sy3.class){
for(int j=0;j<1000000;j++){
i++;
}
}*/
}
public static void main(String[] args) throws InterruptedException {
Sy3 sy3 = new Sy3();
Thread t1=new Thread(sy3);
Thread t2=new Thread(sy3);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
2、synchronized底层含义
synchronized代码块底层含义
将下面代码用javap -c -s -v -l Test.class
反编译
public class Test {
public int i;
public void syncTask(){
synchronized (this){
i++;
}
}
}
得到
Classfile /D:/myFile/code/thread/target/classes/com/common/clockbone/synchronize
dclass/Test.class
Last modified 2019-1-24; size 535 bytes
MD5 checksum 95d2a6f48a1d0f42cba99908bd7c68eb
Compiled from "Test.java"
public class com.common.clockbone.synchronizedclass.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//……
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 4
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 this Lcom/common/clockbone/synchronizedclass/Test
;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class com/common/clockbone/synchronizedclass/Test, class ja
va/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "Test.java"
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
synchronized方法底层含义
将下面代码用javap反编译
public class Test {
public int i;
public synchronized void syncTask(){
i++;
}
}
得到
Classfile /D:/myFile/code/thread/target/classes/com/common/clockbone/synchronize
dclass/Test.class
Last modified 2019-1-24; size 423 bytes
MD5 checksum b2ddc55c47f9d590976420a19692368a
Compiled from "Test.java"
public class com.common.clockbone.synchronizedclass.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//……
public synchronized void syncTask();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 8: 0
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/common/clockbone/synchronizedclass/Test
;
}
SourceFile: "Test.java"
synchronized同步块代码:使用monitorenter和monitorexit指令实现的
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处
synchronized方法:则是则 是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一 个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个 线程获取到由synchronized所保护对象的监视器。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用 时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获 取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED 状态。
synchronized用的锁是存在Java对象头里的。
任意线程对Object的访问,首先要获得 Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。获得了锁的线程释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新 尝试对监视器的获取。
synchronized和 java.util.concurrent.locks.Lock 的异同
相同点:lock能完成synchronized所实现的功能
不同点:lock有更精确的线程语义和更好的性能
sync自动释放锁,lock需手动在finally块中显示释放锁,lock的trylock方法可非阻塞的方式去拿锁
sync和cas不同,为什么大部分情况下cas比sync性能要高
sync加锁实现,多线程争夺锁的情况下有会有上下文切换
上下文切换:多线程在执行sync时,最终只有一个线程执行,其它线程等待,cpu会把这其它线程挂起,当线程行完sync方法释放锁后,cpu会唤醒其它等待的线程,这些线程又会争夺锁…… cpu将线程被挂起和唤醒的(移出cpu和移进cpu)过程就是上下文切换。
cas :通过 获取旧值-》修改旧值-》比较主存旧值和工作内存旧值是否相等-》相等将新值写回主存-》不等再循环一次。cas 是通过比较和循环的方式实现的没有上下文切换,所以cas一般比sync快。
什么情况下cas比sync慢?
很多线程竞争的情况下,cas while循环开销也较高
当一个线程要获取锁,这个锁没有被别的线程获取过,那么这个锁就偏向这个线程
同一个线程再次去获取这个对象,会比较 是否偏向这个线程 如果是 那么直接调
执行完成
3、synchronized 锁四种状态及膨胀过程
无锁 不可偏向
2种情况 不可偏向, 这个对象计算了hashcode
如果计算过hashcode , 那么线程id存不下了,那么 就不可能再偏向了
无锁 可偏向
如果对象是无锁状态
对象包括:对象头 实例数据 对齐填充
对象头: gc 年龄 , hashcode , 锁状态
偏向锁----记录当前获取锁Id-----这个锁只有从头到尾一个线程访问
当一个线程要获取锁,这个锁没有被别的线程获取过,那么这个锁就偏向这个线程
同一个线程再次去获取这个对象,会比较 是否偏向这个线程 如果是 那么直接调
执行完成
轻量锁-----
没有资源竞争 -----多个线程 交替执行(线程A 获取锁–执行—释放锁;线程 B 才开始获取锁,线程B不会在 A还持有锁 就去获取锁,) 轻量锁性能比 偏向锁 要低,因为它要进行一个compareSwap 替换,轻量锁会把获取锁的线程信息 存起来,再来一个线程t2,因为t1已经把锁升级成轻量锁,它们没有资源竞争, t2获取锁后,会把线程信息修改成t2。
重量锁
如果t2在t1还未释放锁的时候 去获取锁,那么它们间会有资源竞争,就是升级成重量锁
升级和膨胀不一样
锁的膨胀是不可逆的,但实际是可以的,但条件比较苛刻
4、synchronized Interger问题
public class SynchronizedTest {
public static void main(String[] args) {
Thread why = new Thread(new TicketConsumer(10), "why");
Thread mx = new Thread(new TicketConsumer(10), "mx");
why.start();
mx.start();
}
}
class TicketConsumer implements Runnable {
private volatile static Integer ticket;
public TicketConsumer(int ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
synchronized (ticket) {
System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
if (ticket > 0) {
try {
//模拟抢票延迟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
} else {
return;
}
}
}
}
}
运行上面代码:可能会得到结果
一开始抢第10张票的时候,只有一个线程能获到锁。但第9张票时,why、mx同时抢到了锁。
1.为什么 synchronized 没有生效?
2.为什么锁对象 System.identityHashCode 的输出是一样的?
why 线程执行了ticket--
操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。
所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。
而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。
好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?
当执行了ticket–后,ticket对象变了。
于是改成如下:
synchronized (TicketConsumer.class)
4.1、tikcet-- 为什么会导致integer对象发生变化
我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf 方法。具体其实就是 ticket--
的这个操作。
对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。
4.2、他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。
标号为① 的程序段,目的:先从缓存中获取,如果缓存中没有则从数据库获取,然后再放到缓存里面去。
如果并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。
于是想用标号 ② 的程序段 来解决这个问题。用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。
在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。
其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。
但是很明显,他的 id 范围肯定比 Integer 缓存范围大。
如何解决上面问题? 如果是我们自己怎么加锁 防止并发。可能会想到redis分布式锁吧。那就没有上面那个问题。如果不用分布式锁呢?
如果你真的必须用 Integer 作为锁,那么你需要搞一个 Map 或 Integer 的 Set,通过集合类做映射,你就可以保证映射出来的是你想要的明确的一个实例。而这个实例,就可以拿来做锁。
然后他给出了这样的代码片段:
就是用 ConcurrentHashMap 然后用 putIfAbsent 方法来做一个映射。
比如多次调用 locks.putIfAbsent(200, 200),在 map 里面也只有一个值为 200 的 Integer 对象,这是 map 的特性保证的,无需过多解释。
其实上面目的就是 构建一个结果缓存。
还有其它解决思路。
4.2、构建高效且可伸缩的结果缓存
四个方案如下,逐步优化:
案例1 使用HashMap和同步机制开初始化缓存
案例2 用ConcurrentHashMap替换HashMap
案例3 基于FutureTask的Memoizing封装器
案例4 Memoizer的最终实现
可以参考