多线程进阶
一。锁策略
1.乐观锁与悲观锁:
这是锁的一种特性, “一类锁”不是具体的锁
乐观锁和悲观锁是对后续锁冲突是否频繁给出的预测
乐观锁:后续锁冲突的概率不大,可以少做一些工作
悲观锁:后续冲突的概率比较大,可能要多做一些工作
2.重量级锁与轻量级锁:
轻量级锁的开销比较小
重量级锁的开销比较大
当然也可以与上面的乐观悲观锁相联系,乐观锁其实就是一种轻量级锁;悲观锁就是一种重量级锁
这两个的区别就是,一个是预测锁冲突概率,一个是预测实际消耗的开销
3.挂起等待锁和自旋锁
自旋锁就属于一种轻量级锁的典型实现,往往是在纯用户态实现的,比如使用一个while循环,不停的检查当前锁是否被释放,如果没有被释放就继续循环,释放了就获得到锁,从而结束循环
挂起等待锁就是一种重量级锁,它的开销是很大的,要借助系统api来实现,一旦有锁竞争,就会在内核中触发一系列的动作(比如说让这个线程进入阻塞状态(阻塞的开销是很大的),暂时不参与系统CPU的调度)
4.读写锁:
读加锁:读的时候能读,但是不能写
写加锁:写的时候不能读,也不能写
5.非公平锁和公平锁:
背景:假设有多个线程,当第一次这些线程进行锁竞争的时候,其中一个线程拿到了锁的使用权,但是当它释放锁的时候,这时哪个线程会得到锁呢?
这就是会涉及到公平锁和非公平锁
公平锁:按照先来后到的顺序进行获取锁
非公平锁:按照随机的顺序进行获取锁
二。CSA
CAS的全称:compare and swap, 进行比较和交换的是内存和寄存器
假设现在有一个内存M,和两个寄存器A,B
CAS(M,A,B)
这时如果M和A的值相同,那么M和B的值进行交换,同时整个操作返回true
如果M和A的值不同,那么无事发生,返回false
交换的本质是为了把B赋值给M,寄存器B中的值是什么,其实没有必要关心
CAS其实是一个CPU指令,也就是说一个CPU指令,就可以完成上述交换的逻辑。单个CPU指令是原子的,就可以用CAS完成一系列操作,进一步替代加锁
优点:保证线程安全,避免阻塞
缺点:代码不好理解且只适用与一些特定的场景,不如synchronized灵活
CAS是可以实现原子类的,比如以int++为例,int++不是原子的
AtomicInteger是基于CAS的方式对int进行了封装,此时进行++就是原子的了,因此可以说明CAS是解决线程安全的另一种写法
import java.util.concurrent.atomic.AtomicInteger;
public class Demo1 {
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.getAndIncrement();//就是count++的意思
//count.getAndDecrement();//就是count--的意思
}
});
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.get());
}
}
原来的写法是public static int a=0;
现在的写法是 public static AtomicInteger a=new AtomicInteger( ); 这个括号里的值就是对a这个变量初始化对值
这个方法多在多线程计数时使用
当然CAS也是存在问题的:ABA问题
CAS进行操作的关键是“值”没有发生变化来作为“没有其他线程穿插执行的依据”
但是这种判断是存在问题的,有可能是这种情况:A->B->A,此时看起来没有任何问题,实际上其他线程已经进行了穿插,只不过最后又改回了原来的值了,ABA就算出现了,一般不会出现bug,就相当于买手机买到了一个二手翻新机
ABA问题出现问题的情况(非常巧合极端才会出现问题):
例子:假设一个人去银行取钱,这个人银行卡里有1000元,当他摁了取500一次,这时机器没有响应,然后他又按了一次,且正巧这时有人给他转了500元
通过上图可以看出,最后得到的结果是500元,这就是典型的ABA问题,在t2线程进行完把value改成了B(500元),再经过线程t3把value改回了A(1000元),这时再经历t1线程value又会被减500
因此可以发现:有增有减就回出现ABA问题,只增或只减就不会出现ABA问题
解决方法:引入一个额外的变量(版本号),约定每次修改余额的时候,都要让版本号自增,此时再使用CAS就不会判定余额了,而是判定版本号了,看版本号是否发生了变化
三。synchronized原理:
重要机制;锁升级;锁消除;锁粗化
锁升级:无锁->偏向锁->自旋锁->重量级锁
偏向锁:不是真正的加锁,只是做了一个标记,偏向锁的核心思想就是“懒汉模式”;能不加锁就不加锁,加锁就意味着开销。原理就是:如果没人来竞争,就不加锁;如果有人来加锁,就最先或得到锁
锁消除:也是一种编译器优化的手段,编译器会自动针对你写的加锁进行判断,如果编译器认为这个地方不需要加锁,则编译器会自动在这里把锁优化掉。
例如:在javaSE阶段学字符串的时候有StringBuilder和StringBuffer,他们之间的区别之一就是StringBuilder不带有synchronized,而StringBuffer带有synchronized,如果这时在单线程用StringBuffer,那么编译器会自动优化掉锁
编译器只会在自己最有把握的时候,才会进行锁消除的操作(触发的概率不会很高)
偏向锁与锁消除的区别:
偏向锁是运行时的事情,运行时多线程的调度情况不同,这个锁可能有人竞争,也有可能没有;锁消除是编译过程中发生的
锁粗化:
锁的粒度:synchronized里,代码越多,锁的粒度越粗,代码越少,锁的粒度越细;
粒度细的时候能够并发执行的逻辑越多,更有利于充分利用多核CPU资源
但是如果颗粒度细的锁反复多次进行加锁解锁的操作,实际效果可能会不如粒度粗的锁
四。collable接口:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//定义任务
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for(int i=0;i<1000;i++){
sum+=i;
}
return sum;
}
};
//把任务放到进程中进行执行
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t=new Thread(futureTask);
t.start();
//此处get就能获取到callable里的返回结果
//由于是并发编程,执行到get的时候,t线程可能还没有执行完,没执行完的化,get就会阻塞
System.out.println(futureTask.get());
}
}
这里的futureTask就相当于买奶茶的的时候,给你开的一张支票,等一会取的时候需要提供发票才能取奶茶
五。信号量(semaphone)
信号量就是一个计数器,描述了“可用资源”的个数。
每次申请一个资源的时候,计数器-1;称为“P操作”(英语中这里指的是acquire)
每次释放一个资源的时候,计数器+1;称为“V操作”(英语中这里指的是release)
这里的+1和-1都是原子的
信号量假设初始情况数值是10,每次进行P操作,数值就-1.当已经进行了10次操作之后,数值就变成了0;如果继续进行P操作,这时就会进入阻塞等待的状态(这里的阻塞等待有一种锁的感觉)
其实锁本质上是一种特殊的信号量,锁就是可用资源为1的信号量,加锁操作P操作,1->0;解锁操作V操作,0->1;因此也可称锁为二院信号量
开发中,如果遇到需要申请资源的情况,就可以使用信号量进行实现
import java.util.*;
import java.util.concurrent.Semaphore;
public class Main {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(4);
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.release();
System.out.println("V操作");
semaphore.acquire();
System.out.println("P操作");
}
}
此代码申请了4个资源,可以看到前4次都在申请资源,当在第5次都时候释放了一个资源,此时再进行申请资源是可以正常申请的,但是如果再在后面申请一次资源,这时会进入阻塞等待的状态
六。CountDownLatch
这个东西主要适用于多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成,比如要把一个大任务,分成多个小任务,让这些任务并发的执行
就可以使用countDownLatch来判定说这些任务是否都完成了
CountDownLatch主要有两个方法:
1.await调用的时候就会进入阻塞,就会等待其他线程完成任务,索引线程都完成任务之后,此时这个await才会返回,才会继续往后走
2.countDown,告诉CountDownLatch,当前这个子任务完成了
import java.util.*;
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(10);
for(int i=0;i<10;i++){
int id=i;
Thread t=new Thread(()->{
System.out.println("thread"+id);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//通知当前任务完成了
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
System.out.println("所有任务都完成了");
}
}