转载

从创建对象到 ConcurrentHashMap

点击上方 蓝字 可以订阅哦

其实创建对象与ConcurrentHashMap之间并没有必然联系,不过很多知识是环环相扣的,这篇文章权当做一次温习吧。

对象和锁

如下 代码, new 一个对象后, j vm会先 检查Student 是否已被加 ,若未加载则先加载, 否则在堆区创建该 对象

Student stu = new Student();

既然对象是分配在堆区,那么对象在堆区的存储结构是怎样的呢,其实主要分为三个部分:

  1. 对象头

  2. 实例数据

  3. 填充数据

这里重点看“对象头”,对象头主要存储信息如下所示。

从创建对象到 ConcurrentHashMap

可以看到,MarkWord标志位存放了对象的锁信息。

我们知道,Java中的任何对象都可以当作锁,那么锁是用来干嘛的呢。在多线程并发中,当多个线程同时访问某个共享资源时容易发生错误,如脏读(线程1读到被线程2修改的数据),因此提供锁机制来 保证线程安全

一般说来,线程对资源的访问无非就是读和写,当多个线程对资源只读时,并不会出现线程安全问题,但如果有至少一条线程写资源,就极易会出现问题。

我们大致来看一下java中的锁。

从设计思想来看 ,java锁可分为 悲观锁乐观锁 ,定义如下:

悲观锁:悲观思想,认为读少写多,每次读写资源时都会上锁,其它线程需要一直阻塞直到获取锁,java中典型的悲观锁就是 synchronizied ,AQS框架下的锁会先进行CAS(比较交换,原子操作)获取锁,获取不到才会转换成悲观锁,如 ReentrantLock

乐观锁:乐观思想,认为读多写少,在读资源时不上锁,但在写资源时会先判断资源是否被他人修改过,一般通过资源版本号来判断:若资源更新后的版本号与期望版本号一致,则未被修改,否则已被修改。

接着我们再来看下 偏向锁、轻量级锁、重量级

偏向锁:运行过程中,若只有一条线程持有锁,没有其它线程与之争夺,那么锁就会偏向于该线程,此锁称为偏向锁,此时只有单条线程,并不需同步操作,能提高程序运行性能。

如果后来有其他线程来争夺锁,那么jvm会将该持有偏向锁的线程挂起,消除它的偏向锁,将锁升级成 轻量级锁

轻量级锁:轻量级锁是相对于重量级锁来说的,使用轻量级锁时只需将MarkWord部分字节更新指向线程栈(每个线程都有自己的栈)中的Lock Record,若更新成功,则获取轻量级锁成功,否则说明目前已经有线程获取了轻量级锁,此时发生了竞争,需要升级成 重量级锁轻量级锁也称为乐观锁。

轻量级锁主要有 自旋锁自适应自旋锁

自旋锁:如果持有锁的线程能在短时间内释放锁,那么其它线程就不用进入阻塞状态( 阻塞和唤醒线程需要操作系统从用户态切换到核心态,开销较大 ),只需要等待一下(自旋),等到锁被释放再去争夺锁。但是自旋需要占用cpu,一旦自旋时间过长,则会造成cpu浪费,所以需要设置一个最大自旋时间,自旋超过最大时间的线程依然会进入阻塞状态。

自适应自旋锁:自适应意味着线程自旋时间是非固定的,会根据情况动态改变。如线程自旋很少成功获得过锁,那么以后可能会减少自旋时间,甚至忽略自旋,避免浪费cpu资源;对于刚刚自旋获得过锁的线程来说,下一次自旋获得锁的可能性较大,所以会适当增加自旋时间。

重量级锁:由轻量级锁升级而来,也称为 互斥锁 ,当系统检测到是重量级锁后,会将等待获取锁的线程置于阻塞态,不会占用cpu,但是 阻塞和唤醒线程需要操作系统从用户态切换到核心态,开销较大。 重量级锁也叫悲观锁。

这里再补充一点知识。

java中每个对象都有两个池,分别为 锁池等待池

锁池:锁被某个线程持有时,其他争夺锁的线程在该线程释放锁之前会进入锁池。

等待池:持有锁的线程在调用对象锁的wait()后会释放锁,并进入等待池。当其他线程调用对象锁的notify()或者notifyAll()后,被唤醒的线程会从等待池进入锁池。

上文说到synchronized是java中的重量级锁,它是一种 独占锁 (线程获取锁后其它线程需要阻塞),除了 synchronizedReentrantLock 也是独占锁。

synchronized

synchronized 是java中一个用来实现锁机制的关键字,可以用来修饰方法(包括静态方法和非静态方法)和代码块。

修饰静态方法时,锁住的是当前class对象;

修饰非静态方法时,锁住的是当前实例对象;

修饰代码块时,锁住的是()中的对象。

刚才说到 synchronized 是独占锁,意味着在某条线程获取到锁后,其它线程需要阻塞直到锁被释放,这样在任意时刻只有一条线程能进入临界区,显然在多线程环境中,拥有较低的并发性能,且阻塞和唤醒还需要操作系统状态的切换,开销较大,因此 synchronized 是一种典型的 重量级锁 。同时它也是 非公平锁 (多个争夺锁的线程中谁能获取锁是随机的,无论时间先后),所以多线程环境下有可能造成“ 饥饿 ”现象(指某个线程长时间未获得锁)。

synchronized 也有它的好处。我们在代码中使用它时无需手动加锁与释放锁,交由jvm和操作系统来处理即可。而且java新版本已经对 synchronized 做了 优化 ,这个后面会讲到。

ReentrantLock

ReentrantLock是java中的一个类,能实现 可重入锁 (获得锁的线程还能继续重复获取锁,常用于循环体中, synchronized也可重入 ,常用于递归迭代中),它要比 synchronized 灵活,功能也更加丰富。

ReentrantLock 也是 独占锁 ,但和 synchronized 不同, ReentrantLock 需要我们调用lock()、unlock() 手动加锁解锁 ,且加锁的次数和解锁的次数需要一致,否则其它线程可能无法获取锁。

synchronized 相比, ReentrantLock 主要有三个特点(区别)。

1. ReentrantLock 能实现 公平锁 (按照线程先来后到的顺序获取锁,可避免饥饿,但性能比非公平锁低),调用无参构造方法或者传入false为非公平锁,传入true为公平锁。

2. ReentrantLock能 实现响应中断。使用 synchronized 时,若线程拿不到锁就会阻塞直到能获取到锁,这种状态无法被中断。但是 ReentrantLock 提供了lockInterruptiably(),可将线程从阻塞状态中断,该方法可用于解决 死锁 问题。

3. ReentrantLock 能实现限时等待。利用其提供的tryLock(),传入时间参数,在指定时间内返回获取锁的结果(true or false),无参则表示立即返回。

在多线程中,线程间常需要进行一些交流,如通知等待和唤醒。

最常见的是Object类的wait()、notify()/notifyAll(),锁对象调用wait(),持有锁的线程会释放锁进入 等待池 ,其它线程调用 notify ()会随机唤醒等待池中的某个线程进入 锁池 ,或调用 notifyAll ()唤醒所有线程。

不同于 synchronized, ReentrantLock结合 Condition 接口来实现通知等待。调用Condition的await()来释放锁,其他线程调用signal()来唤醒线程,类似于wait()、notify()。

这里再说一下wait()和sleep()的区别。

从创建对象到 ConcurrentHashMap

ConcurrentHashMap

HashMap虽然性能好,可它是非线程安全的,在多线程并发下会出现问题,那么有没有解决办法呢?

当然有,可以使用 Collections.synchronizedMap() 将hashmap包装成线程安全的,底层其实使用的就是 synchronized 关键字。但是前面说了,synchronized是重量级锁,独占锁,它会对hashmap的put、get整个都加锁,显然会给并发性能带来影响,类似hashtable。

简单解释一下。

hashmap的底层是哈希表(数组+链表,java1.8后又加上了红黑树),若使用 synchronizedMap() ,那么在线程对哈希表做put/get时,相当于会对整个哈希表加上锁,那么其他线程只能等锁被释放才能争夺锁并操作哈希表,效率较低。

hashtable虽是线程安全的,但其底层也是用 synchronized实现的线程安全,效率也不高。

对此,JUC(java并发包)提供了一种叫做ConcurrentHashMap的线程安全集合类,它使用 分段锁 来实现较高的并发性能。

在java1.7及以下, ConcurrentHashMap 使用的是Segment+ReentrantLock, ReentrantLock 相比于synchronized的优点上文已经介绍过了,我们主要来看下Segment。

从创建对象到 ConcurrentHashMap

我已经在图中附了注释,所以此处便不再做文字说明。

在java1.8后,对 ConcurrentHashMap 做了一些调整,主要有:

  1. 链表长度>=8时,链表会转换为红黑树,<=6时又会恢复成链表;

  2. 1.7及以前,链表采用的是头插法,1.8后改成了尾插法;

  3. Segment+ReentrantLock改成了 CAS+synchronized

主要看第三点,为什么要改成 CAS+synchronized呢?

因为java对 synchronized 进行了优化,这些优化体现在前文所说的 偏向锁、轻量级锁和重量级锁。

这三种锁其实是优化后 synchronized 锁的类别,级别由低到高, 锁只能升级 ,不能降级。每种锁适用于不同场景,整体来看,优化后的 synchronized甚至ReentrantLock 性能要更好。

取消Segment,直接利用table数组单元作为锁,实现了可对每行数据加锁,进一步提高了并发性能。

不过即使有了 ConcurrentHashMap ,也不能忽略HashMap,因为各自适用于不同场景,如 Hash Map 适合于单线程, ConcurrentHashMap 则适合于多线程对map进行操作的环境下。

本篇文章所涉及的内容是面试高频考点,很多地方还可以深挖,请读者自行深入学习。

从创建对象到 ConcurrentHashMap

原文  https://mp.weixin.qq.com/s/dQtm1XassZXg1u7u3IqK3g
正文到此结束
Loading...