Java中每一个对象都可以作为锁,这是synchronized实现同步的基础
当一个线程访问同步代码块时,它首先是需要得到锁,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?
同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。
上面我们提到:Java中每一个对象都可以作为锁,这是synchronized实现同步的基础,且它有三种表现形式,下面我们具体来探讨一下这三种形式。
public class SynchronizedTest {
public synchronized void test(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
}
public static void main(String[] args) throws Exception {
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程1");
}
}).start();
SynchronizedTest t1=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t1.test("线程2");
}
}).start();
}
}
从运行结果,我们可以看出线程2并没有等待线程1结束才开始运行,这说明什么?
说明对于普通方法,如果是不同的对象实例锁是不起作用的
我们把上面的代码修改一下,改为同一个实例
public class SynchronizedTest {
public synchronized void test(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
}
public static void main(String[] args) throws Exception {
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程2");
}
}).start();
}
}
可以看出来,同一个实例下,synchronized生效了。
public class SynchronizedTest {
public synchronized static void test(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
}
public static void main(String[] args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test("线程1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test("线程2");
}
}).start();
}
}
public class SynchronizedTest {
public void test(String name){
Object o=new Object();
synchronized(o.getClass()){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(name+"执行完毕");
}
}
public static void main(String[] args) throws Exception {
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.test("线程1");
}
}).start();
SynchronizedTest t1=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t1.test("线程2");
}
}).start();
}
}
上面代码都很简单,可能在文章里感觉很长,大家不妨自己去运行一下,切身感受一下。
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。
而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
Synchronized用的锁是存在java的对象头里面的。一个对象在new出来之后再内存中主要分为4个部分:
Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和“轻量级锁” :锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
值得我们注意的是锁可以升级但不能降级。
1.偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。
2.轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
3.重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。
Synchronized编码更简单,锁机制由JVM维护,在竞争不激烈的情况下性能更好。Lock功能更强大更灵活,竞争激烈时性能较好。
区别如下:
1.来源:lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
2.异常是否释放锁:synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;
而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
3.是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
4.是否知道获取锁:Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
...
condition.await();
...
condition.signal();
condition.signalAll();
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。、
独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。