关于一些基础的Java问题的解答(十)

上一篇文章的传送门:关于一些基础的Java问题的解答(九)

偏向所锁,轻量级锁及重量级锁

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁,偏向第一个线程。
这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。
但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

综上所述,三种锁的状态如下:

  • 偏向锁:首次同步访问时加入,在对象头表明偏向线程id
  • 轻量级锁:检查偏向锁持有线程仍需要持有锁后升级,自旋处理
  • 重量级锁:升级为轻量级锁后,自旋超过一定次数,或者出现第三个竞争线程时升级,阻塞处理

更详细内容可参考:https://www.cnblogs.com/deltadeblog/p/9559035.html

线程池线程数如何确定

Java的线程池相信大家都不陌生,为了方便线程管理与复用,我们一般都会有使用线程池的习惯
那么当我们创建线程池的时候,核心线程数如何确定呢?
如果线程池的线程数定的太少,会导致CPU空转浪费资源,但如果线程池的线程数定的太多,则会导致CPU把大量的计算时间浪费在上下文切换上

关于线程数的确定,这里给出一个公式:

1
线程数 = CPU核心数 * (1 + cpu等待时间/cpu总时间) + 1

博主个人是这样理解公式的,因为我们不能让CPU空转,因此线程数至少等于CPU核心数
然后在考虑线程出现等待的情况较多的情况下,比如频繁io或者网路请求的场景,这种情况我们肯定是要增加线程数量的,否则CPU就会因为线程都是处于等待状态而空转,因此我们加上 CPU核心数 x cpu等待时间/cpu总时间 的线程数
最后,考虑线程执行逻辑出现异常的情况,我们可以增加一个线程,在其他线程出现异常的情况下,使得CPU不会空转

而且,根据上面的公式,我们还可以推导出两个比较经典的场景:

  • CPU密集型场景,即cpu等待时间/cpu总时间比值接近0,因此线程数一般定为 CPU核心数 + 1
  • IO密集型场景,即cpu等待时间/cpu总时间比值接近1,因此线程数一般定为 CPU核心数 x 2 + 1

参考资料:
https://www.cnblogs.com/dennyzhangdd/p/6909771.html?utm_source=itdadao&utm_medium=referral
https://blog.csdn.net/varyall/article/details/79583036?utm_source=blogxgwz8

反射创建对象 与 new 创建对象区别

在java中,我们既可以通过new来直接创建一个对象,也可以通过反射的api,使用Class.forName().newInstance()来创建对象
那么这两者有何区别呢?

当我们通过new来创建对象时,在编译期间内,对于我们创建对象的类型以及调用的构造函数方法,都是明确可知的
因此这种方式创建对象会更快,而且JVM也会针对这种情况执行一系列的优化

当我们通过Class.forName().newInstance()来创建对象时,我们相当于使用了一种动态的方式来创建对象
这种创建对象的方式会更慢,因为这时创建的对象类型并不是明确的,是不能硬编码到字节码中的
并且JVM可能还需要进行权限的校验,确保执行的代码是否能调用构造函数创建对象
并且JVM还需要对调用的构造函数进行检查,确保拥有跟我们参数匹配的构造函数

综上所述,反射创建对象 慢于 new 创建对象