线程与进程
1.一个进程之内可以分为一到多个线程。
2.一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
3.Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。
1.进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
2.进程拥有共享的资源,如内存空间等,供其内部的线程共享
3.进程间通信较为复杂
a.同一台计算机的进程通信称为 IPC(Inter-process communication)
b.不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
4.线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
5.线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并发与并行
并发(一人多事)—家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
既有并发又有并行—家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
并行(各做个事不冲突)—雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
面试:单例,排序算法,生产者消费者,死锁。
4、生产者和消费者问题(线程通信)
创建多个线程去执行不同的任务,如果这些任务之间有着某种关系,那么线程之间必须能够通信来协调完成工作。
生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据
注意:生产者-消费者模式中的内存缓存区的主要功能是数据在多线程间的共享,此外,通过该缓冲区,可以缓解生产者和消费者的性能差;
Synchronized版本 :问题存在,A线程B线程,没有问题,但现在如果我有四个线程A B C D!,就有问题,存在虚假唤醒。因为if只会判断一次
用if判断多线程会存在虚假唤醒的问题,改用while可避免该问题。if 改为while即可,防止虚假唤醒,保证线程是安全的。
Condition的优势:精准的通知和唤醒的线程
先调用不一定先执行。
静态方法是类一加载就有了。
异常:oom,栈溢出,内存溢出。
5、8锁问题
8锁问题
如果两个锁上锁的是同一个对象,则必须要等到一个方法执行结束才能执行下一个方法,而当两个锁上锁的不是同一个对象,则当一个方法发生执行延迟时,就可以先执行下一个方法!
我们可以采用synchronized 给方法上锁,即表示方法是普通同步锁,使用synchronized 上锁的是方法的调用者;
我们也可以采用static synchronized 给方法上锁,表示方法是静态的同步方法锁,使用static synchronized 上锁锁的是资源类的Class类模板,而Class类模板是全局唯一;
规律:如果上锁的是同一个对象,则执行会按照先后顺序依次执行,只有前面的方法执行结束,才能执行后面的方法;如果上锁的不是同一个对象,首先也会按照先后顺序执行,一旦执行的过程中发现有sleep方法,即延迟,有CPU的空闲,就会执行下面与它不是同一个锁的方法,提高CPU执行的效率!
原文链接:8锁原文参考
6、集合不安全
并发情况下集合可能不安全,JUC中提供了并发下的各种集合类
ArrayList 在并发情况下是不安全的!会出现: java.util.ConcurrentModificationException
vactor比ArrayList先出来且是线程安全的,vactor源码中加了 Synchronized 效率特别低下。。
解决:使用JUC中的包:List arrayList = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList用到了写时复制的技术,读写分离的思想来实现的,CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
CopyOnWrite容器即写时复制的容器。往一个容器中添加一个元素的时候,不直接往当前容器Object[]中添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后新的容器里Object[] newElements添加元素,添加元素之后,再将原容器的引用指向新的容器setArray(newElements)。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前元素不会添加任何元素。所有CopyOnWrite容器也是一种读写分离的思想,读和写是不同的容器。
Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;
解决方案还是两种:
• 使用Collections工具类的synchronized包装的Set类
• 使用CopyOnWriteArraySet 写入复制的JUC解决方案
HashSet底层是什么?
hashSet底层就是一个HashMap;
//add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储
//hashSet就是使用了hashmap key不能重复的原理
同样的HashMap基础类也存在并发修改异常!
解决方案:
• 使用Collections.synchronizedMap(new HashMap<>());处理;
• 使用ConcurrentHashMap进行并发处理
futureTask.get(); 这个方法可能会产生阻塞,因为要等待结果的返回,等待结果的过程可能会很耗时,所以一般把这个放到最后。
起了两个线程,但call 只执行了一次,因为结果被缓存,效率更高。
8、常用的辅助类(必会!)
8.1 CountDownLatch
其实就是一个减法计数器,对于计数器归零之后再进行后面的操作,这是一个计数器!
主要方法:
• countDown 减一操作;
• await 等待计数器归零。
await等待计数器为0,就唤醒,再继续向下运行。
8.2 CyclickBarrier
其实就是一个加法计数器;
lambda表达式是new了一个类,正常是拿不到for循环里的变量,要拿,可以通过final
8.3 Semaphore
Semaphore:信号量
原理:
semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
9、读写锁
如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。
我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证
ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程。
读写互斥,读读共享。
读的时候可以多个线程一起读
写的时候只能一个线程写
10、阻塞队列
blockingQueue 是Collection的一个子类;
什么情况我们会使用 阻塞队列呢?
多线程并发处理、线程池!
SynchronousQueue同步队列
同步队列 没有容量,也可以视为容量为1的队列;
进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;
put方法 和 take方法;
Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;
put了一个元素,就必须从里面先take出来,否则不能再put进去值!
并且SynchronousQueue 的take是使用了lock锁保证线程安全的。
11、线程池(重点)
线程池:三大方法、7大参数、4种拒绝策略
线程池的好处:
1、降低资源的消耗;
2、提高响应的速度;
3、方便管理;
线程复用、可以控制最大并发数、管理线程;
线程池:三大方法
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
7大参数
本质:三种方法都是开启的ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大的线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
RejectedExecutionHandler handler //拒绝策略
) {
拒绝策略4种
(1)new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常
超出最大承载,就会抛出异常:队列容量大小+maxPoolSize
(2)new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理
(3)new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉异常,不会抛出异常。
(4)new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常
如何去设置线程池的最大大小如何去设置?
CPU密集型和IO密集型!
1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小
2、I/O密集型:
在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
12、四大函数式接口(必需掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
13、Stream流式计算
什么是Stream流式计算?
存储+计算!
.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。
15、异步回调
Future 设计的初衷:对将来的某个事件结果进行建模!
其实就是前端 --> 发送ajax异步请求给后端
(1)没有返回值的runAsync异步回调 CompletableFuture.runAsync
(2)有返回值的异步回调supplyAsync CompletableFuture.supplyAsync
16.JMM
多线程操作
number++;
是不安全的,如果不加lock和synchronized ,怎么样保证原子性?解决方法:使用JUC下的原子包下的class;
private static volatile AtomicInteger number = new AtomicInteger();
number.incrementAndGet(); //底层是CAS保证的原子性
• volatile可以保证可见性;
• 不能保证原子性
• 由于内存屏障,可以保证避免指令重排的现象产生
面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式
18、玩转单例模式
饿汉式、DCL懒汉式
饿汉式:类加载的时候就实例化,并且创建单例对象。
单例模式有以下特点:
-
单例类只能有一个实例;
-
单例类必须自己创建自己的唯一实例;
-
单例类必须给所有其他对象提供这一实例;
-
优缺点
优点:由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统运行效率。
缺点:因为系统中只有一个实例,导致了单例类的职责过重,违背了“单一职责原则”,同时不利于扩展。
我给面试官讲解了单例模式后,他对我竖起了大拇指
原文链接:单例模式讲解参考
19、深入理解CAS
什么是CAS?
//CAS : compareAndSet 比较并交换
原子类中的操作
//boolean compareAndSet(int expect, int update)
//期望值、更新值
//如果实际值 和 我的期望值相同,那么就更新
//如果实际值 和 我的期望值不同,那么就不更新
总结:
CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
缺点:
• 循环会耗时;
• 一次性只能保证一个共享变量的原子性;
• 它会存在ABA问题
CAS:ABA问题?(狸猫换太子)
20、原子引用
解决ABA问题,对应的思想:就是使用了乐观锁~
带版本号的 原子操作!
int stamp = atomicStampedReference.getStamp(); // 获得版本号 每次操作后修改版本号,多线程时,就会感知到不同版本号,也就知道是否对所修改的值是没有被动过,解决了ABA问题。
Integer
使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间。
transient临时的
volatile不稳定的
自旋锁,不断的去尝试直到成功为止