一些给锁的实现者来参考的特性
悲观锁:做出最坏的打算,预期锁冲突的概率很高,使线程每次拿数据的时候都会上锁
乐观锁:做出最好的打算,预期锁冲突的概率很低,在数据将要进行更新提交的时候才会检查是否存在并发冲突
读写锁是将加锁的操作进行了细化,将加锁操作细化成了"加读锁" 和 “加写锁”.
其中
重量级锁: 锁的开销比较大,做的工作比较多,其主要应用了操作系统(内核态提供的锁),容易产生线程调度,造成线程阻塞.
轻量级锁: 锁的开销比较小,做的工作比较少.其主要避免使用操作系统提供的锁,尽量在用户态完成功能
上述中的悲观锁经常会使重量级锁,乐观锁经常会是轻量级锁
自旋锁: 自旋锁是轻量级锁的具体实现,也是乐观锁.当发生锁冲突时,线程不会立刻阻塞,反而会再次尝试获取锁.
特点:
挂起等待锁: 挂起等待锁是重量级锁的具体实现,也是悲观锁.但发生锁冲突是,线程会进入阻塞状态.
特点:
公平锁: 遵循"先来后到"的原则,线程谁先来的,谁就先获取到锁
非公平锁: 不遵循"先来后到"的原则,根据操作系统内部对线程的随机调度.
可重入锁: 一个线程可以多次获取同一把锁
不可重入锁: 一个线程无法获取同一把锁
可重入锁在内部记录了这个锁是哪个线程获取到的, 如果发现尝试获取锁的线程和持有锁的线程是一个线程, 就不会产生堵塞, 而是直接获取到锁
同时还会给锁的内部加上一个计数器,记录当前是第几次加锁,并根据计数器来决定什么时候释放锁.
“compare and swap” 全称"比较并交换", 是基于硬件,给JVM提供的一种更轻量的,原子操作的机制
比较,就是比较内存和寄存器中的值,如果值相同了,就把内存中的值和另一个值交换
假设内存中的原数据V, 旧的预期值为A, 需要修改的值为B
真实的CAS是一个原子指令完成的,这个伪代码只是辅助理解CAS的工作流程
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
其中address为待比较的值的地址, expectValue为预期内存中的值,swapValue为希望内存变为的值,&address为取出内存中的值作比较.
上述代码能完成的操作,可有cpu提供的CAS指令实现, 硬件提供了支持,软件方面才得以实现
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
// 这个方法相当于 count++
count.getAndIncrement();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();;
System.out.println("count = " + count);
}
CAS 这样的操作,不会造成线程阻塞。比 synchronized 更高效。
基于 CAS 实现更灵活的锁, 获取到更多的控制权.
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
当owner为null时, CAS成功, 循环结束
当owner为非null时,说明当前的锁已经被其他线程占用了,则要继续循环.
CAS是要先比较值,然后完成交换,比较是在比较当前值和旧值是否相同, 如果这两个值相同,就视为没有修改过.但其实这两个值中间过程可能发生了修改,也可能没有发生修改.
解决这样的问题,我们就要引入版本号,如果发现当前的版本号和之前读到的版本号相同,就执行操作,并修改版本号.如果版本号和预期的不同,则修改失败.
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
JVM和编译器判断锁是否可以消除,如果可以,直接消除.
有些程序的代码,在单线程的环境下用到了 synchronized.例如单线程下的StringBuffer的append.
一段代码中如果出现多次的加锁解锁,JVM和编译器会自动完成锁的粗化
// 细化的锁
for(...){
synchronized(locker){
n++;
}
}
//粗化的锁
synchronized(locker){
for(...){
n++;
}
}
当使用细化的锁时,实际可能没有其他的线程来参与竞争,这是JVM会把锁粗化,避免频繁申请释放锁.
Callable是一个接口,其内部定义了一个带返回值的call方法. Runnable要描述一个带有返回值的方法很麻烦,所以就有了Callable接口,为了使线程执行Callable中的任务,我们还需要用 FutureTask包装一下Callable实例.
public static void main(String[] args) {
// 匿名内部类实现 Callable接口
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0; i < 1000; i++){
sum++;
}
return sum;
}
};
//用FutureTask 包装一下 Callable实例
FutureTask<Integer> task = new FutureTask<Integer>(callable);
Thread t = new Thread(task);
t.start();
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
ReentrantLock使可重入互斥锁,和synchronized的定位类似,
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// working
} finally {
lock.unlock()
}
信号量,用来描述可用资源的个数, 本质上是一个计数器
当申请的资源比资源数多了之后,就进入阻塞状态
public static void main(String[] args) {
//创建Semaphore实例, 参数表示有几个可用资源
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(1000);
System.out.println("我释放资源");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//20 个线程 都在申请资源 但只能保证每刻友四个资源正在被使用
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
CountDownLatch,相当于将一个大任务分成若干个小任务,来判断这些小任务什么时候执行结束
public static void main(String[] args) throws InterruptedException {
// 10个任务需要完成
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runnable() {
@Override
public void run() {
try{
Thread.sleep((long) (Math.random() * 10000));
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
//等待规定的任务数全部执行完
latch.await();
System.out.println(" 比赛结束");
}
HashMap本身时线程不安全的,
故在多线程环境下,我们可以使用 Hashtable 和 ConcurrentHashMap