synchronized和lock(CAS)的区别
在设计多线程开发时,无可避免地需要在多个线程中操作同一个对象,例如多个线程向集合中同时读写数据,执行同一个方法。诚然,做到线程安全的最终方法就是不在线程间同步数据,但有时候同步操作在所难免。
那么,接下来我们来谈谈java提供的两种线程间安全同步数据的方法。
synchronized
synchronized能够保证多个线程在同时执行被synchronized包裹的同一代码块时,有且仅有一个线程能执行相应的代码操作,而其他的线程会被阻塞等待。
例子:
public class SyncTest {
private Integer p = 0;
private synchronized int getP(){
System.out.println("准备获得属性p");
return p;
}
private void pAndOne(){
System.out.println("准备属性p加一");
synchronized (this.p){
this.p +=1;
}
}
private void reset(){
System.out.println("重置属性p");
synchronized (this){
this.p = 0;
}
}
}
在SyncTest类中,getP方法本身是同步的,那么意味着多个线程同时调用该方法,只有一个能打印出准备获得属性p
并返回p,之后别的线程再去竞争方法的使用权。
pAndOne方法中只有p加一是同步的,并且是根据属性p同步,那么意味着多个线程同时调用该方法时,会一起显示准备属性p加一
,但同一时间段只有一个线程能够执行该操作。
reset方法和pAddOne方法差不多,但是同步是根据对象本身设置的,如果再有一个方法也根据对象本身同步,那么只要有一个线程执行根据对象本身同步的代码块,那么这两部分代码块,别的线程都是不能执行的。
实现原理
如果对jvm字节码有一定了解JAVA 虚拟机类加载机制和字节码执行引擎,我们就会知道在类和方法上都会有访问标志这一块内容,用来标记类是否是静态是否是public,方法是否是public等等。
对于方法的同步,是通过方法的访问标志ACC_SYNCHRONIZED
来控制的,即执行指定方法前会通过访问标志来判断是否需要和其他线程同步。
而对于针对对象的同步,则是通过字节指令来实现的,即先引入对象引用到当前栈,使用monitorenter
字节指令告诉虚拟机该引用需要同步,monitorexist
字节指令表示退出。
lock(CAS)
一般情况下使用synchronized已经足够了,但是我们发现它还是比较重,即每个线程在执行相关代码块时都要与其他线程同步确认是否可以执行代码。
这时候,lock和semaphore就有了用武之地。lock可以帮我们实现尝试立刻获取锁,在指定时间内尝试获取锁,一直获取锁等操作,而semaphore信号量可以帮我们实现允许最多指定数量的线程获取锁。
具体操作这里不再展开,网上示例代码还是很多的。
实现原理
我们知道synchronized会一直获取执行权限直到执行完毕,那lock能指定在一定时间内获取锁其中的原理是什么呢?
Unsafe
这里必须要提到java的unsafe不安全的方法,unsafe也是一个类,但是java不建议我们使用,必须采用不安全的方法获取unsafe类
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe)f.get(null);
} catch (Exception e) {
return null;
}
}
public native long objectFieldOffset(Field var1);
public native int getInt(Object var1, long var2);
public native void putInt(Object var1, long var2, int var4);
unsafe中的方法能够帮我们获取到一个对象的属性位于对象开始位置的相对距离,也就是说对象属性所在的地址与对象起始地址的差值。同时,还能获取一个对象指定相对距离后的数据,例如long,int,byte等等。最重要的是可以给一个特定的地址设置上数据。
compare and swap
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
在unsafe类中,支持这样一个方法,compareAndSwapInt。它的含义是给一个对象var1(开始位置+指定长度var2)的地址写入一个int值var4,如果这个地方原来的值是var5。成功返回true,不成功返回false。
这个操作是本地方法调用,而具体一点,这个方法会直接调用cpu的compare_and_swap指令,这个指令是原子性的,即操作内存中一个地址上的值不会被中断。而且多核cpu间都是可见的。
借由这样的一个本地方法调用,jdk实现了一系列轻量级的非阻塞锁以及相关应用,例如ReentrantLock,Semaphore,ConcurrentHashMap,AtomicInteger等等。
总结
面试中被问到cas的实现总是让人难以回答,针对软件工程师而言,cpu与内存等级别的问题的确也不容易掌握。而作为jvm的开发者,他们介于软件与硬件之间,利用了能够利用的cpu指令,帮软件工程师实现了非阻塞锁。而作为软件工程师,即使你不知道cpu是怎么实现cas的,但你也要知道jvm利用了cpu的指令集中特殊的指令实现了很多非阻塞锁和相关应用。