第一节、JUC课程介绍
首先新建一个maven项目。
然后有一些避坑操作:
这里一定是8
这些都是为了使用jdk8的新特性。
第二节、线程和进程
什么是JUC?
都在java.util包下。
有些业务我们可能无法通过普通的线程代码来实,例如new Thread,new Runnable。这样写效率并不高。
Thread只是一个普通的线程类,Runnable接口其实就是丢一个任务给Thread去执行,Runnable没有返回值,并且其效率相比callable较低,功能也没有callable强大。
回顾:
只能通过native方法去调底层的C++方法,Java是无法直接操作硬件的。
第三节、回顾多线程
线程的状态:
Thread.State //Thread的一个枚举类,可以查看线程的状态。
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待(死死的等)
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
wait和sleep的区别:
- 来自不同的类
wait来自于Object类。sleep来自于Thread类。 - 关于锁的释放
wait会释放锁,sleep不会释放锁。 - 使用范围不同
wait必须在同步代码块中,sleep可以在任何地方睡。
TimeUnit.DAYS.sleep(1); // 企业中,一般不用sleep,多用TimeUnit
第四节、sychronized锁
Lock锁(重点)
传统的Synchronized:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
// 并发:多线程操作同一个资源类,把资源丢入线程
Ticket ticket = new Ticket();
// Runnable @FunctionalInterface 函数式接口
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 40; i++) {
ticket.sale();
}
}
}, "A").start();
// jdk1.8 多使用lambda表达式 (参数)-> {代码}
new Thread(() -> {
for (int i = 1; i < 40; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 40; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket {
private int number = 50;
// synchronized 本质: 队列、锁,也就是让所有的线程排成队了买票
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "票,剩余" + number);
}
}
}
第五节、Lock锁
java.util.concurrent.locks包下
有三个接口:Condition、Lock、ReadWriteLock
Lock接口:
两个方法:
三个实现类:
构造方法:无参构造使用的是非公平锁,有参构造中传入true,表示公平锁。
public class SaleTicketDemo2 {
public static void main(String[] args) throws InterruptedException {
Ticket2 ticket = new Ticket2();
new Thread(() -> {
for (int i = 1; i < 40; i++) ticket.sale();
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 40; i++) ticket.sale();
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 40; i++) ticket.sale();
}, "C").start();
}
}
// Lock三部曲
// 1、new ReentrantLock()
// 2、lock.lock; // 加锁
// 3、finally {lock.unlock();} // 解锁
class Ticket2 {
private int number = 50;
Lock lock = new ReentrantLock();
public synchronized void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "票,剩余" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
第六节、Sychronized和Lock的区别
- Synchronized 内置的Java关键字,Lock是一个Java类。
- Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁。
- Synchronized 会自动释放锁(A线程正常执行完会释放锁,A线程抛出异常也会释放锁。),Lock需在finally中手动释放锁(unlock()方法释放锁),否则容易造成线程死锁。
- Synchronized 线程1(获得锁,阻塞)、线程2(等待,死死的等),Lock锁不一定会等待下去,等不到就结束了(lock.tryLock(); // 尝试获取锁,如果尝试获取不到锁,线程可以不用再等待就直接结束)。
- Synchronized 可重入锁,不可以中断的,非公平,Lock,可重入锁,可以判断锁,公平锁还是非公平锁可以自己设置。
- Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
第七节、传统的生产者消费者问题、防止虚假唤醒
锁是什么?如何判断锁的是谁?
生产者和消费者问题:
- Synchronized版本
public class producer {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
// 等待、业务、通知
class Data {
private int number = 0;
// +1
public synchronized void increment() throws InterruptedException {
if (number != 0) {
// 等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "-->" + number);
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
this.notifyAll();
}
}
这样是会有问题的,如果存在A、B、C、D四个线程,就会有2、3出现。
因为我们以上代码用的是if判断,就会产生虚假唤醒问题,那么该如何解决呢?用while
while (number == 0) {
this.wait();
}
面试必须问:单例模式、排序算法、生产者消费者、死锁
第八节、Lock版的生产者消费者问题
之前学synchronized,它有配套的wait()和notify()、notifyAll()
现在我们用Lock,用Condition的await()和signal()、signalAll()取代wait()和notify()、notifyAll()
JUC版的生产者消费者问题:
public class JUCbanben {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
// 等待、业务、通知
class Data2 {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// +1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
// 等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "-->" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// -1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
任何一个新技术,不仅仅是覆盖了原来的技术,还是对原来技术的优势的强化和补充。
以上代码和synchronized版本毫无区别,执行结果也是,A、B、C、D四个线程谁执行,都只是随机操作。
如何让四个线程有序执行呢?这就用到了condition,它可以精准的通知和唤醒线程!
第九节、condition实现精准的唤醒和通知
我现在想要A执行完执行B,B执行完执行C,C执行完执行A
public class JUCjingzhunhuanxing {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data3.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data3.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data3.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
class Data3{
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private int number = 1;
public void printA() throws InterruptedException {
lock.lock();
try {
while(number != 1) {
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"正在执行...");
number = 2;
// 唤醒指定的线程
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() throws InterruptedException {
lock.lock();
try {
while(number != 2) {
// 等待
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"正在执行...");
number = 3;
// 唤醒指定的线程
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() throws InterruptedException {
lock.lock();
try {
while(number != 3) {
// 等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"正在执行...");
number = 1;
// 唤醒指定的线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
第十节课、八种锁现象彻底理解锁
test1:
public class test1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendMes, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(phone::call, "B").start();
}
}
class Phone {
// synchronized锁的对象是方法的调用者,也就是phone
// 两个方法用的是同一个锁,谁先拿到谁先执行
public synchronized void sendMes() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("send message");
}
public synchronized void call() {
System.out.println("call");
}
}
这段代码的执行结果一定是先send message然后在call。
test2:
增加了一个普通方法,先执行send message还是先执行hello?
先执行hello,因为hello方法没有加synchronized关键字,不是同步方法,就不受锁的影响。
public class test2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
new Thread(phone::sendMes, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(phone::hello, "B").start();
}
}
class Phone2 {
// synchronized锁的对象是方法的调用者,也就是phone
// 两个方法用的是同一个锁,谁先拿到谁先执行
public synchronized void sendMes() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("send message");
}
public synchronized void call() {
System.out.println("call");
}
public void hello() {
System.out.println("hello");
}
}
test3:
这次一定是先打印call再打印send message,显然call的sleep时长小于sendMes
public class test3 {
public static void main(String[] args) throws InterruptedException {
// 两个对象,两个调用者,两把锁!
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(phone1::sendMes, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(phone2::call, "B").start();
}
}
test4:
结果是先send message再call
public class test4 {
public static void main(String[] args) throws InterruptedException {
Phone4 phone = new Phone4();
new Thread(() -> {
phone.sendMes();
}, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone4 {
// synchronized锁的对象是方法的调用者,也就是phone
// static 静态方法
// 类一加载就有了,锁的是Class类模版,Class类模版全局唯一
public static synchronized void sendMes() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("send message");
}
public static synchronized void call() {
System.out.println("call");
}
}
test5:
这时一定是先call再send message,因为call用的是phone的锁,sendMes用的是Phone5的Class类模版的锁,两者互不相干,当然是谁的sleep时间短谁先执行了。
public class test5 {
public static void main(String[] args) throws InterruptedException {
Phone5 phone = new Phone5();
new Thread(Phone5::sendMes, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(phone::call, "B").start();
}
}
class Phone5 {
// 静态同步方法,锁的是Class类模版
public static synchronized void sendMes() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("send message");
}
// 普通同步方法,锁的是调用者
public synchronized void call() {
System.out.println("call");
}
}
总结:
静态同步方法,锁的是Class类模版
普通同步方法,锁的是调用者
第十一节、CopyOnWriteArrayList
集合类是不安全的,我们能在ArrayList中进行操作,是因为是在单线程环境下。
List不安全
问异常,要说OOM、stackOverFlow、ConcurrentModifyException
package com.kuang.unsafe;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author liushihao <liushihao@kuaishou.com>
* Created on 2021/1/19 9:33 上午
*/
// Exception in thread "7" java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
public static void main(String[] args) {
// 并发下ArrayList不安全,Synchronized可以解决
// 1、其实换成Vector就可以解决,Vector的add方法就是加了Synchronized关键字
// List<String> list = new Vector<>();
// 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略
// 多个线程调用的时候,对于一个list,读取的时候是固定的,写入的时候先复制一个数组出来,写入完之后在插入就可以了
// vector的add方法是加synchronized的,只要有synchronized的方法,他的效率就会比较低
// 而CopyOnWriteArrayList的add方法用的Lock
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList中的add方法:
Vector的add方法:
总结:
CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,其原理大概可以通俗的理解为:初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
第十二节、CopyOnWriteArraySet
先看一下Set和List的关系:
在Collection类中打开,右击:
同时我们会发现BlockingQueue和List、Set是同级关系。
// 同样,使用HashSet会报:ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
// 1.Set<String> set = Collections.synchronizedSet(new HashSet<>());
// 2.CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
Set的底层是什么?
底层就是new了一个HashMap,而HashSet就取出了HashMap中的键。
第十三节、CopyOnWriteArrayMap
public class MapTest {
public static void main(String[] args) {
// Map<String, String> map = new HashMap<>();
// 上面代码相当于:Map<String, String> map1 = new HashMap<>(16, 0.75F);
// 1. Map<String, String> map1 = Collections.synchronizedMap(new HashMap<>());
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
第十四节、走进Callable
也就是说,Callable接口相比Runnable接口,有以下不同:
1. Callable可以有返回值。
2. Callable可以抛出异常。
3. Callable的实现方法是call()方法,而Runnable的实现方法是run()方法。
FutureTask类的构造方法:
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 怎么启动Callable?
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<V>).start();
// new Thread(new FutureTask<V>(Callable)).start();
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread); // 适配类
new Thread(futureTask, "A").start();
Integer integer = futureTask.get(); // 获取Callable的返回值
System.out.println(integer);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 1024;
}
}
细节:
- 有缓存
代码如果这样写:
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread); // 适配类
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
Integer integer = futureTask.get(); // 获取Callable的返回值
System.out.println(integer);
}
打印栏只会输出
A
1024
或者
B
1024
因为A线程和B线程并发执行,如果A先执行完,B再start的时候,B的call方法走的就是缓存,并不会真正的start。
如果代码改成这样,结果就只是:
A
1024
new Thread(futureTask, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(futureTask, "B").start();
- 结果可能需要等待,因为线程可能会阻塞。
比如MyThread类这样写的话:
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName());
return 1024;
}
}
第15节、CountDownLatch(减法计数器)
如果不用CountDownLatch:
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器,总数是6(必须要执行任务的时候再使用)
// CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "GO OUT");
// countDownLatch.countDown(); // 数量-1
}).start();
}
// 等待计数器归零再向下执行
// countDownLatch.await();
System.out.println("close door");
}
}
究竟什么时候会close door并不清楚,这样就会把还未执行完的线程“关在门里”
用CountDownLatch:
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器,总数是6(必须要执行任务的时候再使用)
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "GO OUT");
countDownLatch.countDown(); // 数量-1
}).start();
}
// 等待计数器归零再向下执行
countDownLatch.await();
System.out.println("close door");
}
}
结果一定是这样的:
第十六节、CyclicBarrier(加法计数器)
CyclicBarrier有两种构造方法:
其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
第二种构造方法表示计数器到达parties这个数量的时候就会执行barrierAction线程:
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 集齐七颗龙珠召唤神龙
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
// Thread线程是取不到i的,因为new Thread是新建一个类
// 而final就可以拿到了
new Thread(() -> {
System.out.println("收集到第"+temp+"颗龙珠.");
try {
cyclicBarrier.await(); // 等待计数器的值为7的时候就执行CyclicBarrier中的线程。
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("第"+temp+"颗龙珠收集完成.");
}).start();
}
}
}
第十七节、Semaphore
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位!限流!总共有3个车位却有7个车来停。
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
// acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // release() 释放
}
}, String.valueOf(i)).start();
}
}
}
第十八节、ReadWriteLock
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 写入
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> myCache.put(temp+"", temp), String.valueOf(i)).start();
}
// 读取
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> myCache.get(temp+""), String.valueOf(i)).start();
}
}
}
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// 存, 写
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完毕.");
}
// 取,读
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读入" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读入完毕.");
}
}
1写的时候3也在写,显然这样是有问题的。
应当用读写锁:
/**
* ReentrantReadWriteLock 更加细粒度的控制
* 读-读 可以共存
* 读-写 不可共存
* 写-写 不可共存
*
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
*/
class MyCache2 {
private volatile Map<String, Object> map = new HashMap<>();
// 读写锁 更加细粒度的控制
// 我们平时用的是ReentrantLock,这种锁显然没有细粒度的控制
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 存, 写
public void put(String key, Object value) {
reentrantReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完毕.");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
// 取,读
public void get(String key) {
reentrantReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读入" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读入完毕.");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
}
结果是写的时候有加锁(写的时候是原子性的操作),读的时候不加锁。
第十九节、阻塞队列BlockingQueue
写入:如果队列满了,必须阻塞等待
读取:如果队列空了,必须阻塞等待生产
BlockingQueue可以类比Set和List,它是Collection的子类,也有ArrayBlockingQueue、LinkedBlockingQueue子类。
什么时候使用阻塞队列?
多线程并发处理、线程池。
第二十节、BlockingQueue的四组API
第二十一节、SynchronousQueue 同步队列
/**
*
* SynchronousQueue 同步队列
* 和其他的BlockingQueue不同,SynchronousQueue不存储元素
* put了一个元素,必须先从里面take出来,否则不能put进去值
*
* 可以把他看成容量为1的BlockingQueue
*/
public class Test {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName() + " put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName() + " put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "取出了" + blockingQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "取出了" + blockingQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "取出了" + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
}
第二十二节、池化技术及线程池的使用
线程池:3大方法、7大参数、4种拒绝策略
3大方法:
ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 只有一个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个有固定线程数的线程池
ExecutorService threadPool = Executors.newCachedThreadPool(); // 线程数目可调节的线程池,遇强则强,遇弱则弱
第二十三节、7大参数和4大拒绝策略
3大方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
会发现Executors的底层调用的都是ThreadPoolExecutor,那我们为什么不直接用ThreadPoolExecutor呢?
7大参数
再看ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大线程池大小
long keepAliveTime, // 超时了没人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler // 拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
讲一个例子来深入的理解一下ThreadPoolExecutor的7大参数:
银行办理业务,银行有5个窗口(maximumPoolSize),但是一般只开2个(corePoolSize)(也就是说maximumPoolSize 一定要 > corePoolSize),有4个候客区(workQueue容量)。
有2个人办理业务时,2个窗口足够用,这时来了4个人,那么这4个人都在候客区等待办理。
这时又来了3个人,就把剩下的3个窗口打开,2个窗口已经忙不过来了。
这时又来了1个人,就会用到拒绝策略(handler)。
假设最后都办理完了,等待1小时(keepAliveTime)还没人来,就会再次关闭那3个窗口。
最大承载:maximumPoolSize+workQueue容量
package com.kuang.pool;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author liushihao <liushihao@kuaishou.com>
* Created on 2021/1/29 9:52 上午
*/
public class demo01 {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 只有一个线程的线程池
// ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个有固定线程数的线程池
// ExecutorService threadPool = Executors.newCachedThreadPool(); // 线程数目可调节的线程池,遇强则强,遇弱则弱
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
// 自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());// 拒绝策略:队列满了,尝试去和最早的任务竞争(因为最早执行的任务可能最早结束),如果竞争成功就执行,失败就把任务丢掉。
try {
for (int i = 1; i <= 9; i++) {
int finalI = i;
threadPool.execute(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " --- ok --- " + finalI);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
4种拒绝策略:
这四种拒绝策略对应之前讲过的BlockingQueue的四种API
new ThreadPoolExecutor.AbortPolicy(); // 队列满了,还有任务来,不处理这个任务并抛出异常
new ThreadPoolExecutor.CallerRunsPolicy(); // 队列满了,还有任务来,哪来的回哪去,任务从main线程来的,就让main线程去执行这个任务
new ThreadPoolExecutor.DiscardPolicy(); // 队列满了,还有任务来,丢掉任务,不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy());// 拒绝策略:队列满了,尝试去和最早的任务竞争(因为最早执行的任务可能最早结束),如果竞争成功就执行,失败就把任务丢掉。
第二十四节、CPU密集型和IO密集型
maximumPoolSize该如何定义?
1、CPU密集型。几核,就定义为几,可以保持CPU的效率最高!
如何查看CPU核数:Runtime.getRuntime().availableProcessors();
2、IO密集型。判断你的程序中十分耗IO的线程有几个。就定义maximumPoolSize大于这个数量即可。最好是这个数量的2倍。
函数式接口:只有一个方法的接口
4大函数式接口:以下4个没有组合单词的类:
第二十五节、函数型接口和断定型接口
- Function 函数式接口,有一个输入参数,有一个输出
/**
*
* Function 函数式接口,有一个输入参数,有一个输出
*/
public class demo1 {
public static void main(String[] args) {
// Function<String, String> function = new Function<String, String>() {
// @Override
// public String apply(String str) {
// return "hello " + str + "!";
// }
// };
Function function = str -> "hello " + str + "!";
System.out.println(function.apply("world"));
}
}
- Predicate断定型接口,返回值只能是boolean值
/**
*
* Predicate断定型接口,返回值只能是boolean值
*/
public class demo2 {
public static void main(String[] args) {
// 判断字符串是否为空
// Predicate<String> predicate = new Predicate<>() {
// @Override
// public boolean test(String str) {
// return str.isEmpty();
// }
// };
// Predicate<String> predicate = str -> str.isEmpty();
Predicate<String> predicate = String::isEmpty;
System.out.println(predicate.test(""));
}
}
第二十六节、消费型接口和供给型接口
- Consumer 消费型接口:只有输入,没有返回值
public class demo3 {
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<>() {
// @Override
// public void accept(String str) {
// System.out.println(str + " niupi");
// }
// };
Consumer<String> consumer = str -> System.out.println(str + " niupi");
consumer.accept("shihao");
}
}
- Supplier 供给型接口 没有参数,只有返回值
/**
*
* Supplier 供给型接口 没有参数,只有返回值
*/
public class demo4 {
public static void main(String[] args) {
// Supplier<Integer> supplier = new Supplier<>() {
// @Override
// public Integer get() {
// System.out.println("get()");
// return 1024;
// }
// };
Supplier<Integer> supplier = () -> 1024;
System.out.println(supplier.get());
}
}
第二十七节、Stream流式计算
package com.kuang.stream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
*
* 筛选:1、ID是偶数
* 2、年龄大于23
* 3、用户名转化为大写
* 4、用户名倒序
* 5、只输出一个用户
*/
public class test {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(5, "e", 25);
// 集合就是存储
List<User> users = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给stream流
// lambda表达式、链式编程、函数式接口、Stream流式计算
users.stream().filter(u -> u.getId() % 2 == 0)
.filter(u -> u.getAge() > 23)
.map(u -> u.getName().toUpperCase())
.sorted(Comparator.reverseOrder())
.limit(1)
.forEach(System.out::println);
}
}
看一下sorted方法源码:
调用的是函数式接口Comparator
而Comparator所执行的方法:
.sorted((user1, user2) -> user2.compareTo(user1)) // 可以写成
.sorted(Comparator.reverseOrder())
.sorted((user1, user2) -> user1.compareTo(user2)) // 可以写成
.sorted(String::compareTo)
第二十八节、ForkJoin分支合并
ForkJoin特点:工作窃取
假设A、B线程执行任务,B线程已经执行完,就会把A线程的任务拿过来执行。这样可以提高工作效率,避免线程等待。
这里面维护的是双端队列。
package com.kuang.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* @author liushihao <liushihao@kuaishou.com>
* Created on 2021/1/29 5:02 下午
*
* 求和计算的任务
* 1、for循环
* 2、ForkJoin
* 3、Stream并行流
*
*
* 如何使用forkjoin?
* 1、通过ForkJoinPool来执行
* 2、计算任务forkJoinPool.execute(ForkJoinTask task)
* 3、计算类要继承ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
// 临界值
public Long temp = 10_00L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
// 计算方法
@Override
protected Long compute() {
if (end - start < temp) {
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 分支合并计算
Long mid = (start + end) / 2; // 中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, mid);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(mid + 1, end);
task2.fork();
return task1.join() + task2.join();
}
}
}
测试类:
package com.kuang.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
/**
* @author liushihao <liushihao@kuaishou.com>
* Created on 2021/1/30 11:32 上午
*/
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//test1(); // 耗时:5672
//test2(); // 耗时:5904
test3(); // 耗时:232
}
// for循环
public static void test1() {
long start = System.currentTimeMillis();
Long sum = 0L;
for (long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + "时间:" + (end - start));
}
// 使用ForkJoin,ForkJoinDemo中的临界值temp可以改变,所以ForkJoin的效率可以更高
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo); //提交任务,有返回值
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + "时间:" + (end - start));
}
// 使用stream并行流
public static void test3() {
long start = System.currentTimeMillis();
// range()表示开区间,rangeClosed表示左开右闭
long sum = LongStream.rangeClosed(0, 10_0000_0000).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + "时间:" + (end - start));
}
}
第二十九节、异步回调
Future设计的初衷:对将来的某个事件的结果进行建模。
参看以下博客:
https://blog.csdn.net/u014209205/article/details/80410039
https://blog.csdn.net/luzhensmart/article/details/82857996
第三十节、理解JMM
Volatile关键字的理解:
1、保证可见性
2、不保证原子性
3、禁止指令重排序
什么是JMM?
JMM:Java内存模型,不存在的东西,概念、约定!
工作内存和主内存、4组操作:
假设主内存中定义了一个变量:flag = true,需要把这个变量同步到工作内存。
工作内存首先会从主内存中read,read完后再load到工作内存,这样工作内存中也会有flag = true。
执行引擎会use这个flag变量,使用完再将flag变量assign给工作内存。
工作内存将flag变量write,然后再store到主内存。
以上3组操作都会有lock和unlock操作。
我们需要让A线程知道主线程中flag的值发生了变化,并将其同步到工作内存。
public class JMMtest {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while(num == 0) {
}
}).start();
TimeUnit.SECONDS.sleep(2);
num = 1;
System.out.println(num);
}
}
例如以上代码,main线程已经将num改为1,但thread0依然在执行。
说明线程0对主内存的变化是不可见的。
第三十一节、volatile
1. 保证可见性
只需要将上述代码改成
private static int num = 0;
main线程将num改为1后,thread0立即停止执行。
2. 不保证原子性
public class volatileDemo2 {
private volatile static int num = 0;
private synchronized static void add() {
num++; // num++本身就不是一个原子性操作
}
public static void main(String[] args) {
// 理论上应该为2万
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 只要存活的线程数 > 2,就算上面的线程还没执行完,Java中有两条线程是默认在执行的:main和gc
while (Thread.activeCount() > 2) {
Thread.yield(); // 礼让
}
// 以上while循环执行完的话就说明线程一定执行完了
System.out.println(Thread.currentThread().getName() + "线程中 num = " + num);
}
}
最后输出的结果总是 < 20000
如果把add方法改成sychronized,最后输出的num一定是20000。
所以,volatile不保证原子性,synchronized保证原子性。
那如果不用sychronized和Lock,如何实现原子性操作?
num++本身就不是原子性操作,它分为3个步骤:
1、获得num的值
2、在这个值的基础上+1
3、写回这个值
接下来看java.util.concurrent.atomic包:
应当使用原子类解决原子性问题,因为synchronized和Lock很耗费资源:
private static final AtomicInteger num = new AtomicInteger();
private synchronized static void add() {
num.getAndIncrement(); // AtomicInteger的+1方法,用的是底层的CAS机制,CPU的并发效率远比synchronized的效率高
}
java.util.concurrent.atomic包下的类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
第三十二节、指令重排详解
volatile就可以避免指令重排序。
有一个CPU指令-----内存屏障,这个指令有两个作用:
1、保证特定操作的执行顺序。
2、保证某些变量的内存可见性。(volatile正是由此保证的可见性)
如果有三个线程,普通读写线程和加volatile关键字线程的区别:
volatile的防止指令重排序(内存屏障)在哪里使用的最多?
单例模式的DCL懒汉式
第三十三节、玩转单例模式
饿汉式:
有可能会浪费内存,例如下面的4组数据,饿汉式会刚开始就把这4组数据全部加载进来,这些数据都存在,但是未必会使用,这样就可能会造成内存空间的浪费。
如果想要在用到这些数据的时候再创建,就用到了懒汉模式。
饿汉模式在类被初始化时就已经在内存中创建了对象,以空间换时间,故不存在线程安全问题。
public class Hungry {
// 可能会浪费空间
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
// new Hungry()的时候就会加载以上4组数据
private static Hungry hungry = new Hungry();
private Hungry() {
}
public static Hungry getInstance() {
return hungry;
}
}
懒汉式单例模式:
懒汉模式在方法被调用后才创建对象,以时间换空间,在多线程环境下存在风险。
public class Lazy {
private static Lazy lazy;
private Lazy() {
System.out.println(Thread.currentThread().getName() + "ok");
}
public static Lazy getInstance() {
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
// 多线程下显然就会出问题
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(Lazy::getInstance).start();
}
}
}
可以发现并不是单例。
解决方案:DCL懒汉式
// 双重校验锁模式的懒汉式,即DCL懒汉式
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
理论上DCL懒汉式没啥问题,但是还是有可能出问题的。
问题在lazy = new Lazy(),因为这行代码并不是一个原子性操作。它分为3步:
1、分配内存空间
2、执行构造方法,初始化对象
3、把引用lazy指向这片内存空间
假如发生了指令重排序,线程A执行的步骤是132,此时来了线程B,由于A已经执行了13,B会认为lazy != null,直接return lazy,但此时A还没有执行2,所以B返回的lazy依旧是null。
由于volatile可以防止指令重排序,所以完整的DCL懒汉式应当是这样写的:
private volatile static Lazy lazy;
// 双重校验锁模式的懒汉式,即DCL懒汉式
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
静态内部类模式:
package com.kuang.single;
/**
* 静态内部类实现,外部类是Holder,内部类是InnerClass
*/
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.holder;
}
public static class InnerClass {
private static final Holder holder = new Holder();
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。
但是以上三种技术都是不安全的,因为有反射技术的存在。
public class Lazy2 {
private volatile static Lazy2 lazy2;
private Lazy2() {
}
// 双重校验锁模式的懒汉式,即DCL懒汉式
public static Lazy2 getInstance() {
if (lazy2 == null) {
synchronized (Lazy.class) {
if (lazy2 == null) {
lazy2 = new Lazy2();
}
}
}
return lazy2;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 获取无参构造方法
Constructor<Lazy2> declaredConstructor = Lazy2.class.getDeclaredConstructor(null);
// 破坏私有权限
declaredConstructor.setAccessible(true);
Lazy2 lazy2 = declaredConstructor.newInstance();
Lazy2 lazy21 = declaredConstructor.newInstance();
System.out.println(lazy2);
System.out.println(lazy21);
}
}
获取到了两个不同的对象。
查看newInstance()方法:
如果clazz是枚举类型,就抛出异常:无法利用反射创建枚举对象。
枚举单例:
// enum本身也是一个class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
// Exception in thread "main" java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
我们看到编译后的EnumSingle.class文件是这样的:
用jad对EnumSingle.class进行反编译后,获得EnumSingle.java文件:
注意:没有无参构造,只有有参构造。
于是将Test中的一行代码改成:
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
结果:
这样,才验证了枚举的单例模式并不能被反射破坏!
第三十四节、深入理解CAS
package com.kuang.CAS;
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS:比较并交换,CAS是CPU的并发原语
*/
public class demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expectedValue, int newValue)
// 如果期望的值达到了,就更新,否则就不更新。
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
AtomicInteger有个方法getAndIncrement(),来看一下getAndIncrement方法是怎么实现的:
Java无法直接操作内存,但是Java可以调用C++(通过native方法)来操作内存。
Unsafe类相当于Java的后门,可以通过这个类操作内存。
VALUE是内存地址偏移值
value避免指令重排序
重新回到AtomicInteger的getAndIncrement()方法:
上图代码的while循环实现的就是一个自旋锁,如果不等,就一直循环。
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,则执行操作!如果不是就一直循环!
CAS有三个操作数:期望的值、比较的值、更新的值。
缺点:
1、自旋锁的循环会耗时
2、一次性只能保证一个共享变量的原子问题
3、ABA问题
第三十五节、原子引用解决ABA问题
CAS:对于内存中的某一个值V,提供一个旧值A和一个新值B。如果提供的旧值V和A相等就把B写入V。这个过程是原子性的。CAS执行结果要么成功要么失败,对于失败的情形下一班采用不断重试。或者放弃。
ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。
关于ABA问题的一个例子:在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。解决这个问题的方案的一个策略是每一次倒水假设有一个自动记录仪记录下,这样主人回来就可以分辨在她离开后是否发生过重新倒满的情况。这也是解决ABA问题目前采用的策略。
ABA问题:
public class demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expectedValue, int newValue)
// 如果期望的值达到了,就更新,否则就不更新。
// -------------- 捣乱的线程 ----------
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// -------------- 期望的线程 ---------
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
如何解决呢?
用带版本号的原子操作,也就是java.util.concurrent.atomic包下的AtomicReference类,它可以控制原子更新的对象引用。实现原理和乐观锁的原理相同。
带时间戳的原子引用:
传入初始引用和初始时间戳值。时间戳就相当于版本号。
package com.kuang.CAS;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class demo2 {
public static void main(String[] args) {
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println("a1.stamp = " + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 期望的值:2020 新值:2021 期望的值的版本号:reference.getStamp() 新值的版本号:reference.getStamp() + 1
System.out.println(reference.compareAndSet(1, 2, reference.getStamp(), reference.getStamp() + 1));
System.out.println("a2.stamp = " + reference.getStamp());
System.out.println(reference.compareAndSet(2, 1, reference.getStamp(), reference.getStamp() + 1));
System.out.println("a3.stamp = " + reference.getStamp());
}, "a").start();
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println("b1.stamp = " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(reference.compareAndSet(1, 6, stamp, stamp + 1));
System.out.println("b2.stamp = " + reference.getStamp());
}, "b").start();
}
}
遇到的坑:
第三十六节、可重入锁
公平锁、非公平锁
公平锁:非常公平,不能插队,必须是先来后到
非公平锁:非常不公平,可以插队(例如一个线程3s执行完,另一个线程3h执行完,不能让3s的线程等待3h的线程吧)
无论synchronized还是Lock默认都是非公平的。
可以查看Lock的源码:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可重入锁(递归锁)
就相当于回家,拿到了大门的锁也就自动获得了卧室的锁。
public class demo1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendMes();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
try {
phone.sendMes();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
}
class Phone {
public synchronized void sendMes() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "sendMes");
TimeUnit.SECONDS.sleep(2);
call();
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + "call");
}
}
结果一定是这样的:
AsendMes后,等待2s,Acall,BsendMes,等待2s,Bcall。
sychronized就是可重入锁,拿到了外面的锁(sendMes)也就拿到了里面的锁(call)。
Lock锁也是可重入锁:
package com.kuang.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class demo1 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(phone::sendMes, "A").start();
new Thread(phone::sendMes, "B").start();
}
}
class Phone2 {
Lock lock = new ReentrantLock();
public void sendMes() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "sendMes");
TimeUnit.SECONDS.sleep(2);
call();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "call");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
执行结果和synchronized一样。
public void sendMes() {
lock.lock(); // 细节问题:Lock锁必须配对(加锁就得解锁),否则就会死锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "sendMes");
TimeUnit.SECONDS.sleep(2);
call(); // 这里也有锁
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
lock.unlock();
}
}
第三十七节、自旋锁
自旋锁在Unsafe类中见过:
package com.kuang.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋锁
*/
public class spinLockDemo {
public static void main(String[] args) {
MySpinLock lock = new MySpinLock();
new Thread(() -> {
lock.lock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "T1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "hahaha");
} finally {
lock.unlock();
}
}, "T2").start();
}
}
class MySpinLock {
// Thread 不赋值的话默认是null
AtomicReference<Thread> reference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "加锁");
// 自旋锁 底层使用的是CAS操作
while (!reference.compareAndSet(null, thread)) {
}
}
public void unlock() {
Thread thread = Thread.currentThread();
reference.compareAndSet(thread, null);
System.out.println(thread.getName() + "解锁");
}
}
第三十八节、死锁排查
死锁是什么?
A线程占有A锁,B线程占有B锁,A想去争夺B锁,B想去争夺A锁,他们都不放自己手中的锁却又想试图获取对方的锁,就会造成死锁。
死锁测试:怎么排除死锁?
死锁代码示例:
package com.kuang.lock;
import java.util.concurrent.TimeUnit;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA, lockB)).start();
new Thread(new MyThread(lockB, lockA)).start();
}
}
@Data
@AllArgsConstructor
class MyThread implements Runnable {
private String lockA;
private String lockB;
@SneakyThrows
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + " get " + lockA);
TimeUnit.SECONDS.sleep(2);
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + " get " + lockB);
}
}
}
}
如何解决呢?
1、使用jps -l查看进程号:
2、使用jstack 进程号查看找到死锁问题:
面试、工作中排查问题:1、看日志。2、看堆栈信息。
面试手写:
单例模式,
排序算法
生产者和消费者
死锁
总结各种锁:
悲观锁(互斥同步锁)和乐观锁(非互斥同步锁):
悲观锁:修改数据前先把数据锁住,再修改数据,其他线程此时是无法访问该数据的。
乐观锁:乐观锁认为自己在对数据进行操作的时候不会有其他线程干扰,所以在更新数据的时候就会对比在我修改的期间这份数据是否被别人更改过(通过版本号的方式),如果没有被改过就更新数据,如果被改过就执行放弃、报错、重试等策略。
一般通过CAS算法或者版本号来实现。
实际场景举例:
- Git中:
乐观锁:push代码到远程仓库,会默认检查远程仓库中代码的版本号和我提交的代码的版本号,如果远程仓库的版本号>我提交的代码的版本号,就会push失败,说明在我push代码之前已经有人push了,我应当先pull下来再push。 - 数据库中:
悲观锁:select xxx from xxx for update;for update就是悲观锁的一种实现。
当左边(事务A)使用了 select … for update 的悲观锁后,右边(事务B)再想使用将被阻塞,同时,阻塞被解除后事务B能看到事务A对数据的修改,所以,这就可以很好地解决并发事务的更新丢失问题啦(诚然,这也是人家悲观锁的分内事)
乐观锁:添加一个字段version,每一个更新都是基于版本号去更新
当一个线程修改xxx表时:
update xxx set name = xxx, age = xxx, version = version + 1 where version = 1;
悲观锁的缺点:
1、阻塞需要排队,唤醒需要时间,性能上的劣势。
2、如果有一个线程被永久阻塞(死循环、死锁),其他线程就永远无法执行。
3、优先级。阻塞的优先级越高,持有锁的优先级越低,导致优先级反转问题。
开销对比:悲观锁的原始开销 > 乐观锁