java.util工具包,用处 ---->在业务中,普通线程代码 Thread 效率不高
Runnable 没有返回值,效率相对于Callable较低
业务中的写法---->降低耦合性 // 将资源和对资源的实现分开
进程是一个程序,线程是程序中的任务
进程:一个程序,如 QQ.exe 一个软件(程序的集合)
一个进程往往可以包含多个线程,至少一个
java中默认有两个线程 main、GC
例:运行Typora(进程),写完字后自动保存(线程)
java实际上是开不了线程的,只能通过本地方法去调用 -----> 例: private native void start()
// 本地方法,去调用底层的c++,java无法直接操作硬件
并发变成:并行,并发
并发(多个线程操作同一个资源)
并行(多个人一起行走)
CPU多核,多个线程可以同时执行
提高性能 —>利用线程池
//获得CPU核数
//CPU密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
并发编程的本质:充分利用cpu的资源
1、来自不同的类
sleep —>object wait ----> Thread
2、关于锁的释放
wait会释放锁,sleep抱着锁睡觉不释放
3、使用范围
wait 必须在同步代码块
sleep可以在任何地方睡
JUC里的睡眠方法----->TimeUnit.SECONDS.sleep( )
本质:排队,分配锁
interface Lock
可实现类: ReentrantLock (可重入锁—常用),ReetrantReadWriteLock.ReadLock (读锁) /WriteLock(写锁)
公平锁:公平,可以先来后到 -----> NofairSync()
非公平锁:不公平,可插队(java中默认,效率高) -----> FairSync()
Lock使用时三步骤:
1、传统的Synchronized锁会自动释放锁;LOCK锁是手动的,如果不释放,会产生死锁
2、Synchronized是内置的java关键字;Lock是一个java类
3、Synchronized无法判断获取锁的状态;Lock锁可以判断是否获取到了锁
4、Synchronized 线程1(获得锁、阻塞…),线程2(傻傻地等) ;Lock 锁不一定会等待下去,有trylock()方法会尝试获取锁
5、Synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平(可以自己设置)
6、Synchronized适合锁少量的代码同步问题;Lock适合锁大量的同步代码
synchronized () {
if(condition)
this.wait(timeout);
... // Perform action appropriate to condition
this.notifyAll();
}
线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。 换句话说,等待应该总是出现在循环中,就像这样:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
如果当前线程interrupted任何线程之前或在等待时,那么InterruptedException
被抛出。 如上所述,在该对象的锁定状态已恢复之前,不会抛出此异常。
解决方法:用while判断而不是if
用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后。
Condition
因素出Object
监视器方法( wait
, notify
和notifyAll
)成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock
个实现。 Lock
替换synchronized
方法和语句的使用, Cndition
取代了对象监视器方法的使用。
Lock lock = new Reentrantlock();
Condition condition = lock.newCondition;
condition.await( ); //等待
condition.signalAll( ) ; //唤醒全部
业务代码(判断 ->执行 ->通知)前要上锁,完成后释放,锁在业务代码前(try…catch前)获得
Condition实现精确通知唤醒
condition.signal( )唤醒指定的condition监视器
8锁就是关于锁的8个问题:
1、两个同步方法,一个对象调用。
synchronized 锁的是方法的调用者,也就是对象锁。两个方法持有的是同一把锁,因此谁先拿到锁谁先执行。
2、两个同步方法,两个对象调用。
synchronized 锁的是方法的调用者,也就是对象锁。两个对象分别调用两个方法持有的是两把把锁,一个的执行不需要等待另一个
3、一个同步方法,一个普通方法,一个对象调用
普通方法没有锁,不需要竞争锁。
4、两个同步方法,一个对象调用,一个方法执行会先睡眠但在main方法中执行顺序在前
synchronized 锁的是方法的调用者,也就是对象锁。两个方法持有的是同一把锁,因此谁先拿到锁谁先执行,此种情况下,执行顺序在前的会先拿到锁,与执行时是否睡眠无关。
5、两个静态同步方法,一个对象调用
static方法类一加载就执行,synchronized 锁的是Class对象即类锁,所以两个方法持有一把锁,谁先得到谁先执行。
6、 两个静态同步方法,两个对象调用
static方法类一加载就执行,synchronized 锁的是Class对象即类锁,所以两个方法持有一把锁,谁先得到谁先执行。
7、一个静态同步方法,一个普通同步方法,一个对象调用!!!
静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。
8、一个静态同步方法,一个普通同步方法,两个对象调用
静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。
小结:
普通方法 没有锁,不用等待获得锁再执行
普通同步方法 锁的是方法的调用者
static同步方法 锁的是唯一的一个class模板
并发下ArrayList 不安全,可能会引发ConcurrentModification 并发修改异常
解决方案:
1、用Vector代替 vector是什么 实际上就是用synchronized解决,Vector方法 synchronized修饰的
2、用Collections集合类,转化为synchronized方法 (工具类写法)
List <string> list = Collctions.synchronizedlist(new Arraylist<>())
3、调用CopyOnWriteArrayList类(JUC写法)
List <string> list = new CopyOnWriteArraylist<>()
CopyOnWrite(写入时复制):
简称COW思想,是计算机程序设计领域的一种优化策略
多个线程调用的时候,如果资源是唯一的,读取的时候是固定的,写入的时候可能会出现覆盖情况 ---------->使用 CopyOnWrite避免覆盖,造成数据问题
CopyOnWrite 相比于 Vector的优势:
并发下也可能抛出并发修复异常
原码: Set set = new HashSet<>()
解决方案:
Set<String> set = Collctions.synchronizedlist(new HeshSet<>())
2、JUC写法
Set <String> set = new CopyOnWriteArraySet<>()
//HashSet底层就是一个HashMap
public HashSet(){
map = new HashMap<>();
}
//HashSet的add方法,本质就是 map key是无法重复的,PRESENT是个常量
public boolean add(E e){
return map.put(e,PRESENT)
}
Map在高并发中也不安全
默认等价于 new HashMap<>(16,0.75) // 默认加载因子0.75 、初始化容量16 (底层是位运算)
原码:Map
解决方案:
1、Collections写法
Map<String,String> map = Collctions.synchronizedMap(new HashMap<>())
2、JUC写法
Map<String,String> map = new ConcurrentHashMap<>()
类似于Runnable,都是为线程执行的一种接口
不同点:
Callable无法直接调用Thread类,需要通过Runnable,所以通过其一个适配类来启动
class Callable{
main{
FutureTask ft = new FutureTask(thread); //适配类
new Thread (ft,name"A").start();
new Thread (ft,name"B").start();
//-------> 结果只会返回一个123
String s =(String)futureTask.get() //获取callable 返回结果
}
}
class MyThread implement Callable< String>{
public String call()
{
return "123";
}
}
注意:
解决方法:get( )获取方法代码最后或者通过异步通信
// 对于缓存的解释: JVM第二次调用ft这个对象所持有的线程时,futuretask的线程状态已非new状态,则会直接结束对应线程,导致任务不执行,只是在第一次调用时返回结果保存了
原理:
public CountDownLatch(int count) //count代表记时总数
public void countDown() //数量减一
public void await()
如果当前计数为零,则此方法立即返回。
如果当前计数大于零,则当前线程将被禁用以进行线程调度,并处于休眠状态,直至发生两件事情之一:
public CyclicBarrier(int parties,Runnable barrierAction)
参数
parties
- 屏障跳闸前必须调用 await()
的线程数
barrierAction
- 当屏障跳闸时执行的命令,或 null
如果没有动作
异常
IllegalArgumentException
- 如果 parties
小于1
public int await(long timeout,TimeUnit unit) //timeout - 等待屏障的时间
unit - 超时参数的时间单位
如果当前线程不是最后一个线程,那么它被禁用以进行线程调度,并且处于休眠状态(当前线程阻塞,不执行),直到发生下列事情之一:
reset()
public Semaphore(int permits) //permits 规定指定数量的线程
public void acquire()
获得许可证,如果有可用并立即返回,则将可用许可证数量减少一个。
如果没有可用的许可证,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,直至发生两件事情之一:
public void release() //释放许可证,将其返回到信号量
作用:多个共享资源互斥的使用 , 并发限流 , 控制最大线程数
小结:
读写锁可以更加细粒度的控制 ----> 读的时候可以被多个线程读,写的时候只能有一个线程去写
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock lock = new Lock;
readWriteLock.write/readlock().lock/unlock(); //给读/写锁 ,上/开锁
writeLock() //用于书写的锁
readLock() //用于阅读的锁
独占锁(写锁):一次只能被一个线程占有
共享锁 (读锁):多个线程可同时占有
阻塞队列的应用:多线程并发处理,线程池
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞,等待 | 超时,等待 |
---|---|---|---|---|
添加 | add( ) | offer( ) | put( ) | offer(元素 ,等待时间,等待单位) |
移除 | remove( ) | poll( ) | take( ) | poll( 等待时间,等待单位) |
检测队首元素 | element( ) | peek( ) | _ | _ |
1、抛出异常
2、不抛出异常
3、阻塞等待
4、超时等待
没有容量,不储存元素
进去一个元素,必须等待取出来之后,才能往里面放一个元素 (put了一个元素,必须从里面先take取出来,否则不能再put进去值)
掌握:三大方法,七大参数,四种拒绝策略
好处:
1、降低资源消耗
2、提高响应速度
3、方便管理
线程复用、可以控制最大并发数、管理线程
ExecutorService threadpool = Executors.newSingleThreadExecutor(); //创建单个线程
ExecutorService threadpool = Executors.newFixedThreadPool(int num); //创建一个固定大小的线程,执行永远只有num个
ExecutorService threadpool = Executors.newCachedThreadPool(); //可伸缩的,大小可规定,不过是否能达到规定个数还要看电脑内存
try {
//使用线程池来创建线程
threadpool.execute( ()->{
System.out.println(Thread.currentThread().getName());
} );
} catch (Exception e) {
e.printStackTrace();
} finally {
threadpool.shutdown(); //线程池用完,程序结束,关闭线程池
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d58fCJ8L-1628042380371)(/home/lhx/图片/8.png)]
三大方法实质:
public ThreadPoolExecutor(
int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handler //拒绝策略
)
//自定义线程举例:
ExecutorService threadpool = new ThreadPoolExecutor
(
corePoolSize 2,
maximumPoolSize 5,
keepAliveTime 3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 最大承载 = 阻塞队列大小 + 最大核心线程池大小
AbortPolicy() //银行满了,还有人进来,不处理这个人,抛出异常
CallerRunsPolicy() //哪里来的去哪里,例:本来应该用main线程处理,就返回到main线程
DiscardOldestPolicy() //队列慢了,丢掉任务,不抛出异常
DiscardPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常
拓展:
最大线程如何定义(调优)
1、CPU密集型 CPU是几核就是几,可以保持CPU效率最高
System.out.println(Runtime.getRuntime().availableProcessors()); //获取CPU内核数
2、IO密集型
判断你程序中十分耗IO的线程,大于其数量
新时代程序员需掌握:lambda表达式,链式编程,函数式接口,stream流式计算
函数式接口:只有一个方法的接口 ----> @FunctionalInterface
Runnable接口
public interface Runnable{
public abstract void run ( ) ;
}
// 超级多的Functional Interface
// 简化编程模型,在新版本的框架底层 大量应用
// foreach(消费者类的函数式接口)
四大函数式接口: Function ,Consumer , Predicate , Supplier
函数式接口要和Lambda表达式结合起来
public interface Function<T, R> { //传入参数类型为T,返回参数类型为R
R apply(T t); ----> // 需要重写的方法
public static void main(String[] args) {
Function<String,String> function = new Function<String,String>() {
@Override
public String apply(String str) {
return "OK";
}
};
System.out.println(function.apply("ok"));
// lambda 写法
Function<String,String> function1 = (str) -> { return str ; };
System.out.println(function1.apply("ok"));
}
public interface Predicate<T> { //传入参数类型为T
boolean test(T t); //重写的返回参数类型为布尔型 ----> 可用于判断字符串是否为空
public static void main(String[] args) {
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty(); //判断字符串是否为空
}
};
// lambda 写法
Predicate<String> predicate1 = (str) ->{ return true ; };
System.out.println(predicate1.test("OK"));
}
public interface Consumer<T> { //只有输入,没有返回值
void accept(T t);
public static void main(String[] args) {
Consumer<String> consumer =new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("OK");
Consumer <String> consumer1 =(s) ->{
System.out.println(s);
};
consumer1.accept("ok");
}
public interface Supplier<T> { //只有返回值,没有输入
T get();
public static void main(String[] args) {
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "OK";
}
};
System.out.println(supplier.get());
Supplier<String> supplier1 = () -> { return "ok"; };
System.out.println(supplier.get());
}
大数据:储存+计算
集合、MySQL本质就是储存东西的
计算都应该交给流来操作
例:
// list.stream.方法 ----> 链式编程
**// 双引号::是一种新的语法,被称为“方法引用” **
- **Lambda写法: s -> system.out .println (s ) **
- 方法引用写法:system.out ::println
forkjoin类似大数据中的 Map Reduce (把大任务拆分为小任务) -------并行执行任务,提高效率,适用于大数据量
工作特点:
工作窃取 -------> 假设有两个线程A、B,如果A已经完成了自己的任务,B还没有完成,这时A会窃取B的未完成的任务去完成,从而提高工作效率
原因: 这个里买维护的都是双端队列 (两边都可以操作)
如何使用ForkJoin:
1 、通过ForkJoinPool执行
2、计算任务ForkJoinPool.execute(ForkJoinPool task)
3、计算类需要继承ForkJoinTask
例: 计算1到一亿的和(使用ForkJoin)
三种方式的比较: 简单的For循环 < ForkJoin < Stream
举例:
异步调用 --> 你喊朋友去吃饭,朋友在忙,说待会忙完去找你,你就先去做别的,朋友忙完了找你,你们一起去
同步调用 --> 你喊朋友去吃饭,朋友在忙,你就一直在等,等朋友忙完了,你们一起去
回归到线程上,就是一个任务发起请求后,不会占用程序的时间,但最终会加载回结果,降低了时间成本
与同步处理相比,异步处理不用阻塞当前线程来等待处理完成
在生活中常用于抢票、支付等
异步调用:CompletableFuture(类似与ajax)
Future的设计初衷:对将来某个事件的结果进行建模,但里面有延时,所以一般使用异步回调用CompletableFuture
回调都是函数式接口,可以结合lambda
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
//返回与此阶段相同结果或异常的新CompletionStage,当此阶段完成时,将使用结果(或 null如果没有))和此阶段的异常(或 null如果没有))执行给定操作。
action - 要执行的动作
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
//返回一个新的CompletableFuture,当CompletableFuture完成时完成,结果是异常触发此CompletableFuture的完成特殊功能的给定功能; 否则,如果此CompletableFuture正常完成,则返回的CompletableFuture也会以相同的值正常完成。
fn - 用于计算返回的CompletableFuture的值的函数,如果此CompletableFuture异常完成
JMM:java内存模型,不存在的东西。是个概念,约定。
关于JMM的一些同步约定:
1、线程解锁前,必须把自己的工作内存(自己的共享变量)立刻刷回主存
2、线程加锁前,必须读取主存中的最新值到工作内存中
3、加锁和解锁是同一把锁
存在问题:线程B修改了值,但是线程A不能及时可见 --------> 通过volatile解决
例A:
private static int num =0; //定义了静态变量,线程1开始会一直运行
public static void main(String[] args) {
new Thread( //线程1
() ->{
while (num == 0){}
}).start();
num=1; //按道理说主线程执行到这,num=1后,线程1会停止,但却没有,因为线程1不知道主内存中num值的改变
}
}
八种内存交互操作:
操作规则:
不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回 主内存。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存 中。
一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值。
如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
Volatile是java虚拟机提供的轻量级的同步机制
特点:
1、保证可见性
在例A中,由于num值的改变线程A无法获得的问题解决:
private volatile static int num =0 ; //通过volatile使num变为可见性的变量
public class Test01 {
private static int num =0;
private static void add(){
num++;
};
public static void main(String[] args) {
for (int i = 0; i <20 ;i++) {
new Thread( () ->{
for (int i = 0; i <20 ;i++) { add(); } //理论上应该num最后结果为400,但却因为 num++ 并非原子性操作,所以运行时20个线程不会“排队”,得到的结果每次都不同
}).start();
}
Lock锁和Synchronized关键字会保证原子性,可以解决这个问题
另外可以通过原子类Atomic解决
原子类为什么这么高级 -----> 方法的底层都是调用的C,直接与操作系统挂钩,在内存中修改值
指令重排:你写的程序,计算机并不是按照你写的那样去执行
源代码 —> 编译器优化的重排 —> 指令并行也可能重排 ----->内存系统也会重排 -----> 执行
==处理器在进行指令重排的时候,考虑:数据之间的依赖性
例1:
int x = 1 ; //1
int y = 2; //2
x = x + 5; //3
y = x * x; //4
我们所期望的 : 1234执行 但可能执行的时候变回 2134 1324
但不可能是 4123
例2:
a , b , x , y 默认值为0;
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
正常的结果 : x=0 ;y=0
由于指令重排导致的问题:
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
导致结果 : x=2;y=2
通过Volatile避免指令重排:
内存屏障(常用于单例模式) —>CPU指令
作用:
1、保证特定的操作的执行顺序
2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)
未启用就会占用所有的资源,浪费内存空间
利用反射对枚举尝试破坏:
如何破坏:
总结:
单例不安全,反射会破坏单例,但反射不能破坏枚举
在命令行,学会用 ==Javap - p + class 文件名 ----------> 反编译得到 反编译源码
CAS:CompareAndSet ------> 比较并交换
Unsafe类
底层的实现 --> 自选锁的使用
小结:
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行操作,如果不是就一直循环
缺点:
1、循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题:会造成资源提前挪用
ABA举例:你和你的女朋友分手了,过了一段时间复合了,他还是你的女朋友,但你并不知道她有没有交过其他男朋友(你以为数据没改过,实际是改过以后的)
解决思想:乐观锁,CAS就是乐观锁的一种,还有一种就是版本号
**原子引用: **就是带版本号的原子操作 // AtomicReference <> ---------> 实现类: AtomicStampReference
注意: **如果泛型是个包装类(Integer…),注意对象引用问题 **
正常业务中,这里面比较的一般是一个个对象,比较他们是否相等
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(期望值,版本号stamp); // 期望值要在上述范围内
int stamp = atomicStampedReference.getStamp(); //获得版本号
atomicStampedReference.compareAndSet(1,1,stamp,stamp+1);
public boolean compareAndSet(V expectedReference, //期望值
V newReference, //新值
int expectedStamp, //版本号
int newStamp) //新的版本号
公平锁:非常公平,不能插队,必须先来后
非公平锁:不公平,可以插队 (默认的,防止一个耗时长的长时间占用资源)
可重入锁:递归锁,所有的锁都是可重入锁
Synchronized版
Lock版
注意LOCK的配对问题:,lock( )和unlock( )必须数量同时对应,否则会死锁
自旋锁:不断的去循环尝试,直到成功为止
自定义自旋锁测试:
结果 :
T1 lock
T2 lock
T1 unlock
T2 unlock
在idea终端,利用jsp命令:
1、使用 jsp -L定位进程号
2、使用jstack +进程号找到死锁问题