syschronized底层实现(1)
理解锁的基本知识
乐观锁与悲观锁
乐观锁
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现)。
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。如果持有锁的线程执行实践超过竞争线程的最大自旋等待时间,则会进入阻塞状态。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized。
偏向锁
偏向锁假定自始至终都只有一个下线程在使用锁,并且没有其他实际竞争线程,需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁。当线程执行过程中有其他线程申请锁,则偏向锁改为轻量级锁。**使用偏向锁的目的是:减少无竞争且只有一个线程在使用锁的情况下,使用轻量级锁所产生的性能消耗。**偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
用户态和内核态
-
内核态(Kernel Mode):运行操作系统程序
-
用户态(User Mode):运行用户程序
用户态和内核态是操作系统的两种运行级别,特权级别分为R0(内核态),R1,R2,R3(用户态)。
区别:
处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ;
而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
导致用户态切换到内核态的情况:系统调用,异常,外围设备的中断。
Java线程阻塞的代价
synchronized会导致争用不到锁的线程进入阻塞状态。Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换。这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
Java对象结构
对象头
HotSpot虚拟机的对象头包括两部分信息:
markword
第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。
klass
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
以上内容转载自:https://blog.csdn.net/zqz_zqz/article/details/70246212
CAS
CAS(compare and swap),比较和交换,是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
链接:https://www.jianshu.com/p/059529726918
到这里,已经对Java的各种锁以及了解syschronized底层实现前需要了解的基本知识作出了详细介绍,关于类似的介绍网上有很多,这里引用了比较通俗易懂的,感谢各位大佬的总结!
syschronized简介
下面我们引入一个案例。售票时会有多个售票窗口,在销售票的过程中极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至为负数。这些“意外”都是有多线程菜哦做共享资源而导致的线程安全问题。这些售票窗口就相当于多个线程,为了解决这些问题,我们必须得保证在任何时刻只能有一个线程访问共享资源。于是我们就使用关键字syschronized。
syschronized经典卖票案例:
public class TicketsCodeBlock implements Runnable {
private int count = 100; //总票数目
public static void main(String[] args) {
TicketsCodeBlock tickets = new TicketsCodeBlock();
Thread t1 = new Thread(tickets);
Thread t2 = new Thread(tickets);
Thread t3 = new Thread(tickets);
t1.start();
t2.start();
t3.start();
}
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (this) { // 同步代码块
if (count > 0) {
try {
Thread.sleep(500); // 500ms
} catch (Exception e){
e.printStackTrace();
}
Thread t = Thread.currentThread();
System.out.println(t.getName() + " 剩余票的数目: " + (count--));
}
}
}
}
}
我们使用syschronized(lock)来处理共享资源,保证任何时刻只能有一个线程进入同步代码块。lock是一个锁对象,它是同步代码的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块并执行代码。循环外服,知道共享资源被处理完为止。这个过程就好比一个公用电话亭,只能前一个人打完电话出来后,后面的人才能打。
syschronized就相当于重量级锁,当有一个线程获得锁时,其他申请锁的线程都必须进入阻塞等待状态,上述对线程阻塞的代价进行了简要说明,阻塞和唤醒的过程都要进行上下文切换,这种切换会消耗大量的系统资源。