目录
4.JUC(java.util.concurrent)的常见类
4.4ReentrantLock与synchronized的区别
1.常见的锁策略
以下所说的锁策略并不针对于Java中的锁,任何关于锁的都可以参考以下策略。
1.1乐观锁和悲观锁
乐观和悲观指的是锁具有的特性,并不是锁的具体实现。
悲观锁:
每次拿数据的时候,总是认为锁竞争激烈,那么在拿数据的时候,就会真正上锁,其他线程想获取这个数据时就会阻塞等待锁释放,也就会在CPU上来回调度。
乐观锁:
与悲观锁相反,在每次拿数据时会认为锁竞争不激烈,直接访问,不会真正加锁,在更新数据提交到主内存时,会通过检测版本号数据是否被其他线程修改,如果被修改了,则返回错误信息
1.2重量级锁和轻量级锁
重量级锁:
CPU提供了原子操作指令实现了锁的原子性,操作系统基于原子操作指令实现了mutex互斥锁,JVM又对其封装提供了synchronized关键字,重量级锁就是依赖了CPU的原子操作指令,导致线程争抢锁时在CPU上来回调度阻塞等待锁。
轻量级锁:
加锁的逻辑只在代码中实现,并不会用到操作系统中的原子操作指令,线程也就不会在CPU中来回阻塞等待,而是在CPU中一直调度。
1.3自旋锁和挂起等待锁
自旋锁:
自旋锁是应用程序级别的,在获取锁失败时不会阻塞等待,而是占用CPU资源一直尝试获取锁。在其他线程锁释放时,会第一时间获取到锁,但在此期间会占用CPU资源。
挂起等待锁:
挂起等待锁是操作系统级别的,在获取锁失败时会进行阻塞等待状态,不知过了多久才会被重新调会CPU上再争抢锁。
1.4公平锁和非公平锁
公平和非公平针对的是在争抢锁时是否按照先来后到顺序让线程获取锁
公平锁:
按照线程争抢锁的顺序,在获取锁的线程释放锁后,其他线程按照之前争抢的顺序依次获取锁,这也就会导致要消耗很多资源在存储结构上。
非公平锁:
线程争抢锁时,不遵循先来后到,完全取决于CPU的随机调度,也就不会消耗额外的资源组织线程。
1.5可重入锁和非可重入锁
可重入锁:
指的是在一个线程获取到锁时,在锁内执行逻辑时又加了一次相同的锁,不会产生无法获取这把锁的情况,因为该锁对象中记录了获取到锁的线程和获取次数信息,同一个线程可以在获取同一把锁时继续获取这把锁。
不可重入锁:
获取到一把锁的线程在未释放这把锁时,不能第二次获取这把锁,与可重入恰恰相反。
1.6读写锁
在多线程对同一变量读时并不会引发线程安全问题,但如果有线程进行了修改,则会引发安全问题,为此出现了读写锁。
读锁与读锁之间不会互斥,可以多个线程一起读。
读锁与写锁互斥,只能有一个线程读或者一个线程写。
写锁与写锁互斥,只能有一个线程写。
JAVA标准库中为我们提供了一个类用来描述读写锁。
ReentrantReadWriteLock类,该类中有两个子类分别表示读锁和写锁 ReadLock类表示读锁,有lock(),unlock()方法进行加锁和解锁。 WriteLock类表示写锁,有lock(),unlock()方法进行加锁和解锁。
读写锁适用于频繁读,不频繁写的场景。
2.CAS
2.1什么是CAS
CAS的全称是conpare and swap,比较然后交换,假设内存中的某个变量的值为A,我们手中的预期值是V,我们会把V和A比较,如果相等,则把我们需要赋的值B写入这个变量内存中,如果不相等,则会进行下一次CAS操作。下面写一个伪代码方便理解一下。
address表示内存地址,expectValue表示预期值,newValue表示需要写入的值。
boolean CAS(address,expectValue,newValue){
if(&address == wxpectValue){
&address = newValue;
return true;
}
return false;
}
CAS操作是原子性的,他在硬件层面表示一条指令cmpxchg。
2.2CAS是如何保证线程安全
假设两个线程分别对同一变量进行加1操作,普通环境下可能会由于CPU调度问题造成安全问题。
1.A,B同时加载了内存中的值到工作内存中,并把预期值都加载了为0.
2.假设A先进行CAS操作,A的预期值和内存中的值相等,就会把要写入的值覆盖在内存中,也就是加1操作,线程A完成了CAS操作。
3.此时B来进行CAS操作,将预期值0和内存中的值比较发现不相等,那么就进行下一次CAS操作,先将预期值更新为内存中的值1,然后进行CAS操作,比较过后发现相等,那么再进行加1操作,这样就保证了安全问题。
2.3CAS实现自旋锁
CAS实现自旋锁可以将三个参数修改
自旋锁伪代码
class SpinLock{
private Thread owner = null;
public void lock(){
//判断当前锁是否被线程获取
//没被获取,比较和null相等,那么锁就记录当前线程
//如果被获取了,那么就自旋等待并一直判断
while(!CAS(this.owner,null,Thread.currentThread())){
}
}
public void unlock(){
this.owner = null;
}
}
2.4CAS的ABA问题
有这样一个场景:
小明银行卡里有1000块钱,小明告诉张三和李四说下午5点之前,如果卡里有1000块钱,你们就可以取走500块,假如在3点时张三去查看银行卡发现里面有1000块,取走了500,李四还没去查看,这期间小明的公司往小明银行卡里又充值了500块,李四再去查看银行卡的,预期值1000和银行卡里的钱一样,那么李四又取走了500块,这就导致他们本该一起花500块,却拿走了1000块。
这就是ABA问题,在某一个线程拿着预期值去和一个变量A比较时,虽然可能现在内存中的值是A,但是这个变量可能是被其他线程修改成B之后,又修改回成了A,此时的A并非当时预期的A。
ABA解决办法:
解决办法是为修改的值引入版本号,在每次CAS操作时,不仅要比较预期值,还要比较预期版本号和变量的版本号是否一致,修改时,也要更新变量的版本号,如果发现变量版本号与预期版本号不一致,则进行下一次CAS操作。
3.synchronized锁升级
JVM将synchronized锁状态分为四种,无锁,偏向锁,自旋锁,重量级锁,JVM会根据不同情况依次将锁升级。前三钟状态都属于用户态的操作,只用重量级锁才进入了内核态
synchronized一些优化
锁消除:在一些加了synchronized的代码中,实际上是在单线程的环境下跑的,那么就没必要加锁,所以编译器和JVM识别出来就会进行锁消除。
锁粗化:
假如有一个业务逻辑需要连续执行四个加锁的方法,其中无其他操作,那么频繁加锁解锁一定会有很大开销,既然四个方法需要锁,那么直接在一整段业务逻辑中加一次锁足以,所以JVM和编译器会自动帮我们锁粗化,加一次锁即可。
4.JUC(java.util.concurrent)的常见类
该包中主要包含了一些线程安全的类。
4.1Callable接口
Callable是一个函数式接口,也是用来定义线程任务的,并且这个任务可以有一个返回值,如果任务不成功,也会抛出异常。
如何利用Callable接口来实现线程的任务,一个场景,让一个线程计算1+2+...+100,并将结果返回给主线程。
public class Demo_01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//利用匿名内部类实现接口
Callable <Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
//任务逻辑
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
return sum;
}
};
//利用FutureTask包装一下任务
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//将futureTask作为参数传递给线程,让线程执行futureTask中的callable的call方法。
Thread thread =new Thread(futureTask);
thread.start();
//获取结果,futureTask可以阻塞等待线程完成任务并获取结果
Integer ans = futureTask.get();
System.out.println(ans);
}
}
Callable接口相当于就是带有返回结果的任务,需要搭配FuteruTask类来使用,FutureTask用来保存call方法中的结果,但是call方法是在线程中执行的,何时执行完并不知道,那么就有FutureTask来完成这个等待工作。
FutureTask就相当于你去吃饭时,店员给了你一张小票,你可以通过这张小票随时去看饭好了没。
4.2ReentrantLock
ReentrantLock是一个类,是可重入互斥锁,和synchronized相似,但是ReentrantLock是基于JAVA实现的,都是用来保证线程安全的。
4.2.1ReentrantLock用法
ReentrantLock需要手动加锁解锁。通过调用方法
lock():加锁,获取不到锁会死等
trylock(超时时间):加锁,如果获取不到锁,等待一定时间之后就放弃加锁。
unlock():解锁
例如,两个线程对同一变量自增5000次
public class Demo_02 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock Lock = new ReentrantLock();
Count counter = new Count();
Thread thread01 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
try {
//加锁
Lock.lock();
counter.increase();
} finally {
//解锁
Lock.unlock();
}
}
});
Thread thread02 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
try {
//加锁
Lock.lock();
counter.increase();
} finally {
//解锁
Lock.unlock();
}
}
});
thread01.start();
thread02.start();
thread01.join();
thread02.join();
System.out.println(counter.count);
}
}
class Count{
public int count = 0;
public void increase(){
count++;
}
}
可保证线程安全。
ReentrantLock还可以实现公平锁,在构造方法中即可实现,传入true则为公平锁,反之。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
4.3 ReentrantReadWriteLock
这个类也是java.util.concurrent包中的类,主要实现读写锁,该类中有两个静态内部类,可通过相应方法获取读写锁对象。
public class Demo_03 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//ReentrantReadWriteLock内部有两个静态内部类,通过方法获取这个类中的两个实例对象
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//读锁的加锁解锁
readLock.lock();
readLock.unlock();
//写锁的加锁解锁
writeLock.lock();
writeLock.unlock();
}
}
注意读锁是互斥锁,与读锁和写锁不能同时获取锁,
写锁只可以和写锁共同读,但不可以和写锁共存。
4.4ReentrantLock与synchronized的区别
1.synchronized是关键字,大概率是由C++实现的,属于JVM内部层面,ReentrantLock是java中的类,属于JVM外部层面。
2.synchronized不需要手动解锁,而ReentrantLock需要手动加锁解锁,并且ReentrantLock可以指定获取锁的时间,如果获取不到就放弃。
3.synchronized和ReentrantLock默认都是公平锁,但是ReentrantLock可以在构造时实现公平锁。
4.5信号量(Semaphore)
这个类是用于标记可用资源的个数,也就是一个计数器,在多线程环境下使用是安全的,在多个线程获取资源时,相应的信号量中计数器会-1,如果信号量中计数器为0了,那么其他线程想要继续获取资源时,就会阻塞等待其他线程释放资源,也就是信号量中的计数器加1。
下面用代码模拟一个停车场,初始有5个停车位,也就初始化一个信号量对象,大小为5,用10个线程模拟车,来竞争这个停车场中的停车位。
public class Demo_05 {
public static void main(String[] args) {
//停车场的可用车位初始化为5
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
try {
//尝试获取车位,如果信号量不为0,则获取资源,否则阻塞等待
System.out.println("开始申请资源");
//申请资源
semaphore.acquire();
//处理业务逻辑
System.out.println(Thread.currentThread().getName()+"获取到资源");
TimeUnit.SECONDS.sleep(1);
System.out.println("释放资源");
//资源使用结束,释放
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
}
可见最多同时有5个线程获取到资源,只有其他线程释放资源后,才能继续获取资源。
4.6CountDownLatch
这个类作用是用于主线程中,阻塞等待某些线程执行完他们的任务后,才能继续执行之后的代码,这种功能类似于Thread类中的join方法,但是单个的用过于繁琐,使用CountDownLatch可以更方便。
下面模拟一下跑步比赛,有10个同学赛跑,即10个线程,只用等到这10个同学都到达终点后,才能进行颁奖,把颁奖作为主线程的任务。
public class Demo_06 {
public static void main(String[] args) throws InterruptedException {
//初始化需要等待的线程个数
CountDownLatch countDownLatch = new CountDownLatch(10);
System.out.println("各就各位,准备");
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"开跑");
//模拟比赛时
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"到达终点");
//到达终点后,用countDownLatch标记一个线程已经完成任务,让其计数减一
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
//主线程阻塞等待所有选手到达终点
countDownLatch.await();
System.out.println("颁奖");
}
}
4.7多线程环境下使用集合类
4.7.1使用Arraylist
为了保证线程安全我们常用三种方法
1.自己使用synchronized或ReentrantLock保证线程安全
2.利用集合类工具Collections,将不安全的ArrayList转换为多线程环境下安全的集合类
public class Demo_0202 { public static void main(String[] args) { List<Integer> list = Collections.synchronizedList(new ArrayList<>()); } }
3.使用CopyOnWriteArrayList,,是java.util.concurrent包中的一个多线程安全的集合类,他保证安全的逻辑是,在我们写入集合时,会复制一个副本,在副本上修改,再用这个副本更换这个原来的集合,只需对这一步操作进行加锁操作,而读操作不需加锁,大大提高效率,又保证线程安全,所以他的名字也叫写时复制(CopyOnWriteArrayList)。
4.7.2使用队列
在多线程环境下使用队列需要使用阻塞队列,如
1. ArrayBlockingQueue
2. LinkedBlockingQueue
3. PriorityBlockingQueue
4.7.3使用哈希表
HashMap在多线程环境下是不安全的,他的所有操作都无加锁,为保证多线程环境下安全,我们可以使用HashTable,ConcurrentHashMap
1.HashTable虽然是多线程安全的,但是他是对方法直接加锁,在高并发环境下效率不高
2.ConcurrentHashMap在多线程环境下既安全效率又高,这是因为在哈希表中,数组每一个下标的位置代表一个哈希桶,我们在写操作时,实际上只需要对某一个哈希桶进行操作,而ConcurrentHashMap正是仅仅在对桶操作时加了锁,对于其他哈希桶,其他线程可同时访问,相对于HashTable锁住全部哈希桶,效率得到大大提升。
3.ConcurrentHashMap的扩容思想,首先会生成一个大小为原来两倍的数组,同时搬运一部分数据到新数组中,在之后每个线程来访问哈希表中的数据时,再将访问的某个哈希桶搬运到新数组中,直到最后一个桶被搬运到新数组中,此时销毁原数组。
5.死锁
5.1死锁是什么
死锁通常是指多个线程,例如A线程在等待B线程的资源释放,而B线程占着这个资源等待A线程释放资源,两个人互相一直阻塞等待,导致程序无法继续运行下去,造成死锁问题,下面模拟一下这种情况。
public class Demo_0203 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"尝试获取锁lock1");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+"获取到锁lock1");
//模拟业务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"尝试获取锁lock2");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"获取到锁lock2");
}
}
});
Thread thread2 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"尝试获取锁lock2");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"获取到锁lock2");
//模拟业务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"尝试获取锁lock1");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+"获取到锁lock1");
}
}
});
thread1.start();
thread2.start();
}
}
程序运行至此,无法继续下去,造成死锁问题。
5.2死锁形成原因
1.互斥使⽤:即一个资源被线程占用时,其他线程不可使用此资源
2.不可抢占:资源被线程占用时,除非线程主动释放,不然其他线程不能抢占访问。
3.请求和保持:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4.循环等待:比如A线程等待B释放,B等待C释放,C等待A释放。
当这四个条件形成时,就会造成死锁。
5.3解决死锁
在死锁形成原因下,我们不能解决1和2原因,因为这本来就是锁的逻辑,我们不能改变锁的逻辑,但我们可以在3和4条件下改变。
请求和保持我们可以在代码逻辑上实现,在占用资源时不请求其他资源,如果业务需求实在需要,那我们对循环等待这个点进行突破。
当多个线程需要同时获取多把锁时,我们可以对这些锁进行编号,保证他们获取锁的顺序一致,比如从小到大获取,就不会出现循环等待。例如多个线程都遵守这个准则,先获取锁1,才能获取锁2,再能获取锁3,这样就不会出现一个循环等待的序列。
多线程进阶结束啦,本文仅供自己复习参考。