Java核心---线程进阶
Posted 一位懒得写博客的小学生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心---线程进阶相关的知识,希望对你有一定的参考价值。
目录
乐观锁 VS 悲观锁
- 乐观锁:(CAS(比较并且交换)、ABA、JUC) 乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发生冲突了,则让返回用户错误的信息,让用户决定如何去做。
- 悲观锁:(synchronized) 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- 悲观锁的问题: 总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高;
- 乐观锁的问题: 并不总是能处理所有问题,所以会引入一定的系统复杂度。
什么是CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作
- 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
-
- 比较 A 与 V 是否相等。(比较)
-
- 如果比较相等,将 B 写入 V。(交换)
-
- 返回操作是否成功
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。
CAS 是怎么实现的
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲
- java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性
实现
private static AtomicInteger count = new AtomicInteger(0);
private static final int MAXSIZE = 100000;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
count.getAndIncrement();
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
count.getAndDecrement();
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println(count);
CAS的缺点:
ABA问题 -> AtomicInteger 是存在ABA问题
ABA 统一解决方案:增加版本号,每次修改之后更新版本号。AtomicStampedReference()
ABA问题
private static AtomicReference money = new AtomicReference(100);
public static void main(String[] args) throws InterruptedException {
Thread m1 = new Thread(new Runnable() {
@Override
public void run() {
//转出100元
boolean res = money.compareAndSet(100,0);
System.out.println("第一次" + res);
}
});
m1.start();
m1.join();
Thread m3 = new Thread(new Runnable() {
@Override
public void run() {
//转入100元
boolean res = money.compareAndSet(0,100);
System.out.println("第三次" + res);
}
});
m3.start();
m3.join();
Thread m2 = new Thread(new Runnable() {
@Override
public void run() {
//转出100元
boolean res = money.compareAndSet(100,0);
System.out.println("第二次" + res);
}
});
m2.start();
}
//执行结果
第一次true
第三次true
第二次true
ABA问题解决
//解决ABA问题
private static AtomicStampedReference money = new AtomicStampedReference(100,0);
public static void main(String[] args) throws InterruptedException {
Thread m1 = new Thread(new Runnable() {
@Override
public void run() {
//转出100元
boolean res = money.compareAndSet(100,0,0,1);
System.out.println("第一次" + res);
}
});
m1.start();
m1.join();
Thread m3 = new Thread(new Runnable() {
@Override
public void run() {
//转入100元
boolean res = money.compareAndSet(0,100,1,2);
System.out.println("第三次" + res);
}
});
m3.start();
m3.join();
Thread m2 = new Thread(new Runnable() {
@Override
public void run() {
//转出100元
boolean res = money.compareAndSet(100,0,0,1);
System.out.println("第二次" + res);
}
});
m2.start();
}
//执行结果
第一次true
第三次true
第二次false
共享锁/非共享锁
共享锁:一把锁可以被多个线程拥有;读写锁中的读锁就是共享锁。
非共享锁:一把锁只能被一个线程拥有。(synchronized)
读写锁
就是将一把锁分为2个,一个用于读数据的锁,另一把锁叫做写锁。读锁是可以被多个线程同时拥有的,而写锁只能被一个线程拥有。
读写锁的具体实现:ReentrantReadWriteLock
读写锁的优势: 锁的粒度更加地小,性能也更高。
注意事项: 读写锁中的读锁和写锁是互斥的。(防止同时读写所产生的脏数据)。
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
//读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
//线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " " + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " " + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName() + " " + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName() + " " + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
}
//执行结果
pool-1-thread-2 Fri May 28 16:10:41 CST 2021
pool-1-thread-1 Fri May 28 16:10:41 CST 2021
pool-1-thread-3 Fri May 28 16:10:44 CST 2021
pool-1-thread-4 Fri May 28 16:10:47 CST 2021
公平锁/非公平锁
公平锁(new ReentrantLock(true)): 锁的获取顺序必须和线程的先后顺序保持一致。
优点:性能比较高
非公平锁(new ReentrantLock()/new ReentrantLock(false)/synchronized):锁的获取顺序和线程获取顺序的前后顺序无关(默认锁策略);
优点:执行时有序的,结果也是可以预期的。
自旋锁
按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了。
//只要没抢到锁,就死等
while (抢锁(lock) == 失败) {}
缺点:如果发生死锁则会一直自旋(循环),所以会带来一定的额外开销。
可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
private static Object lock = new Object();
public static void main(String[] args) {
//第一次进入锁
synchronized (lock) {
System.out.println("第一次进入锁");
synchronized (lock) {
System.out.println("第二次进入锁");
}
}
}
//执行结果
第一次进入锁
第二次进入锁
怎么理解乐观锁和悲观锁的
- 乐观锁 -> CAS -> Atomic*,CAS 是由V(内存值)A(预期旧值)B(新值)组成,然后执行的时候是使用 V == A对比,如果结果为 true 则表明没有发生冲突,则可以直接修改,否则不能修改。CAS 是通过调用C++ 实现提供的 Unsafe 中的本地方法(CompareAndSwapXXX)来实现的,C++是通过操作系统中 Atomic::cmpxchg(原子指令)来实现的。
- 悲观锁 -> synchronized 在java 中将锁的 ID 存放在对象头来实现的,synchronized 在JVM 层面是通过监视器锁来实现的,synchronized 在操作系统层面是通过互斥锁 mutex 实现。
synchronized (独占锁)锁优化(锁消除)
JDK 1.6 锁升级的过程
- 无锁
- 偏向锁(第一个线程第一次访问)将线程ID存储在对象头中的偏向标识。
- 轻量级锁(自旋)
- 重量级锁
java.util.concurrent 包下的常见类
- Reentrantlock
lock 写在 try 之前
一定要记得在 final 里面进行 unlock
信号量
public static void main(String[] args) {
//信号量
Semaphore semaphore = new Semaphore(2);
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
for (int i = 0; i < 4; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "到达停车场");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//试图获取锁
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取到锁了
System.out.println(Thread.currentThread().getName() + "进入停车场");
int num = 1 + new Random().nextInt(5);
try {
Thread.sleep(num * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "离开停车场");
//释放锁
semaphore.release();
}
});
};
}
//执行结果
pool-1-thread-2到达停车场
pool-1-thread-4到达停车场
pool-1-thread-3到达停车场
pool-1-thread-1到达停车场
pool-1-thread-4进入停车场
pool-1-thread-1进入停车场
pool-1-thread-4离开停车场
pool-1-thread-3进入停车场
pool-1-thread-1离开停车场
pool-1-thread-2进入停车场
pool-1-thread-2离开停车场
pool-1-thread-3离开停车场
计数器 CountDownLauth
CountDownLauth 是如何实现的?
在 CountDownLauth 里面有一个计数器,每次调用 countdown 方法时计数器的数量-1;直到减到0的时候就可以执行 await()后边的代码。
public static void main(String[] args) throws InterruptedException {
//计数器
CountDownLatch latch = new CountDownLatch(5);
for (int i = 1; i < 6; i++) {
final int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +
" 开始起跑");
try {
Thread.sleep(finalI * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" 到达终点");
以上是关于Java核心---线程进阶的主要内容,如果未能解决你的问题,请参考以下文章