Java 并发编程
并发编程可以充分利⽤计算机的资源,把计算机的性能发挥到最⼤,可以最⼤程度节约公司的成本,提⾼效率。
1、什么是⾼并发
- 并发 concurrency:多线程“同时”操作同⼀个资源,并不是真正的同时操作,⽽是交替操作,单核CPU的情况下,资源按时间段分配给多个线程。张三李四王五使⽤⼀⼝锅炒菜,交替
- 并⾏ parallelism:是真正的多个线程同时执⾏,多核CPU,每个线程使⽤⼀个 CPU 资源来运⾏。张三李四王五使⽤三⼝锅炒菜,同时进⾏
并发编程描述的是⼀种使系统允许多个任务可以在重叠的时间段内执⾏的设计结构,不是指多个任务在同⼀时间段内执⾏,⽽是指系统具备处理多个任务在同⼀时间段内同时执⾏的能⼒。
⾼并发是指我们设计的程序,可以⽀持海量任务的执⾏在时间段上重叠的情况。
进程和线程
Java 默认的线程数 2 个
- mian 主线程
- GC 垃圾回收机制
Java 本身是⽆法开启线程的,Java ⽆法操作硬件,只能通过调⽤本地⽅法,C++ 编写的动态函数库
Java 中实现多线程有⼏种⽅式?
1、继承 Thread 类
2、实现 Runnable 接⼝
3、实现 Callable 接⼝
sleep 和 wait
- sleep 是让当前线程休眠,wait 是让访问当前对象的线程休眠。
- sleep 不会释放锁,wait 会释放锁。
synchronized 锁定的是什么
1、synchronized 修饰⾮静态⽅法,锁定⽅法的调⽤者
2、synchronized 修饰静态⽅法,锁定的是类
3、synchronized 静态⽅法和实例⽅法同时存在,静态⽅法锁定的是类,实例⽅法锁定的是对象
synchronized 和 lock 的区别
1、synchronized ⾃动上锁,⾃动释放锁,Lock ⼿动上锁,⼿动释放锁。
2、synchronized ⽆法判断是否获取到了锁,Lock 可以判断是否拿到了锁。
3、synchronized 拿不到锁就会⼀直等待,Lock 不⼀定会⼀直等待。
4、synchronized 是 Java 关键字,Lock 是接⼝。
5、synchronized 是⾮公平锁,Lock 可以设置是否为公平锁。
⽣产者消费者模式
synchronized
package com.southwind.demo3;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.decrement();
}
},"B").start();
}
}
class Data{
private Integer num = 0;
public synchronized void increment(){
while(num!=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
this.notify();
System.out.println(Thread.currentThread().getName()+"⽣产了汉堡"+num);
}
public synchronized void decrement(){
while(num == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
this.notify();
System.out.println(Thread.currentThread().getName()+"消费了汉堡"+num);
}
}
必须使⽤ while 判断,不能⽤ if,因为 if 会存在线程虚假唤醒的问题,虚假唤醒就是⼀些 wait ⽅法会在除了 notify 的其他情况被唤醒,不是真正的唤醒,使⽤ while 完成多重检测,避免这⼀问题。
Lock
package com.southwind.demo4;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.decrement();
}
},"B").start();
}
}
class Data{
private Integer num = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
while(num!=0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
condition.signal();
System.out.println(Thread.currentThread().getName()+"⽣产了汉堡"+num);
lock.unlock();
}
public synchronized void decrement(){
lock.lock();
while(num == 0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
condition.signal();
System.out.println(Thread.currentThread().getName()+"消费了汉堡"+num);
lock.unlock();
}
}
使⽤ Lock 锁,就不能通过 wait 和 notify 来暂停线程和唤醒线程,⽽应该使⽤ Condition 的 await 和signal 来暂停和唤醒线程。
ConcurrentModificationException
并发访问异常
如何解决?
1、Vector
2、Collections.synchronizedList
3、JUC:CopyOnWriteArrayList
CopyOnWrite 写时复制,当我们往⼀个容器添加元素的时候,不是直接给容器添加,⽽是先将当前容器复制⼀份,向新的容器中添加数据,添加完成之后,再将原容器的引⽤指向新的容器。
4、Set
5、Map
线程池
优势:
- 提⾼线程的利⽤率
- 提⾼响应速度
- 便于统⼀管理线程对象
- 可控制最⼤并发数
线程池的具体设计思想
- 核⼼池的⼤⼩
- 线程池的最⼤容量
- 等待队列
- 拒绝策略
线程池启动的时候会按照核⼼池的数来创建初始化的线程对象 2 个。
开始分配任务,如果同时来了多个任务, 2 个线程对象都被占⽤了,第 3 个以及之后的任务进⼊等待队列,当前有线程完成任务恢复空闲状态的时候,等待队列中的任务获取线程对象。
如果等待队列也占满了,⼜有新的任务进来,需要去协调,让线程池再创建新的线程对象,但是线程池不可能⽆限去创建线程对象,⼀定会有⼀个最⼤上限,就是线程池的最⼤容量。
如果线程池已经达到了最⼤上限,并且等待队列也占满了,此时如果有新的任务进来,只能选择拒绝,并且需要根据拒绝策略来选择对应的⽅案。
ThreadPoolExecutor
直接实例化 ThreadPoolExecutor ,实现定制化的线程池,⽽不推荐使⽤ Executors 提供的封装好的⽅法,因为这种⽅式代码不够灵活,⽆法实现定制化。
ThreadPoolExecutor 核⼼参数⼀共有 7 个
-
corePoolSize:核⼼池的⼤⼩
-
maximumPoolSize:线程池的最⼤容量
-
keepAliveTime:线程存活时间(在没有任务可执⾏的情况下),必须是线程池中的数量⼤于 corePoolSize,才会⽣效
-
TimeUnit:存活时间单位
-
BlockingQueue:等待队列,存储等待执⾏的任务
-
ThreadFactory:线程⼯⼚,⽤来创建线程对象
-
RejectedExecutionHandler:拒绝策略
1、AbortPolicy:直接抛出异常
2、DiscardPolicy:放弃任务,不抛出异常
3、DiscardOldestPolicy:尝试与等待队列中最前⾯的任务去争夺,不抛出异常
4、CallerRunsPolicy:谁调⽤谁处理
package com.zeng;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test1 {
public static void main(String[] args) {
//单例
// ExecutorService executorService = Executors.newSingleThreadExecutor();
//数量固定线程池
// ExecutorService executorService = Executors.newFixedThreadPool(5);
//缓存线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 30; i++) {
final int temp= i;
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+":"+temp);
});
}
}
}
线程池 3 ⼤考点:
1、Executors ⼯具类的 3 种实现
//单例
ExecutorService executorService = Executors.newSingleThreadExecutor();
//数量固定线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
//缓存线程池
ExecutorService executorService = Executors.newCachedThreadPool();
2、7 个参数
-
corePoolSize:核⼼池的⼤⼩
-
maximumPoolSize:线程池的最⼤容量
-
keepAliveTime:线程存活时间(在没有任务可执⾏的情况下),必须是线程池中的数量⼤于 corePoolSize,才会⽣效
-
TimeUnit:存活时间单位
-
BlockingQueue:等待队列,存储等待执⾏的任务
-
ThreadFactory:线程⼯⼚,⽤来创建线程对象
-
RejectedExecutionHandler:拒绝策略
3、4 种拒绝策略
1、AbortPolicy:直接抛出异常
2、DiscardPolicy:放弃任务,不抛出异常
3、DiscardOldestPolicy:尝试与等待队列中最前⾯的任务去争夺,不抛出异常
4、CallerRunsPolicy:谁调⽤谁处理
ForkJoin 框架
本质上是对线程池的⼀种的补充,对线程池功能的⼀种扩展,基于线程池的,它的核⼼思想就是将⼀个⼤型的任务拆分成很多个⼩任务,分别执⾏,最终将⼩任务的结果进⾏汇总,⽣成最终的结果。
- 本质就是把⼀个线程的任务拆分成多个⼩任务,然后由多个线程并发执⾏,最终将结果进⾏汇总。
- ⽐如 A B 两个线程同时还执⾏,A 的任务⽐较多,B 的任务相对较少,B 先执⾏完毕,这时候 B 去帮助A 完成任务(将 A的⼀部分任务拿过来替 A 执⾏,执⾏完毕之后再把结果进⾏汇总),从⽽提⾼效率。
⼯作窃取
ForkJoin 框架,核⼼是两个类 - ForkJoinTask (描述任务)
- ForkJoinPool(线程池)提供多线程并发⼯作窃取
使⽤ ForkJoinTask 最重要的就是要搞清楚如何拆分任务,这⾥⽤的是递归思想。
1、需要创建⼀个 ForkJoinTask 任务,ForkJoinTask 是⼀个抽象类,不能直接创建 ForkJoinTask 的实例化对象,开发者需要⾃定义⼀个类,继承 ForkJoinTask 的⼦类 RecursiveTask ,Recursive 就是递归的意思,该类就提供了实现递归的功能。
Volatile 关键字
Volatile 是 JVM 提供的轻量级同步机制,可⻅性,主内存对象线程可⻅。
⼀个线程执⾏完任务之后还,会把变量存回到主内存中,并且从主内存中读取当前最新的值,如果是⼀个空的任务,则不会重新读取主内存中的值。
package com.zeng;
import java.util.concurrent.TimeUnit;
public class volatileTest {
private static volatile int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(1);
}
}
package com.zeng;
import java.util.concurrent.TimeUnit;
public class volatileTest {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(1);
}
}
package com.zeng;
import java.util.concurrent.TimeUnit;
public class volatileTest {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
System.out.println("--------");
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(1);
}
}