Java真的可以开启线程么?
不可以
java是运行在虚拟机上的,无法直接操作系统
java的start方法,调用的是一个native方法,这个方法时底层c++的方法,用于开启线程
并发与并行
并发:多个线程操作一个资源(单核cpu,模拟出来多个线程,操作一个资源)
并行:在cpu多核情况下才发生,多个线程可以同时执行(线程池)
并发编程的本质:充分利用CPU的资源!
wait和sleep区别
1.来自不同的类
wait来自Object
sleep来自Thread
2.锁的释放
sleep抱着锁睡
wait会释放锁
3.使用范围不同
sleep能在任何地方用
wait必须在同步代码块中用
4.是否捕获异常
wait不用捕获
sleep需要捕获
Synchornized和 Lock的区别
- Synchronized是一个java关键字,而lock是一个java类
- Synchronized无法判断锁的状态,lock通过lock unlock方法知晓是否获得锁
- SYnchronized是全自动,lock是需要手动获取和关闭的
- Synchronized和 lock都是可重入锁,但是lock有方法trylock,可以防止别的未获得锁的线程持续等待
- Synchronized是非公平的;lock可以设置为公平和非公平锁
- Synchronized适合少量代码的锁,lock适合大量代码,更安全
生产者消费者模式产生的问题
在这种模式中会存在虚假唤醒的情况。
也就是说,如果两个消费者线程,无论哪个线程抢到了资源,另一个线程的唤醒就没必要了,就称为虚假唤醒。
解决虚假唤醒的方法是 每次资源改变的时候都要重新判断,所以将if语句改成while就可解决
Synchronized的wait和notify对应lock的什么方法?
wait 和notify对应 Condition接口中的await方法和singal方法(signalAll)
Condition替代了Synchronized中同步监视器的方法
Condition的出现可以让线程更精准的执行和等待,具体方法是根据每个线程都创建各自的Condition,然后精准的执行等待和唤醒
锁的是谁???
synchronized 修饰方法时,锁的是调用该方法的对象
static synchronized修饰方法时,锁的是class类模板
Class类和对象互不相关,比如有两个对象,分别锁了对象和锁了class模板,那么两个线程互不相干(对象在堆,class模板在静态方法区)
如何解决List不安全的问题
- 用Vector代替ArrayList
- 用Collections.Synchronized(arraylist)集合方法让其线程安全
- JUC下的包有个 CopyOnWriteArrayList类,这个类的底层是一个 transient volatile 数组,保证其安全
这种机制对应的就是安全失败机制
为什么用copyonwrite而 不用Vector呢?
前者是在读取的时候复制一份,然后再set回数组
而Vector用的是syn关键字,
并且前者的读取是没有锁的
所前者以效率更高
怎么解决HashMap线程不安全?
-
Collections.syn方法转换
(原理:synchronizedMap方法内维护了一个Map和一个mutx排斥锁,如果创建这个对象的时候,只传入map对象,那么将对象的排斥锁设置为this对象,即传入的map,若传入了map对象和排斥锁mutx,那么排斥锁设置为传入的mutx) -
使用HashTable替代HashMap,线程安全,但是效率不高,因为所有的方法都加上了Synchronized关键字
-
用 new ConcurrentHashMap替代 HashMap
HashTable为什么不允许空的key-value,而HashMap允许有空的key-value?
因为HashTable设置了对于空值的处理,put的时候会直接报空指针异常。只要出现null值,就会报异常
底层原因是 HashTable使用的是安全失败机制,这样就会导致每次读到时数据都是未修改的原始数据,安全失败机制在改数据的时候会在复制的集合中改
所以 使用null值就无法判断key是存在还是 null,无法用contains判断是否key是存在的
安全失败
fail-safe
JUC包下的容器都是安全失败的,如CopyOnWrite和ConcurrentHashMap,原理就是在写入的时候,将集合的内容复制一份,然后在拷贝的集合上遍历,然后,遍历的情况是不会在原有集合所检查到的
所以不会触发 ConcurrentModifyException
快速失败
多线程下,在原有的集合上直接修改数据是 快速失败,非线程安全
多线程下会有 ConcurrentModifyException异常
说回ConcurrentHashMap的底层原理
1.7以前:和HashMap一样,都是用数组+链表实现(Segment+HashEntry)
并发量高的原因
Segment使用的是分段锁,Segment继承于ReentrantLock,一个线程访问一个Segment时,不会影响其他Segment的锁。
支持线程并发的数量 就是Segment分段锁的数量
get方法时全程不加锁的,HashEntry中的值都是被volatile修饰的,保证每次都是最新的可见的值。所以get方法很高效
1.8以后的数据结构
1会发现1.7之前 put方法还是要先找到数组,再遍历链表对应的值
效率不高
在1.8以后,摒弃了Segment分段式锁,转而采用了 CAS+synchronized确保并发的安全
把HashEntry改成了Node,把next采用volatile修饰。并且引入了红黑树
Callable
怎么调用?
新建类实现callable接口,然后在主线程中 new Thread,并把new的 FutureTask(新建类)放入Thread,并start开启
本质上:FutureTask是Runnable接口下的实现类,FutureTask内的构造方法有两种,一种是传入Callable,一种是传入Runnable,所以用FutureTask就是间接使用了Runnable
辅助工具(JUC下), AQS同步器的组件(AQS的实现类!)
1。减法计数器(CountDownLatch)
new CountDownLatch(n),传入的参数是计数器初始数量
每次
cd.countdown(); 计数器就减一
cd.await();就会等待计数器归零,再执行下面的操作
用于执行一些必须要执行完的任务
2.加法计数器(CyclicBarrier)
new Cyclic Barrier(7,()->{召唤神龙});
内部参数为需要运行的线程数,和满足条件后执行的线程
通过await方法,让计数器++;数量到达设定值时可以召唤神龙
3。 信号量Semaphore
new Semaphore(3) 参数表示 许可证的数量
acquire方法用于获取许可证 数量-1
release方法用于释放许可证 数量+1
当许可证为0时,其他线程需要等待获得许可证
让线程数量有限
在限流中会用到
读写锁
实现类:ReentrantReadWriteLock
可多个线程读,写的时候只能由有一个线程写。
更加细粒度的控制线程
在写的时候 lock.writelock().lock();进行加写锁
在读的时候 lock.readlock().lock(); 进行加读锁
解锁有unlock方法
ReentrantReadWriteLock中有读锁和写锁,读和写的时候分别操作不同的锁
这种方式能提高并发量
BolckingQueue
BlockingQueue继承Queue是Collection接口下的接口,与 Set List 一样。
实现类有ArrayListBlockingQueue 和 LinkListBlockingQueue
四组BolckingQueue的API
1.会抛出异常:add,remove
2. 不会抛出异常,有返回值(true,false/ true,null):offer,poll
SynchronousQueue(继承BlockingQueue)
进去一个元素,必须等待取出,才能继续
相当于只能存一个元素,put存 take取
线程池
线程池的好处
- 防止资源消耗
- 提高响应速度
- 方便管理
- 可以控制最大并发数
为什么不建议用Exectuors建立线程池
会造成OOM。
所以用ThreadPoolExecutor可以规避风险,防止资源耗尽
三大方法,七大参数,四种拒绝策略
三大方法
new SinglePoolThreadExector;
new FixedPoolThreadExector;
new CachedPoolThreadExector;
三大方法底层调用的都是ThreadPoolExecutor!!
七大参数
四大拒绝策略
最大线程数 如何定义?
1. CPU密集型 (CPU是几核,就写多少)
如何获取本机CPU的核数呢??
用 Runtime.getRuntime().availableProcessors 获取 并填入 最大线程数
2. IO密集型
先判断你的程序中 十分耗用IO的线程数,假设为15,那么就需要把 最大线程数 设置为 两倍的IO线程数(30)
四大函数式接口
函数式接口:只有一个方法的接口
简化编程
Function 函数型接口
lamda表达式 可简化为
Predicate 断定型接口
输入一个固定的值,返回一个boolean类型的值
lambda表达式简化
Consumer 消费型接口
Supplier 供给型接口
没有参数,只有返回值
Stream流式计算,链式编程,lamda表达式,函数式接口
ForkJoin(分支合并计算)
大数据量,并行执行数据会用
ForkJoin的工作窃取
维护的是一个双端队列
使用步骤:
- new ForkJoinPool();
- 通过这个pool .execute();或 submit();
- 新建ForkJoinTask 一个需要执行的任务,并把其任务作为参数放入execute的方法中
- submit.get 拿到结果
并行流计算
Java线程中的异步调用
Future接口下的 CompletableFuture 实现线程异步通信
无返回值的异步回调
有返回值的异步回调,编译成功和编译失败都返回结果
Volatile
说说你对Volatile的理解?
Volatile 是java虚拟机提供的轻量级的 同步机制
1. 保证可见性例如两个线程,如果主存中的数据不加Volatile。那么在其中一个线程改变主存中的数据时候,另一个线程中读到的数据就不是最新的值。所以加上Volatile就可以保证这种情况不会发生
2. 不保证原子性:原子性就是不可再分,所有的操作要么一起执行;要么都不执行。Volatile不能确保原子性。所以为了确保原子性,在不用Synchronized 和Lock的情况下,可以用Atom原子类包来代替基本的数据类型,来保证原子性。 Atom的顶层原理是CAS
3. 禁止指令重排
指令重排:指的是程序并不按照代码的顺序执行。源代码会经过编译器优化重排,内存系统重排,最后再执行。
Volatile 可以禁止指令重排
Volatile关键字被增加后,会在该条代码的执行前和执行后增加内存屏障,内存屏障用于保证固定的执行顺序,并且保证某些变量的可见性
什么是JMM
是java内存模型,不存在的东西,是一种约定
Atoms类
底层直接和操作系统挂钩,直接在内存中修改值
懒汉式单例模式 以及volatile怎么防止了指令重排
对于以上的饿汉式单例模式可以用反射的setAccessible破解
所以为了防止被反射破解,可以更改构造器的代码,加锁
但是归根结底,还是能用反射破解单例模式,道高一尺,魔高一丈!
所以这个时候就需要枚举了,枚举是一个类,它自带单例模式!可以防止被反射破解
深入 CAS
CAS:Compare And Set(expect,update) 比较并交换,是原子类的一个方法。
比较的是工作内存中的值,和主内存中的值,如果相等,那么就执行更新主内存中的值为update
Atoms原子类底层的方法调用了Unsafe类,Unsafe类是java留的后门,Unsafe类帮助java直接进行系统操作。Unsafe类中大部分都为native方法,用于直接操作系统,unsafe中的方法有一个 Compare and SwapInt 是更底层的CPU的cas
但是Java层面的CAS是在 atoms类中的方法 CompareAndSet
CPU层面的CAS方法是自旋锁,一直不停的比较并交换!!
CAS的 缺点
CAS的ABA问题
使用原子类的时间戳类,防止ABA问题
自旋锁,不断地去旋转执行,直到成功
死锁怎么排查?
- 使用 jps-l 定位进程号
- 使用 jstack 进程号 找到死锁问题
AQS
JUC下很多锁的 加锁和解锁操作都是来自于AQS下的组件
AQS内部维护了 一个state变量,用于上锁;还有加锁线程变量等,共同维护了一个锁的状态。
AQS内部还有一个等待队列,用于存放那些加锁失败的线程