介绍
使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程的上下文切换开销。对于解决内存可见性问题,Java还提供了一种弱形式的同步,就是volatile关键字。
该关键字可以确保对一个变量的更新对其他 线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器中,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值。
volatile保证内存可见性,但不保证操作的原子性。
dont be shy, just try!
StampedLock是java8在java.util.concurrent.locks新增的一个API 。
该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写:在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!
因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。
所以读写都存在的情况下,使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!
在上文中提到了Lock接口以及对象,使用它,很优雅的控制了竞争资源的安全访问,但是这种锁不区分读写,称这种锁为普通锁。为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁允许访问共享数据的并发性高于互斥锁允许的并发性。它利用了这样一个事实:虽然一次只有一个线程可以修改共享数据,但在许多情况下,任何数量的线程都可以同时读取数据。理论上,使用读写锁所允许的并发性的增加将导致相互使用互斥锁的性能提高。实际上,这种并发性的增加只能在多处理器上完全实现,并且只有在共享数据的访问模式合适时才能实现。
读写锁是适用于写少读多的场景。
Java 5中引入了新的锁机制—java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作。
Lock接口实现类:ReentrantLock可重入独占锁。
lock必须被显式地创建、锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例化。
为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁,尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。因此,采用lock加锁和释放锁的一般形式如下:
1 | //默认使用非公平锁,如果要使用公平锁,需要传入参数true |
AQS是抽象同步队列AbstractQueuedSynchronizer的简称,AbstractQueuedSynchronizer 是JUC 中通过 Sync Queue(并发安全的 CLH Queue), Condition Queue(普通的 list) , volatile 变量 state 提供的 控制线程获取统一资源(state) 的 Synchronized 工具,是实现同步器的基础组件,也是并发包中锁的底层的实现。
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如ReentrantLock、Semaphore、CountDownLatch等等。
工作机制:AQS的等待队列是基于链表实现的FIFO的等待队列,队列每个节点只关心其前驱节点的状态,线程唤醒时只唤醒队头等待线程(即head的后继节点,并且等待状态不大于0)。
CAS即Compare and Swap,是一种非阻塞算法,一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。其JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。
使用synchronized关键字可以实现线程安全性,即内存可见性和原子性,但这样会导致线程上下文切换和增加重新调度的开销。
java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这弥补了锁带来的开销问题,但volatile只能保证共享变量的可见性,不能解决读-改-写等的原子性问题。