Java多线程之Synchronized详解

235 阅读8分钟

    一直以来对于Synchronized都比较迷惑,尤其还对于ReentrantLock并不了解他们之间的区别,今天闲来无事,学习了。

1,为什么要使用Synchronized

    首先看Synchronized关键字的作用,以及我们为什么要用Synchronized,需求在哪里?

    在程序开发过程中,当遇到多线程并发情况时,对于一些临界资源的访问便成为了一个问

题:

    一是有可能导致错误的结果(进程A尚未执行完毕,进程B强行进入改变了之前的变量,此时

A再做处理结果肯定是错误的)。例如下面,A,B两进程同时开始运行,A

//继承Runnable接口 实现run方法 打印count值
public class test implement Runnable{
     //计数器初始为0
     int count = 0;
     //count自增
     @override
     public void run(){
          system.out.println(Thread.currentThread().getName()+"_running start count:"+count);
          count++;
          Thread.sleep(100);
          system.out.println(Thread.currentThread().getName()+"_running end count:"+count);
     }
}
//若此时有A,B两个线程同时要访问
//A线程要循环100次结果

     二是有可能导致死锁或者饥饿现象发生,饥饿现象即A进程加锁之后进入临界区之后,中间

发生了异常或者执行时间无限循环,未能正确退出临界区释放锁,排队的B进程永远无法获取资

源,整个系统gg。而死锁就好比A拿一只筷子,B拿一只筷子,但是各自又都不放弃自己手中的

筷子,故谁也不能组成一双筷子,于是谁也吃不了饭。

    所以我们需要对线程访问资源做一定的处理。

2,Synchronized实现的基本原理

2.1 线程的概念以及线程同步简单的实现

    在计算机操作系统中,进程作为资源分配和任务调度的基本单位,在线程出现之后,线程便

成为了操作系统调度的基本单位。而一个进程内会有多个线程,同一个进程内的线程共享这个

进程里的所有资源,那么为了合理的访问这些共享资源,需要理解进程(线程同理)的同步以及互

斥,及PV操作和信号量机制。PV操作就是一种对于临界资源访问的处理方法。

P(signal);     //加锁
do something;  //访问临界资源
V(signal);     //释放锁

我们可以通过一个最简单的例子(不考虑死锁 饥饿问题,只是举个例子)来实现它:

//一个书架上只有一本书 一次只能拿一本 剩下的人等待
//P操作实现 实际复杂的多 此方法仅仅为基础原理
P(mutex){
       if (mutex>0){
           mutex--;
       }
}
//V操作实现 
V(mutex){
    mutex++;
}
//首先定义互斥信号量 mutex
int mutex = 1;
//对于读者
Reader:
while(true){
     P(mutex);   //首先mutex-1 加锁 
     Reading;    //阅读
     V(mutex);   //mutex+1 释放临界资源:书
}

    当然以上只是基本原理,Java中的Synchronized实现更为复杂,但是基本原理和目的大致相

同的。

2.2 Synchronized的底层实现

    在Java早期版本中,Synchronized的实现是利用操作系统内核的功能来实现,由于线程若实

现加锁,必须从用户态转为核心态,才能使操作原子化,但是调用核心态耗时较多,导致早期

版本的Synchronized效率低下,自从Java1.6开始,HotSpot对于底层JVM不断优化,使得

Synchronized的性能已经有了不错的效率表现。

    现在Synchronized的功能实现的基础是Java的对象头和Monitor。

    首先来说说Java对象头是什么鬼,每一个Java类在加载时候,JVM会给这个类创建一个

KClass实例用来保存该类的信息和数据,当在程序中new一个该类的对象时候,JVM会创建一

个OopDesc对象,此对象中包含了java的对象头和实例化数据。

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    wideKlassOop    _klass;
    narrowOop       _compressed_klass;
  } _metadata;
}

    其中的_mark就是Mark Word即标记字段,一般用存储对象中的哈希码(HashCode)、GC分

代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。

    而其中的_metadata是指向原有类的指针,代表了此对象是什么类型,哪一个类的实例。

    而Monitor顾名思义即监视器,Java中每一个类都有唯一的一个Monitor,主要是记录当前对

象的状态信息。当多线程争取锁的时候,首先所有的线程会进入一个竞争队列ContentionLIst

中,之后有资格满足条件的进入EntryList等待获取锁,当获取锁时,Monitor中的Owner属性会

设置为当前线程,count也会+1,若线程调用wait()方法,该线程会进入waitset集合中等待唤

醒,并且设置count-1,owner置为NULL。若直接运行完毕,则exit()退出。

    当java文件编译为class文件之后,在JVM中执行的过程。第33行和第109行分别显示

monitorenter.monitorexit表示进入监视器,退出监视器。以此作为tag

        33: monitorenter
        34: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        37: new           #12                 // class java/lang/StringBuilder
        40: dup
        41: invokespecial #13                 // Method java/lang/StringBuilder."<init>":()V
        44: invokestatic  #2                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        47: invokevirtual #3                  // Method java/lang/Thread.getName:()Ljava/lang/String;
        50: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        53: ldc           #18                 // String _synObjectBlock_start
        55: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        58: invokevirtual #16                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        61: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        64: ldc2_w        #19                 // long 100l
        67: invokestatic  #21                 // Method java/lang/Thread.sleep:(J)V
        70: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        73: new           #12                 // class java/lang/StringBuilder
        76: dup
        77: invokespecial #13                 // Method java/lang/StringBuilder."<init>":()V
        80: invokestatic  #2                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        83: invokevirtual #3                  // Method java/lang/Thread.getName:()Ljava/lang/String;
        86: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        89: ldc           #22                 // String _synObjectBlock_end
        91: invokevirtual #14                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        94: invokevirtual #16                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        97: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       100: goto          108
       103: astore_2
       104: aload_2
       105: invokevirtual #24                 // Method java/lang/InterruptedException.printStackTrace:()V
       108: aload_1
       109: monitorexit

2.3 Synchronized优化

    在JDK1.6之后,HotSpot对于Synchronized进行了大量优化,添加了自旋锁,自适应自旋

锁,锁消除,锁粗化等,并且划分了无锁状态、轻量锁、偏向锁、重量级锁四种状态。

    自旋锁之所以被引入,是因为对于程序运行期间,大部分线程对于锁的持有时间都是非常短

暂的,而每次当一个线程争取到锁,虽然只占用了很短暂时间,但是另一线程仍要被阻塞。频

繁的阻塞唤醒,对于CPU而言要从用户态和核心态来回转变是非常消耗时间的,也就造成了效

率的低下。那么自旋锁就是让另一线程执行无意义的循环(自旋)只是为了暂时不进行阻塞,对于

先前持有锁的进程执行完毕后它立马就可以继续运行,提高了效率。但是自旋锁是应用肯定要

分情况而言,它的自旋缺点占用了CPU的时间,优点在于提高了效率但仅在持有锁线程执行时

间很短,很快就可以释放锁的前提下,如果持有锁迟迟不释放锁,那么此操作反而会浪费CPU

的资源,相当于做无用功还占着CPU。所以我们为自旋规定的时间限度,超过一定时间没有释

放锁,则停止自旋,被阻塞。

    自适应自旋锁,就很高级哈哈。相比于自旋锁,多了“AI”,也就是更加聪明了。如果之前此

线程通过自旋提高效率,那么之后允许自旋的次数就更多。反之一样。那么随着程序的不断运

行,那么越来越多的监控数据就会让判断的准确度进一步提升,对于提高系统效率会大有帮

助。

    锁消除,就是在某些情况下,JVM检测到此共享资源并不会存在竞争,如果不加锁,既不影

响运行,又能提高效率。那么如何知道不存在竞争呢?JVM一般是对于代码中变量的数据流向

进行分析。当然对于不存在竞争的代码,我们在编写过程中也不会给代码块加上同步啊。

    锁粗化,当某一线程频繁进行对于某一资源的使用,那么就不断的加锁释放锁,这样相当的

浪费资源,降低了效率。那么此时JVM就会把加锁范围放大,例如把锁放置在for循环之外,让

此线程再访问的时候就不需要重复加锁释放锁。


   以上只是简单的对于Synchronized的几个特性和底层如何去实现的原理简单介绍,还需要多

多去学习,有可能一些地方描述的不准确,会逐步修改,争取准确。