`## 并发编程
1. 什么是juc
java.util.concurrent
在并发编程中使用的工具类
2. 线程基础
2.1 进程和线程
-
进程
一个程序,QQ.exe
一个进程往往可以包含多个线程
java有多个线程? 2个 main GC
-
线程
比如安全管家是一个进程,那么杀毒、垃圾清理都是一个线程
2.2 并发和并行
Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别
并发是两个队列交替使用一台咖啡机
并行是两个队列同时使用两台咖啡机
串行是一个队列只能使用一台咖啡机顺序执行
并发是不是一个线程,并行是一个线程?
答:并行和并发都可以是多个线程,就看这些线程能否同时被多个cpu执行,如果可以就是并行,而并发是多个线程被一个cpu轮流切换执行
2.3 线程的几个状态
// Thread.state 枚举类
public enum State {
// 新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待
TIMED_WAITING,
//终止、停止
TERMINATED;
}
2.5 wait/sleep区别
-
来自不同的类
wait => Object
sleep => Thread
-
关于锁的释放
wait会释放锁
sleep不会释放锁
-
使用返回不同
wait 只能在同步代码块中使用
sleep 可以在任何地方使用
-
是否需要捕获异常
wait 不需要捕获
sleep 必须捕获
2.6 Synchronzed和lock的区别
-
Synchonized是java关键字, lock是一个java类
-
Synchonized 无法判断获取锁的状态,lock可以判断是否获取到了锁
-
Synchonized 会自动释放锁 , lock需要手动释放锁!如果不释放锁会造成死锁
-
Synchonized 线程1(获得锁,阻塞),线程2(等待); lock就不一定会等在下去
lock.tryLock()
-
可重入锁,不可中断的,非公平; lock,可重入锁,可以判断锁,非公平(可以自己设置)
-
Synchonized 适合锁少量的代码同步问题, lock适合锁大量的同步代码
2.7 创建线程的方式
-
继承
Thread
类 -
实现
Runable
接口 -
实现
Callable
接口实现Runable和Callable接口的类只能被当成一个 在线程中运行的任务,不是真正意思上的线程,因此最终还是要通过Thread来调用。
2.8 线程创建-接口VS实现类
实现接口要好一些因为:
- java不支持多继承,因此集成Thread的类无法继承别的类,但是可以实现多个接口
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
3. Snchronized和Lock
java提供了两种锁机制来控制多个线程对共享资源的互斥访问。第一个是JVM的Synchronized关键字,另一个是jdk实现的ReentrantLock
3.1 synchronized
synchronized关键字可以修饰代码块、一个方法、一个类、一个静态方法
public synchronized void func () {
// ...
}
3.2 ReentrantLock
ReentrantLock是java.util.concurrent(J.U.C)
中的锁
public class ReentrantLockDemo {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
3.3 区别
-
锁的实现
Synchronized
是JVM实现的是一个关键字,ReentrantLock
是jdk实现的 -
性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
-
等待可中断
当持有锁的线程长时间不释放锁的时候,正在等待的其它线程可以选择放弃等待,改为处理其它事情
-
公平锁非公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时候来顺序执行的。 java中一般都是非公平锁。
列: 如果两个线程一个执行10分钟,另一个执行1秒钟情况呢? 肯定使用非公平锁最好喽
synchornized
是非公平锁
,ReentrantLock
默认也是非公平锁
,但是可以改为公平锁 -
锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
线程之间的协作:
wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
4. JMM、volatile
在说volatile之前需要先了解一下
JMM(java内存模型)
4.1 并发编程的3个基本概念
原子性
定义: 即一个操作或者多个操作,要么全部执行并且执行过程中不被任何因素打断,要么就都不执行
可见性
定义: 即多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立刻看到修改的值
有序性
定义: 即程序执行的顺序按照代码的先后顺序执行
4.2 JMM
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。
4.3 volatile
保证可见性
不保证原子性
禁止指令重排
5. JUC Lock汇总
5.1 Condition 接口
condition
是在java1.5中才出现的,它是用来替代传统Object的wait(),notify()实现线程之间的协作的
,相对使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效
。
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
5.2 Lock 接口
java中有两种方式实现同步访问, 第一是使用synchronized关键字,另一种方式就是在java.util.concurrent.locks包下提供了一种方式来实现同步访问这就是
lock
// Lock接口的源码
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
5.3 ReadWriteLock 接口
Synchronized的存在明显的问题就是
读与读之间互斥
,所以出现了ReadWriteLock
接口
ReentrantReadWriteLock
是ReadWriteLock 的实现类
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
5.4 ReentrantLock 类
ReentrantLock为常用类,它是一个
可重入的互斥锁 Lock
,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。
5.5 ReentrantReadWriteLock 类
ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类
,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。
6. JUC 工具类
6.1 CountDownLatch 计数器
CountDownLatch为常用类,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
// 计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown(); // 数量-1
},String.valueOf(i)).start();
}
countDownLatch.await(); // 等待计数器归零,然后再向下执行
System.out.println("Close Door");
}
}
countDownLatch.countDown(); // 数量-1
countDownLatch.await(); // 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续
执行!
6.2 CyclicBarrier 加法计数器
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <=7 ; i++) {
final int temp = i;
// lambda能操作到 i 吗
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
try {
cyclicBarrier.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
6.3 Semaphore:信号量
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; 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();
}
}
}
原理:
semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
7. Collections 并发集合
7.1 BlockingQueue 阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
先进先出。 写入:
如果队列满了就必须阻塞等待
获取:如果队列是空的就必须阻塞等待生产
BlockingQueue 具有4组不同的方法用于插入、移除、以及对队列中的元素进行检查。
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer |
移除 | remove | poll | take | poll |
检测队首元素 | element | peek | - |
例:
public static void test1(){
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// IllegalStateException: Queue full 抛出异常!
// System.out.println(blockingQueue.add("d"));
System.out.println("=-===========");
System.out.println(blockingQueue.element()); // 查看队首元素是谁
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// java.util.NoSuchElementException 抛出异常!
// System.out.println(blockingQueue.remove());
}
实现类:
ArrayBlockingQueue
LinkedBlockingQueue
ConcurrentLinkedQueue
7.2 Deque 双端阻塞队列
该队列同时支持FIFO和FILO两种操作方式
8. 线程池
线程池:
3大方法、7大参数、4种拒绝策略
8.1 Executors 工具类
ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线 程// ExecutorService ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一 个固定的线程池的大小 // ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩 的,遇强则强,遇弱则弱
8.2 ThreadPoolExecutor 类
4种拒绝策略
// ThreadPoolExecutor 构造器
// new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
// new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
// new ThreadPoolExecutor.DiscardPolicy() // 队列满了,丢掉任务,不会抛出异常!
// new ThreadPoolExecutor.DiscardOldestPolicy() // 队列满了,尝试去和最早的竞争,也不会抛出异常!
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大线程池大小
long keepAliveTime, //存活时间超时了没有人调用就会释放
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) { //拒绝策略
// ....
}
public class Demo01 {
public static void main(String[] args) {
// 最大线程到底该如何定义
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
// 2、IO 密集型 > 判断你程序中十分耗IO的线程,
// 程序 15个大型任务 io十分占用资源!
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
List list = new ArrayList();
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
9. 四大函数式接口
函数式接口: 只有一个方法的接口 如Runable接口里面只有一个run方法
9.1 Function函数式接口
/**
* Function 函数型接口, 有一个输入参数,有一个输出
* 只要是 函数型接口 可以 用 lambda表达式简化
*/
public class FunctionDemo1 {
public static void main(String[] args) {
// 传统方式
// Function<String,String> function = new Function<String,String>() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
// lambda表达式简化
Function<String,String> function = str->{return str;};
System.out.println(function.apply("asd"));
}
}
9.2 Predicate断定式
/**
* 断定型接口:有一个输入参数,返回值只能是 布尔值!
*/
public class Demo02 {
public static void main(String[] args) {
// 判断字符串是否为空
// Predicate<String> predicate = new Predicate<String>(){
// @Override
// public boolean test(String str) {
// return str.isEmpty();
// }
// };
Predicate<String> predicate = (str)->{return str.isEmpty(); };
System.out.println(predicate.test(""));
}
}
9.3 Consumer 消费型接口
/**
* Consumer 消费型接口: 只有输入,没有返回值
*/
public class Demo03 {
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer = (str)->{System.out.println(str);};
consumer.accept("sdadasd");
}
}
9.4 Supplier 供给型接口
/**
* Supplier 供给型接口 没有参数,只有返回值
*/
public class Demo04 {
public static void main(String[] args) {
// Supplier supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// System.out.println("get()");
// return 1024;
// }
// };
Supplier supplier = ()->{ return 1024; };
System.out.println(supplier.get());
}
}
10. CAS
CAS的全称是Compare-And-Swap,它是一条CPU并发原语。
正如它的名字一样,比较并交换,它是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。
CAS的方式为乐观锁,Synchronized为悲观锁。因此使用CAS解决并发问题通常情况下更优
10.1 CAS的ABA问题
所谓ABA问题,其实用最通俗易懂的话语来总结就是狸猫换太子
就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。
例:
比如有两个线程:
一开始都从主内存中拷贝了原值3;
A线程执行var5=this.getIntVolatile,即var5=3。此时A线程挂起;
B修改原为4,B线程执行完毕;
然后B线程觉得修改错了,然后又重新把值改为3;
A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值等于快照值3,(**但是却不知道B曾经修改过**),修改成功。
如何解决?
通过AtomicReference原子引用,就是加版本号。 本质上就是一个乐观锁的实现。
10.2 Unsafe 类
java不能直接访问操作系统,而是通过本地方法来调用。Unsafe类提供了硬件级别的原子操作,主要提供了一下操作
- 内存管理。包括分配内存、释放内存等。
- 非常规的对象实例化
- 操作类、对象、变量
- 数组操作
- 多线程同步
- 挂起与恢复。
- 内存屏障。
10.3 AtomicStampedReference 原子引用
解决ABA问题,引入原子引用! 跟乐观锁思想类似
public class CASDemo {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(1, 1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Lock lock = new ReentrantLock(true);
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("a2=>" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}