Java并发 -- StampedLock
StampedLock VS ReadWriteLock
StampedLock同样适用于读多写少的场景,性能比ReadWriteLock好
ReadWriteLock支持两种模式:写锁、读锁
StampedLock支持三种模式:写锁、悲观读锁、_乐观读_(关键)
StampedLock的写锁、悲观读锁的语义和ReadWriteLock的写锁、读锁的语义非常类似
允许多个线程同时获取悲观读锁,只允许一个线程获取写锁,写锁和悲观读锁是互斥的
但StampedLock里的写锁和悲观读锁加锁成功之后,都会返回一个stamp,然后解锁的时候需要传入这个stmap
12345678910111213141516171819202122232425public class StampedLockExample { private final StampedLock stampedLock = new StampedLock(); @Test // 悲观读锁 public void pessimisticReadLockTest() { long st ...
Java并发 -- ReadWriteLock
读多写少
理论上,利用管程和信号量可以解决所有并发问题,但JUC提供了很多工具类,_细分场景优化性能,提升易用性_
针对读多写少的并发场景,JUC提供了读写锁,即ReadWriteLock
读写锁
读写锁是一种广泛使用的通用技术,并非Java所特有
所有读写锁都遵守3条基本原则
允许多个线程同时读共享变量 – 与互斥锁的重要区别
只允许一个线程写共享变量
如果一个写线程正常执行写操作,此时禁止读线程读取共享变量
缓存1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253public class Cache<K, V> { private final Map<K, V> map = new HashMap<>(); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 读锁 private fi ...
Java并发 -- Semaphore
历史
信号量是由计算机科学家Dijkstra在1965年提出,在之后的15年,信号量一直都是并发编程领域的终结者
直到1980年管程被提出来,才有了第二选择,目前所有支持并发编程的语言都支持信号量机制
信号量模型
在信号量模型里,计数器和等待队列对外都是透明的,只能通过信号量模型提供的三个方法来访问它们,即init/down/up
init():设置计数器的初始值
down():计数器的值减1,如果此时计数器的值小于0,则当前线程将阻塞,否则当前线程可以继续执行
up():计数器加1,如果此时计数器的值大于或等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除
init/down/up都是原子性的,这个原子性由信号量模型的实现方保证
在JUC中,信号量模型由java.util.concurrent.Semaphore实现,Semaphore能够保证这三个方法的原子性操作
在信号量模型里,down/up这两个操作最早被称为P操作和V操作,因此信号量模型也被称为_PV原语_
在JUC中,down和up对应的是acquire和release
...
Java并发 -- Condition
Condition
Condition实现了管程模型中的_条件变量_
Java内置的管程(synchronized)只有一个条件变量,而Lock&Condition实现的管程支持多个条件变量
在很多并发场景下,支持多个条件变量能够让并发程序的可读性更好,也更容易实现
阻塞队列1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253// 下列三对操作的语义是相同的// Condition.await() Object.wait()// Condition.signal() Object.notify()// Condition.signalAll() Object.notifyAll()public class BlockedQueue<T> { private static final int MAX_SIZE = 10; // 可重入锁 private final Lock lock = ...
Java核心 -- Vector + ArrayList + LinkedList
概述
Vector、ArrayList、LinkedList都实现了java.util.List接口,即有序集合
Vector是Java早期提供的线程安全的动态数组,同步有一定的额外开销
Vector内部使用对象数组来保存数据,可以自动扩容(创建新的数组,拷贝原有数组的数据)
ArrayList是应用更加广泛的动态数组,本身并不是线程安全的,性能比Vector要好很多
Vector的自动扩容是增加1倍,而ArrayList的自动扩容是增加50%
LinkedList是双向链表,不是线程安全的,也不需要动态调整容量
适用场景
Vector和ArrayList都是动态数组,其内部元素是以数组的形式存储的,非常适合随机访问的场合
除了在尾部插入和删除元素,性能都比较差,因为要移动后续所有的元素
LinkedList进行元素的插入和删除操作会高效很多,但随机访问性能要比动态数组差
集合
容器包括集合和Map,_Map并不是真正的集合_
List:有序结合
Set:不允许重复元素(equals判断)
Queue/Deque:标准队列,支持FIFO或者LIFO
每种集合的通用逻辑,都被抽 ...
Java并发 -- Lock
管程
并发领域的两大核心问题:互斥 + 同步
互斥:同一时刻只允许一个线程访问共享资源
同步:线程之间的通信和协作
JUC通过Lock和Condition两个接口实现管程,其中Lock用于解决互斥问题,而Condition用于解决同步问题
再造管程的理由
Java语言对管程的原生实现:synchronized
在Java 1.5中,synchronized的性能不如JUC中的Lock,在Java 1.6中,synchronized做了很多的性能优化
再造管程的核心理由:synchronized无法破坏不可抢占条件(死锁的条件之一)
synchronized在申请资源的时候,如果申请不到,线程直接进入阻塞状态,也不会释放线程已经占有的资源
更合理的情况:占用部分资源的线程如果进一步申请其它资源的时,如果申请不到,可以主动释放它所占有的资源
解决方案
能够响应中断
synchronized:持有锁A的线程在尝试获取锁B失败,进入阻塞状态,如果发生死锁,将没有机会唤醒阻塞线程
如果处于阻塞状态的线程能够响应中断信号,那阻塞线程就有机会释放曾经持有的锁A
支持超时
如果线程在一段时间内没有获得锁,不是进 ...
Java核心 -- int + Integer
包装类
Integer是int对应的包装类,里面有一个int类型的字段存储数据,并提供了基本的操作
在Java 5,引入了自动装箱和自动拆箱(boxing/unboxing),Java可以根据上下文,自动进行转换
在Java 5,还引入了值缓存(静态工厂方法valueOf),默认缓存范围为**-128 ~ 127**
Boolean,缓存Boolean.TRUE/Boolean.FALSE
Short,缓存**-128 ~ 127**
Byte,数值有限,全部缓存
Character,缓存\u0000 ~ \u007F
自动装箱 + 自动拆箱12Integer integer = 1;int unboxing = integer++;
1234561: invokestatic // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;8: invokevirtual // Method java/lang/Integer.intValue:()I11: iconst_112: iadd13: invokes ...
Java核心 -- 动态代理
编程语言分类
动态类型和静态类型:语言类型是运行时检查,还是编译期检查
强类型和弱类型:为不同类型的变量赋值时,是否需要进行显式的类型转换
Java是静态的强类型语言,但提供了类似反射等机制,因此也具备了部分动态类型语言的能力
反射
反射机制是Java语言提供的一种基础功能,赋予程序在运行时自省的能力
通过反射可以直接操作类或者对象
获取某个对象的类定义
获取类声明的属性和方法
调用方法或者构造函数
运行时修改类定义
setAccessible
AccessibleObject.setAccessible(boolean flag):可以在运行时修改成员的访问限制
setAccessible的应用遍布在日常开发、测试、依赖注入等框架中
在O/R Mapping框架中,为一个Java实体对象,运行时自动生成getter/setter方法
绕过API的访问控制,来调用内部API
动态代理
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制
很多场景都是利用类似的机制来实现的,例如用来包装RPC调用和AOP
实现动态代理的方式
JDK自身提供的动态代理,主要利用反 ...
Java并发 -- 面向对象
封装共享变量
面向对象思想里面有一个很重要的特性:_封装_
封装:将属性和实现细节封装在对象内部,外部对象只能通过目标对象的公共方法来间接访问目标对象的内部属性
利用面向对象思想写并发程序的思路:将共享变量作为对象属性封装在内部,对所有公共方法制定_并发访问策略_
Countervalue为共享变量,作为Counter的实例属性,将get()和addOne()声明为synchronized方法,Counter就是一个线程安全的类
1234567891011public class Counter { private long value; public synchronized long get() { return value; } public synchronized long addOne() { return ++value; }}
不可变的共享变量
实际场景中,会有很多共享变量,如银行账户有卡号、姓名、身份证、信用额度等,其中卡号、姓名和身份证是不会变的
对于不可变的共享变 ...
Java并发 -- 局部变量
斐波那契数列1234567891011int[] fibonacci(int n) { // 创建结果数组 int[] r = new int[n]; // 初始化第一、第二个数 r[0] = r[1] = 1; // ① // 计算 2..n for (int i = 2; i < n; i++) { r[i] = r[i - 2] + r[i - 1]; } return r;}
方法调用过程123int a = 7;int[] b = fibonacci(a);int[] c = b;
当调用fibonacci(a)的时候,CPU需要先找到方法fibonacci()的地址,然后跳转到这个地址去执行代码
最后CPU执行完fibonacci()方法之后,要能够返回,需要找到调用fibonacci()方法的下一条语句的地址
即int[] c = b;然后跳转到这个地址去执行
栈寄存器
CPU支持一种栈结构,与方法调用相关,称为调用栈,Java虽然靠JVM解释执行,但方法调用也 ...