在并发环境下,多个线程会对同一个资源进行争抢。可能会导致数据不一致的问题。为了解决这个问题,很多编程语言都引入了锁机制。
通过一种抽象的锁来对资源进行锁定。
JVM运行时内存结构主要包含了程序计数器、JVM栈、Native方法栈、堆、方法区。
最下面一层中蓝色区域是所有线程共享的数据区域。
最下面一层红色区域是各个线程私有的,对于这个区域中的数据,不会出现线程竞争的问题(线程安全)。
Java堆中存放的是所有对象。
方法区中存放的是类信息,常量,静态变量相关信息。
当多个线程竞争其中的一些数据时会发生难以预料的异常情况。因此需要锁机制对其进行限制。
在Java中,每个object,也就是每个对象都拥有一把锁,锁中记录了当前对象被哪个线程所占用。
对象的结构:对象头 + 实例数据 + 对齐填充字节
Class Point就是一个指针,指向了当前所在对象类型所在方法区中的类型数据。
Mark Word存储了很多和当前对象运行时状态信息有关的数据。比如说hashcode、锁状态标志、指向锁记录的指针、偏向锁ID等等。其中最重要的是锁标志位。
Java中的synchronized关键词可以用来同步线程。
synchronized被编译后会生成monitorenter和monitorexit两个字节码指令。依赖这两个字节码指令来进行线程同步。
monitor依赖于操作系统的mutex lock来实现的。
Java线程实际上是对操作系统线程的映射。所以每当挂起或者唤醒一个线程都要切换操作系统内核态。这种操作是比较重量级的。在一些情况下甚至切换时间本身将会超出线程执行任务的时间。这样的话,使用synchronized将会对程序的性能产生很严重的影响。
但是从Java6开始,synchronized进行了优化,引入了偏向锁、轻量级锁,所以锁总共有四种状态。从低到高分别是无锁、偏向锁、轻量级锁、重量级锁。这就分别对应了Mark Word中的四种状态。
需要注意的是锁只能升级不能降级。
没有对资源进行锁定。所有线程都可以访问到同一资源。
这会出现两种情况:
如果有多个线程想要修改一个值,我们不通过锁定资源的方式而是通过其他方式来限制同时只有一个线程能够修改成功,而其他修改失败的线程将会不断重试直到修改成功。
CAS在操作系统中通过一条指令来实现,所以它就可以保证原子性。通过诸如CAS这种方式,我们可以进行无锁编程。
假如一个对象被加锁了,但在实际运行时只有一个线程会获取这个对象锁。那么我们最理想的方式就是不通过线程切换,也不要通过CAS来获得锁。
因为这样多多少少还是会耗费一些资源。我们设想的是最好对象能够认识这个线程。
只要这个线程过来,那么对象就直接把锁交出去。我们就可以认为这个对象偏爱这个线程,所以被称为偏向锁。
在Mark Word中,当锁标志位是01时,那么判断倒数第三个bit是否为1。
如果是1,那么代表当前对象的锁状态为偏向锁。否则则为无锁。
如果当前对象的锁状态为偏向锁,于是再去读Mark Word的前23个bit,这23个bit的值就是线程ID。通过线程ID来确认当前想要获得对象锁的这个线程是不是老顾客。
假如情况发生了变化,对象发现目前不只有一个线程而是有多个线程正在竞争锁,那么偏向锁将会升级为轻量级锁。
当锁的状态还是偏向锁时,是通过Mark Word中的线程ID来找到占有这个锁的线程。
那么当锁的状态升级到轻量级锁时,如何判断线程和锁之间的关系呢
当一个线程想要获得某个对象的锁时,假如看到锁标志位为00那么就知道它是轻量级锁。
将前30个bit变为指向线程栈中锁记录的指针。
当一个线程想要获得某个对象的锁时,假如看到锁标志位为00,那么就知道它是轻量级锁。
这时线程会在自己的虚拟机栈中开辟一块被称为Lock Record的空间(线程私有的)。
Lock Record中存放的是对象头中的Mark Word的副本以及owner指针。
线程通过CAS去尝试获取锁。一旦获得那么将会复制该对象头中的Mark Word到Lock Record中。并且将Lock Record中的owner指针指向该对象。
另一方面,对象的Mark Word的前30个bit将会生成一个指针指向线程虚拟机栈中的Lock Record。
这样一来,就实现了线程和对象锁的绑定。他们就互相知道了对方的存在。
这样这个对象就已经被锁定了,获取这个对象锁的线程就可以去执行一些任务。
这时候万一有其他线程也想要获取这个对象,此时其他的线程将会自旋等待。
可以理解为轮询。线程自己在不断地循环去尝试着看一下目标对象的锁有没有被释放。如果释放了,那么就去获取。如果没有释放,那么就进入下一个循环。
这种方式区别于被操作系统挂起阻塞,因为如果对象的锁很快就会被释放的话,自旋就不需要进行系统中断和现场恢复。所以它的效率更高。
自旋相当于CPU在空转。如果长时间自旋将会浪费CPU资源,于是出现了一种叫做“适应性自旋”的优化。
简单来说,就是自旋的时间不再固定,而是由上一次在同一个锁上的自旋时间以及锁状态,这两个条件进行决定的。
举个例子,比如说在同一个锁上,当前正在自旋等待的线程刚刚已经成功获得过锁。但是锁目前是被其他线程占用。那么虚拟机就会认为这次自旋也很有可能会再次成功。进而它将允许更长的自旋时间。这就是适应性自旋。
假如此时有一个线程正在进行自旋,那么这个线程将会进行等待。如果同时有多个线程想要获得这个对象锁。也就是说一旦自旋等待的线程超过1个,那么轻量级锁将会升级为重量级锁。
需要通过Monitor来对线程进行控制。
此时将会完全锁定资源,对线程的管控也最为严格。
可以通过Synchronized来同步线程。
四种锁的状态以及实现方式等。
参考资料:【Java并发】月薪30K必须知道的Java锁机制