一、锁
1、synchronized 关键字
synchronized是由jvm层而用c实现的,是一个可重入的、有升级性质的锁(jdk1.5之后增加的特性),升级过程入下:
无锁状态:没有任何线程竟争锁的时候,就是无锁状态。
偏向锁:当只有一个线程要申请锁的时候,synchronized由无锁状态转为偏向锁,线程拥有了锁,并且将锁相关的对像的对像头中偏向线程id改为当前线程id(锁的相关信息是存储在对像的对像头中的)。
轻量级锁:当处于偏向锁时,另一个线程来竞争锁,这时候升级成轻量级锁,轻量级锁采用自旋(cas)的方式竞争锁。
自旋是一种非常重要的知识点,java中很多地方都使用到了这种方式,比如大部分jdk实现的锁,都是通过继承AbstractQueuedSynchronizer(aqs,里面保存重入次数据属性是用volatile关键字修饰的,这个非常重要,后面再解释)实现的,aqs也是通过自旋来竞争锁的(可以看看实现源码,相对来说还是比较简洁的,没太复杂)。
开发分布式系统的时候有时候需要我们自己去实现分布式锁,也是用这种自旋的思路,借用redis或者数据库实现。
自旋的思路简单说就是修改一个变量的值,但修改前会去判断这个值是否等于之前的值,当然了,这两个操作必须是原子性的,不等就一直重复,直当操作成功。
好处就是思路简单,也不用线程等待、唤醒各种切换,只需要不停的去试就行了,坏处也很明显,只要没有得到锁就会一直重试,如果中间没有sleep等操作的话就会非常占用cpu,cpu经常100%,所有它只适用于大部分时候线程间运行是没有冲突的场景,即使冲突了,只需要几次自旋就可以取得锁。
重量级锁:就如上面所说,如果竞争很激烈,自旋的代价就很大,这时候就需要设置一个自旋的次数,即有线程循环一定次数后还没有取到锁,则不使用自旋这种方式了,直接升级为重量级锁。
重量级锁的逻辑就是当有一个线程竞争锁的时候,先让它自旋一次,如果取到锁了就把锁给它,如果没有取到,就直接挂起,然后放到等待队列,待别人释放锁的时候再竞争(线程释放锁的时候是换醒等待队列中的所有的线程,大家都一起竞争,这也是synchronized是不公平的一个原因),这样的话这个线程就释放了资源。
2、ReentrantLock
reentrantlock是jdk用java实现的一个可重入锁,通过源码可以看得到它里面包含一个sync对像,这个对像有两种实现方式,一个是公平的,一个是非公平的,都是继承了AbstractQueuedSynchronizer类;所以reentrantlock表现为公平或者非平台的,取决于初始化时传的值,默认是非公平锁。
reentrantLock的具体实现看源码更具体,它的源码并不麻烦,上面也提到过aqs的实现,其实主要的还是自旋这种方式。
3、Semaphore 信号量
初始化的时候给一个数值limit,调用acquire(int n)方法使limit减n,当limit - n < 0的时候就阻塞;信号量的作用是用来限制并发线程的数量,需要手动调用release方法把释放limit,就是把数值加回去,它的实现方式也是使用aqs,不多说,看源码。
4、CountDownLatch 计数器
初始化的时候给一个数值n,每次调用await()方法的时候减1,并阻塞当前线程,当n为0的时候唤醒所有线程(注意,n的数值是不会恢复的,当n为0了再调用await()将不再用阻塞作用,所以它是一次性的)。它的实现同样是通过aqs,一般被用在一个任务由多个线程并发去做,然后需要在每个线程完成之后汇总结果才能执行下去的场景。
5.CyclicBarrier 循环栅栏
从功能上看CyclicBarrier 其实是CountDownLatch 的升级版本,初始化的时候给定一个数据,调用await()方法阻塞本线程并减1,当数值减到0的时候唤醒所有线程并重置数量,所有它是可以循环使用的。它的实现方式和countdownlatch不同,它是使用condition和reentrantlock交互实现的,而且它里面有两个属性保存初始化的数值,因此要以重置回初始状态。
condition功能和object着不多,让当前线程等待或唤醒线程,但这里很让人疑惑,如果差不多为啥还要新搞个condition出来?其实虽然功能差不多,但个人觉得主要还是condition能够应对多条件的场景,一个lock可以有多个condition,有多个等待队列,那么在等待唤醒的时候就可以比较有针对性,当然这只是个人理解,如果有误,欢迎指出。
6.Exchanger 交换器
这是两个线程交换数据时使用的,当一个线程调用exchange方法,将进入阻塞,另一个线程也调用exchanger方法的时候马上唤醒同时交换数据。内部实现是通过threadLocal实现的,将数据保存在当前线程的map(thread的属性)中,当第二个线程调用exchanger方法的时候交的双方数据。
工作这么多年,还没有使用过这个工具。
二、原子性和可见性
1、volatile 关键字
volatile关键字是保证它修饰的对像的可见性。
主要由两个方面来体现:
1)修改立即回写。线程有自己的工作内存,运行的时候会把主内存数据复制到工作内存中,所以对工作内存中变量的修改主内存无法感知,只有回写的时候才知道,但线程并不是修改了就马上回写的,就会造成线程间不一致的情况。volatile 的作用就是使修改的变量值马上回写到主内存中,这样后续读的线程就能看到最新的值,当然在修改前已经复制了的线程的值还是原来的,它只影响修改之后的线程。
2)禁止重排序。我们平时写的代码顺序其实并非执行顺序,编译器会对代码顺序作一些优化调整,对单线程任务的话是不存在影响的,但并发环境就有可能因为顺序的调整造成共享数据被错误的修改(这些示例网上很多,也有更深层的解释如何禁止重排序的,如内存屏障等等,有兴趣的可以去找找),所以要保证属性值被正确的修改需要禁止重排序这个功能。
2、原子类型
AtomicBoolean、AtomicInteger等等,对值修改的方法都通过cas的方式来保证原子性,需要注意的是它只保证它提供的方法的原子性;同时保存值的属性变量通过volatile关键字保证可见性,如此,这些原子类型具有原子型和可见性的特点。
工作中如果担心变量在并发的时候会出现问题,用原子类型就对了