Java技术——同步锁的各种知识总结

1. 线程同步的方法  

1.1 同步方法  

就是使用synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

 

1.2 同步代码块  

即由synchronized关键字修饰的代码块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

因为同步是一种高开销的操作,如果没有必要同步整个方法,那就使用同步代码块同步关键代码即可。

 

1.3 使用volatile关键字实现线程同步

使用volatile修饰变量就相当于告诉JVM,该值可能会被很多线程访问,每次访问时都是从内存中读取,而不是从缓存中读取,因此每个线程访问到的变量值都是一样的,这也就是所谓的可见性。这样就保证了同步。

但是volatile不会提供任何原子操作,因此volatile不能代替synchronized。也不能用来修饰final类型的变量。

 

1.4 使用ReentrantLock类实现线程同步

它相对于synchronized关键字有更强的灵活性,而且在竞争比较激烈时有更好的性能,但是需要在finally代码中手动释放锁。

 

1.5 使用ThreadLocal

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

 

2.  Lock原理基石——CAS算法简述  

比如i++的操作,第一个线程先读取i的值,执行计算后获得一个新值Bi1变成2),然后CAS算法就试图将i的值从1改为2。这时如果检测到i的值未被其他线程更改(值还是1),则说明这段时间无竞争,CAS 算法将i的值变成2

若其他线程修改了变量(比如i1变成了3),那么CAS会检测到这个变化,就会放弃把i的值赋值为2。这时会获得新值3,并重新进行自己的计算。

 

CAS算法无法避免ABA问题,误认为没有竞争,可以使用版本号解决,即1A2B3A

 

3.  Synchronized的实现原理

Java中的每个对象都可以作为锁。

1)普通同步方法,锁是当前实例对象。

2)静态同步方法,锁是当前类的class对象。

3)同步代码块,锁是括号中的对象。


3.1  Synchronized的使用

public class SynchronizedTest {
    private static Object object = new Object();
    public static void main(String[] args) throws Exception{
        synchronized(object) {

        }
    }
    public static synchronized void m() {}
}

通过javap -c命令查看程序对应的字节码。

public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field object:Ljava/lang/Object

         3: dup
         4: astore_1
         5: monitorenter            //监视器进入,获取锁
         6: aload_1
         7: monitorexit              //监视器退出,释放锁
         8: goto          16
        11: astore_2
        12: aload_1
        13: monitorexit
        14: aload_2
        15: athrow
        16: return

   public static synchronized void m();
   descriptor: ()V
   flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
   Code:
     stack=0, locals=0, args_size=0
        0: return
     LineNumberTable:
       line 9: 0


3.2  对于同步代码块

可以看到编译器会在同步代码块的前后,分别会为我们添加MonitorenterMonitorexit指令

关于MonitorenterMonitorexit的官方解释如下:

//Each object is associated with a monitor. A monitor is locked if and only if it has an owner. 
//The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
//If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. 
//The thread is then the owner of the monitor.
//If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
//If another thread already owns the monitor associated with objectref, the thread blocks 
//until the monitor's entry count is zero, then tries again to gain ownership.
//The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
//The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. 
//Other threads that are blocking to enter the monitor are allowed to attempt to do so.


简单翻译总结概述如下:

每个对象有一个monitor。当monitor被占有时就会处于锁定状态,线程执行到Monitorenter指令时尝试获取monitor的所有权:

1)如果monitor的进入数为0,那么该线程就会进入monitor,然后将进入数置为1,从而该线程即占有了monitor

2)如果该线程已经占有该monitor,重新进入会使monitor的进入数加1

3)并且执行monitorexit的线程肯定是monitor的持有者。当monitorexit指令执行时,monitor的进入数减1,如果进入数变为0,占有锁的线程就会退出monitor,不再是这个monitor的持有者。

4)如果其他线程已经占有了monitor,那么这个线程就会进入阻塞状态,直到monitor的进入数为0,才会尝试去获取monitor的所有权。

 

通过上面的描述,我们知道Synchronized关键字的底层是通过一个monitor的对象来完成同步的功能,其实wait/notify等方法也是依赖于monitor对象,所以Java才规定只有在同步块/方法中才能调用wait/notify等方法。

 

3.3  对于同步方法

同步方法中依靠方法的ACC_SYNCHRONIZED标识符来实现。当同步方法被调用时,会检查方法的 ACC_SYNCHRONIZED标识符是否被设置,如果设置了,执行线程将先获取monitor后才能执行方法体,执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

综上,两种同步方式本质上都是对指定对象相关联的monitor的互斥性获取。


锁的四种状态

一般锁有4种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。

偏向锁,轻量级锁,重量级锁分别解决三个问题,只有一个线程进入临界区,多个线程交替进入临界区,多线程同时进入临界区。

 

4.1  重量级锁

Synchronized同步是通过对象内部的monitor来实现的。JVM实现线程之间的切换成本比较高,这也是为什么Synchronized效率低的原因。因此,称之为重量级锁。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,引入了“轻量级锁”和“偏向锁”。

 

4.2  偏向锁

public class SynchronizedTest {
    private static Object lock = new Object();
    public static void main(String[] args) {
        method1();
        method2();
    }
    synchronized static void method1() {}
    synchronized static void method2() {}
}

当某个线程访问method1时,会在SynchronizedTest.class对象的对象头栈帧的锁记录中存储该线程ID,下次该线程在进入同步方法2时,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行加锁和解锁操作。


4.3  轻量级锁

轻量级锁并不是用来代替重量级锁的,它的本意是为了减少多线程进入互斥的几率,并不是要替代互斥。

 

轻量级锁性能提升的依据是“大部分同步代码一般都处于无锁竞争状态”,这是一个经验数据。轻量级锁的轻就体现在,如果没有竞争,就会使用CAS操作完成锁的获取和释放,然后避免使用互斥锁的开销。但如果存在竞争。除了互斥的开销外,还要有额外的CAS操作。因此在有竞争的情况下,轻量级锁会比传统的重量级锁还要慢。这个时候轻量锁就不再有效,要膨胀为重量级锁,后面等待锁的线程要进入阻塞状态。

 

其他的一些优化

5.1 自旋锁

自旋不是锁的一种状态,只是轻锁膨胀成重锁后的一个优化动作

重锁的互斥同步操作对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转移到内核态去执行,这些操作会降低并发性能,同时很多时候synchronized代码块中逻辑简单执行速度快,为了这段时间去挂起和恢复线程并不值得。

因此,当线程在获取轻量级锁执行CAS操作失败的时候,要通过自旋来获取重量级锁的就是让等待锁的线程不要被阻塞,而是在Synchronized的边界做忙循环,这就是自旋。就是让后面请求的线程先“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。当尝试一定的次数后如果仍然没有成功获得锁,再进入阻塞状态。

(自旋锁在1.4就有只不过默认的是关闭的,jdk1.6是默认开启的)


5.2 适应性自旋锁

自旋本身也要一直占用处理器时间,如果锁被占用的时间很短,那么自旋等待的效果就会很好反之自旋的线程只能白白的占用处理器自旋,而不会做任何有用的工作,反而会给性能带来浪费。所以自旋几次没有获取到锁之后才把线程挂起。

但是JDK1.6之后采用了一种更聪明的方式——适应性自旋,简单来说就是如果在同一个锁对象上,自旋锁刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也很有可能再次成功,进而他会将自旋等待的时间相对更长一些,比如100个循环。反之,如果在这个锁上通过自旋从来没有获得过,以后获取这个锁将可能省略了自旋过程,以避免浪费处理器时间。所以说适应性自旋是对自旋的一种优化。

 

5.3 锁粗化

原则上,将同步代码块的范围尽量缩小,使得需要同步的操作尽可能的少,如果存在锁竞争那等待的线程也能很快拿到锁。但是如果在没有竞争时,对一个对象进行一系列连续的加解锁,就会造成性能的浪费,比如:

StringBuffer sb = new StringBuffer();
sb.append(1);
sb.append(2);
sb.append(3);

这种情况下,虚拟机就会进行锁粗化,以上述代码为例,就是将锁范围扩展到第一个append之前到最后一个append之后。这样只需要一次加锁就可以了。

 

5.4 锁消除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

for (int i = 0; i < 100000000; i++) {
     append("abc", "def");
}
public void append(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}

虽然StringBufferappend是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,就可以进行锁消除。


博主水平有限,如有问题请多指正。转载请注明出处为:http://blog.csdn.net/seu_calvin/article/details/68512707

    原文作者:java锁
    原文地址: https://blog.csdn.net/SEU_Calvin/article/details/68512707
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞