之前有一篇文章我们简单的谈到了Java
中同步的问题,但是可能在平常的开发中,有些理论甚至是某些方式是用不到的,但是从程序的角度看,这些理论思想我们可以运用到我们的开发中,比如是不是应该一谈到同步问题,就应该想到用synchronized
?,什么时候应该用ReentrantLock
?,是不是应该考虑用原子类
解决某些问题?那我们就来聊一聊我们如何用这个锁。
上一篇文章中第一个例子,我们后来通过对方法加synchronized
修改了代码,在这里还有一种修改方式,我们可以考虑使用原子类,这样也可以解决这个问题,我们来看看伪代码:
private static AtomicInteger i = new AtomicInteger(0);
private static void increse(){
i.incrementAndGet();
}
因为在这个问题上,不只是有线程的可见性问题,还有一个就是操作的原子性问题,说到这里,我们应该明白,在我们平常的开发中,有时候应该区分什么时候是需要原子操作,什么时候只需要可见性,那么这样我们才能够正确的判断用某种合理的方式写出合理的代码。
好,这是上次的一点小问题我们再这里重复一下,接下来我们聊一聊具体的锁。
互斥同步
synchronized
就是一个很典型的例子,这种锁也叫做可重入锁,就是一个线程持有一个相同的锁对象,可以进行重复加解锁,换句话说,持有相同锁对象的线程不会自己把自己锁住。
还有另外一个和synchronized
很相近的锁,就是我们上面提到的ReentrantLock
,这个和synchronized
一样,都提供了可重入性,这两个锁的效果是差不多的(在以前的一些比较旧的JDK版本中,并发数比较大的情况下,ReentrantLock
的性能是要优于synchronized
的),可能有些小伙伴还没有用过这种锁,那这里我们就简单说一下具体用法:
ReentrantLock lock = new ReentrantLock();
lock.lock();
// code
lock.unlock();
这里从API的层面提供了一个比较简单的写法,同时也提供了一些比较高级的特性,像信号量,线程终止等等,有兴趣的小伙伴可以去查阅相关资料去了解一下,在这里我们不深入探讨,但是有一个地方需要注意一下,就是ReentrantLock提供了两种竞争策略,一种是公平策略,另一种是非公平策略。
ReentrantLock lock = new ReentrantLock();// 非公平策略
ReentrantLock lock = new ReentrantLock(true);// 公平策略
- 公平策略与非公平策略
这里我就先举一个比较简单的例子,如果我们在公司上班,有时候我们中午会带午饭,然后需要用微波炉来加热一下,那这里就有个问题了,在我们去加热午饭的时候,如果前面有同事在排队,当然了,我们都是有素质的人,出于礼貌,我们也需要排队,等待前面的同事操作完,后面来的同事当然也需要排在我们后面,那这种情况下,我们可以称之为公平策略
。
如果在前面正在热午饭的同事,他有关系比较好的同事也来热午饭,这个时候是否可以插队,如果这位同事插队成功了,那么他就可以继续热午饭了,那么我们又得在后面等了,这种情况我们可以称之为非公平策略
。这两种策略的区别就在于,公平策略会让等待时间长的线程优先执行,非公平策略则是等待时间长的线程不一定会执行,存在一个抢占资源的问题。如果从源码的角度来看,那么我们就来简单的说一下非公平策略的执行方式。
- 非公平策略执行方式
一个线程首先会试探性的获取一次锁,如果获取到,则将当前锁设置为该线程独占,如果没有设置成功,则再次试探性的获取一次,如果还是没有成功,则将该线程加入到等待队列,后面再次等待获取,看到这里,可能有一些小伙伴不太明白了,那么非公平是体现在哪里,如果排在你前面的以为同事刚好热好午饭,然后你在后面玩手机,前面的同事还没有跟你说,“喂,该你去热午饭了。”,然后这会儿又来了一个其他人插队
,那么作为高素质的你,肯定不能和别人计较了,好了,那就等着吧,非公平策略就体现在这里了。这里实现的方式很复杂,可以一点一点去看,其中有用到AQS
,CAS
等这些比较底层的原理,值得一提的是,现在JVM
对synchronized
的优化已经相当不错,其性能表型已经和ReentrantLock
不相上下,如果从推荐的角度来说,还是推荐使用synchronized
,除非需要使用一些比较高级的特性。
非互斥同步
接下来我们再来聊一聊什么是非互斥同步,互斥同步就是指阻塞同步,就是阻塞线程让其一直等待某个锁可用的过程,那么非互斥同步刚和和这个是对立的,这里用到了一个CAS
的原理,这个是操作系统的指令,即compare and swap
,比较并交换,说的简单一点,就是如果在内存中一个值为V,然后我们又一个预估值A,然后还有一个新值B,如果V和A相同,那么就把V的值替换为B,然后返回V,这里要说一下,无论是否能替换成功,都会返回V的值,这个过程称作CAS
。这种就涉及到操作系统级别了,因为在之前的阻塞同步中,阻塞线程然后再将其恢复获得锁的过程,是比较耗费性能的,我们知道,Java中的线程是对操作系统线程的一个映射,如果我们在阻塞同步的时候,将一个线程挂起,然后再恢复,那么这里要经历一个向核心态的转换过程,消耗是比较巨大的。因此,在非互斥同步中,通过CAS这种方式,能够相对比较好的处理这个问题,但是有个问题需要明确一下,就是在Java
中,这种处理方式,是通过这个类来完成的,sun.misc.Unsafe
,这个类只能通过boot class loader
来调用,要么就是通过反射,或者通过某些方法来调用,比如AtomicInteger
类中的incrementAndGet
()方法,当然,还有很多这种方法,个人也是对CAS
这种机制理解的比较片面,还需要更加深层次的研究。到这里,我们不得不说,Doug Lea
的编码能力实属上乘,确实佩服!
锁优化
我们再来谈一谈锁优化,JVM
为我们的代码或者说其内部就做了一些优化方式,我们就来简单的说几个。
- 自旋
什么是自旋,是的,有些小伙伴也可以这么理解,就是自己旋转,当然这是开个玩笑,不过这种方式对于阻塞线程来说,是有一定的效果,简单来说,就是如果一个线程获得了锁,然后另一个线程按照以前的方式只能阻塞等待,那么JVM对这里做了一个优化,就是让这种等待的线程去执行一个循环,但是此时CPU的时间片是不会让出去的,也就是说这里的这个线程还是占有着一个处理时间,如果循环结束之后,这个锁就能获取了,那这个线程就拿到这个锁继续执行,这样做的一个目的就在于我们上面说的,线程从挂起到恢复需要像核心态的一个转换,这个性能的消耗和占用时间片的消耗比是很大的,但是同时也有一个问题就是如果自旋结束后,还是没有获得锁,那么这段时间性能的消耗就是浪费了,所以也很难权衡,因此在一些比较旧的JDK版本中,JVM是禁止使用自旋的。
- 自适应自旋
这里就是说如果一个线程在一次成功的自旋结束后,并且成功的获得了锁然后成功运行,那么JVM
会“认为”这次自旋是成功的,那么下次如果继续有线程发生自旋,那么JVM
能够判断出来是否需要让这个线程自旋来减少性能的消耗,从这里来看,JVM
还是相对比较“机智”的。
- 锁消除
这个例子在上一篇文章中曾经提到过,对于字符串的拼接,我们反编译就能看出来,如果能够判断不存在方法逃逸的情况下,那么JVM
会对这种操作转换为StringBuilder
的append
操作,可能有的小伙伴会有疑问,为什么不转换为StringBuffer
呢?因为JVM
已经能够正确判断出没有方法逃逸,那么如果再用线程安全类来处理也意义不大。
好了,这篇文章就先到这里了,这里只是很简略的聊了一下在Java
中的锁的相关知识,更深入的还有待后面继续研究。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。