利用锁的机制来实现同步。

锁具有两种特性:
互斥性:在同一时间内只允许有一个线程持有同一个对象的锁,其他线程只能进入阻塞状态等待,这样既可保证原子性,即当前线程完整的操作完,不会被其他线程抢到执行权
可见性:必须保证在操作完资源后释放锁后,其他线程可以再获取锁后可以感知到共享变量发生过改变,不会使用工作空间的本地缓存,重新去主内存中获取一份被上个程序修改过后的数据进行操作

synchronized的使用方式

  • 加在静态方法上
    • 即代表锁住的是方法区中的class对象
  • 加在实例方法上
    • 锁住的是当前对象
  • 代码块

    • 锁住的是代码块中的内容,使用一个变量作为条件,一般使用this,静态方法使用本类的class对象

      monitor

      在java中,每个对象都会有一个monitor属性,这个属性相当于一个监视器,我们对对象加锁实际上就是使monitor这个计数器+1,执行完成过后再-1,当monitor非0的时候,其他线程只能等待,只有当monitor为0时才可以抢夺到执行权进入运行阶段,线程执行完毕后会减-1而不是置0的原因是因为可重入锁的存在,即嵌套锁,对一个对象的monitor加1后再使用同步代码块+1,都执行完后才可以让别的线程执行。

      jconsole

      在程序运行期间,可以在dos窗口使用java的jconsole命令进行线程的堆栈跟踪,可以清晰的看到各个线程的状态和执行过程。

      Jvm指令

      使用javap -v 字节码文件名 对一个class文件反编译,java是一门解释型语言并不能只能操作硬件,而是通过jvm解释指令执行。
      Monitorenter和Monitorexit
      Monitorenter在反编译后的文件中代表从这行以下的代码都是具有原子性的,直到遇到Monitorexit结束。
      在反编译后的字节码中,有两个Monitorexit,一个代表正常执行退出,一个代表异常执行退出。
      对方法加锁时,使用的是标记,字节码中有ACC_SYNCHRONIZED标记。

      jvm对synchronized的优化

      在jdk6之前,synchronized是重量级锁,强占用性,这样会损失一些性能。
      而6之后进行了优化,即锁的升级过程。
      一个对象由对象头,实例变量,填充数据等构成。
      对象头中保存了对象的hashcode,对象的年龄,锁信息等。
      无锁状态 :没有加锁,可并发访问
      偏向锁:对象第一次被某个线程占用时,会记录这个线程的id,并不会释放锁,在下次线程再来访问时就可以执行执行,不需要判断对象头中的锁标志位,此时性能是很高的。
      轻量级锁:当偏向锁发生了竞争时,那么偏向锁升级为自旋锁,此时进行几次空循环,适用于线程交互使用。 会浪费一些cpu时间
      重量级锁:强互斥性,此时别的线程只能阻塞等待。
      在jdk1.6之后,为了减少锁的获取和释放,引入了偏向锁和轻量级锁的概念。
      在synchronized中,锁分为四种状态,并且是在一定条件下会进行升级的。
      无锁,偏向锁,排他锁,重量级锁。
  • 当一个线程第一次进入到同步代码块中,首先获取锁的markword,判断ThreadId是不是为空,并且biased_lock是否为1

  • 如果为空,biased_lock为1,那么就说明是可偏向状态。
    • 线程会通过cas操作,将ThreadId设置为自己的线程ID,然后执行同步代码块,在下一次执行的时候不需要获取锁就能执行
    • 获取失败,说明有别的线程已经获取到了偏向锁,说明存在线程之间的竞争关系,此时会撤销已经获得偏向锁的线程,并且在没有线程执行字节码文件的时候将偏向锁升级为轻量级锁
  • 如果是已偏向状态,那么那么会检查markwrod中保存的threadID是否为自身
    • 如果不是,锁升级
    • 如果是,不需要获取锁的步骤
    • 在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁
  • 轻量级锁属于自旋锁,是一种乐观锁,当线程获得了锁的时候,另外的线程就不会阻塞,而是在原地自旋cas获取锁,直到获取成功。
    • 当我们的同步代码块执行时间很快的时候,自旋操作反而比起线程挂起更节约cpu性能
    • 自旋锁默认是自旋十次,可以使用参数设置,在jdk1.6后加入了自适应自旋锁的概念。
    • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
  • 轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

重量级锁是悲观锁,自旋锁是乐观锁。
升级到重量级锁的时候线程就只能阻塞了,并且需要在操作系统中的用户态和内核态来回切换,很耗性能。
wait是进入阻塞状态,进入到等待队列。会释放锁,不会释放cpu资源
sleep进入等待状态,不释放锁,释放cpu资源。