JUC
Posted 小田mas
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC相关的知识,希望对你有一定的参考价值。
JUC
多线程的三种方法
1.继承Thread这个类,重写run方法。
2.实现Runnable接口,实现run方法。(Thread是Runnable的实现类)
(以上两种方法都 无法抛出异常 不拥有返回值)
3.实现Callable接口,实现call方法。(这个方法可对外抛出异常和拥有返回值)
java.util.concurrent
接口不可以new
匿名内部类
runnable函数式接口
JDK1.8 lambda表达式:(参数)->{代码}
synchronized 在 run 方法前面加
wait 和 sleep的区别
- 来自不同的类 wait obiect类 sleep Thread类
- 释放锁也有区别 wait会释放锁,sleep不会释放锁
- 两个都要捕获 异常,notify,notifyall不需要
- 使用的范围不同,wait只能在同步代码块中,sleep可以在任何地方
并发和并行
- 单核,多个线程同时操作一个资源
- 多核,多个人一起走,
线程和进程
- 进程资源的最小单位,一个程序
- 线程操作的最小单位,多个线程可以组成一个进程
Lock锁
import java.util.concurrent.locks.Lock;
公平锁与非公平锁:是否可以插队。
使用方法,用之前加锁,用完之后解锁。
将lock锁与synchronized的区别展示下方
Lock锁的步骤
- 先new ReentrantLock()
- 再Lock.lock();加锁
- 再在finally里面lock.unlock();
还是需要注意一个问题,往new的线程里面扔资源,再如之前一样实现多线程。
大哥说的比较简化的代码,就是把大括号去了,能往一行挤得就挤到一行。
Lock锁和synchronized的区别
- synchronized是一个内置的关键字,Lock是类
- synchronized是无法判断获取锁的状态,Lock可以判断是否获取到了锁
- synchronized可以自动释放锁,Lock必须要手动释放锁,如果不释放锁就会导致死锁。
- synchronized当一个线程用,另一个线程就会一直等待,Lock锁不会这样,Lock.tryLock().
- synchronized默认是可重入的,不可以中断的,非公平;Lock锁,可重入锁,可以判断锁,非公平(可以自己设置)。
- 适用范围 s适合少量的代码块重复问题,Lock锁适合大量的同步代码。
那 锁 是什么?如何判断锁的是谁?
还是先来了解一下
生产者消费者问题
一个口诀:判断等待,处理业务,进行通知
就是wait ,notify的操作
但是要注意,两个线程调用资源的时候不会出现问题,但是当线程个数更多,就需要等待放在while中,因为会出现虚假唤醒问题,不能再放在if中!
虚假唤醒:当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
因为if只会执行一次,执行完会接着向下执行if()外边的
而while不会,直到条件满足才会向下执行while()外边的,参考了什么是Java虚假唤醒及如何避免虚假唤醒?
写一个资源类
然后再由线程去操作资源类。
单例模式、排序算法、生产者消费者、
虚假唤醒,wait语句应该在循环之中。
Condition
左右等价
采用condition解决该问题。
所谓的精准就是指对于condition的await和signal精准实施。
起初给每一个方法定义属于其自己的condition,然后再利用标记字段实施特定的手段。
扔Runnable接口,现在也不用了,就是在大括号里面放上 资源的方法。
conditioni.await();
conditioni.signal();//注意i是自己之前定义的!!!
8锁现象
判断锁的是谁?知道什么是锁?
对象、class
其实就是关于锁的8个问题:
//实现相应的延迟
import java.util.concurrent.Timeunit;
Timeunit.SECONDS.sleep(1);//要补货异常
- synchronized锁的对象是方法的调用者,谁先拿到谁先执行。
需要有延迟,同步方法(synchronized)。
static 静态方法(类一加载就有了,是一个模板)的话就是 锁的是Class。
不管是几个对象,只要是一个类模板就有相应的顺序。
注意跟普通的方法进行区分。
出现了一个小问题:
一个是静态方法,一个是普通方法,
而这里锁住了同步代码块,锁不住另一个对象
两个不是一个锁。这里需要重要理解。
集合类不安全
list.foreach(system.out::println);//也没啥就是建议学习下打印方式
嗯,不安全,报错了
前两种解决方案
//两种解决方法
vector+collections.synchronizedList
第三种方法
CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrarList<>();
CopyOnWrite 写入时复制,COW 计算机程序设计领域的的一种优化策略。
多个线程调用的时候,读的时候固定的,写入的时候复制一份,copyof再set回去,避免覆盖造成数据问题。
读写分离。
CopyOnWrite比vector更好在:
只要有synchronzied方法,效率就相对较低。
set不安全
hashset的底层就是hashmap
Map不安全
三个重载方法,容量+影响因子
Map<String,String> map = new HashMap<>();
可以用concurrentHashMap
Callable
- 有返回值
- 可以抛出异常
- 方法不同,call方法,run方法
泛型的参数等于方法的返回值。
callable通过runnable去和Thread连接。
而Runnable的实现类跟callable是有关系的。
可以通过这个图进行理解。
futuretask中有一个带callable类型参数的构造器。
传说中的 适配类 就是futuretask。
获得返回参数就要看下图啦!
get方法可能会产生阻塞。
所以把他放到最后,或者采用异步通信。
最终只会出现一个调用的结果,因为结果会被缓存,效率更高。
细节:
- 有缓存
- 结果可能需要等待,存在阻塞。
常用的辅助类
CountDownLatch
减法计数器!
主要就是减一和归零向下操作:
countDownLatch.countDown();
countDownLatch.await();
每次有线程要用,就减一,到零await()就会激活。
CyclicBarrier
计数器加一。
插播插播:
lambda是不能拿到循环里面的i的!需要用到final
其实源码就是–count。
Semaphore并发里面用得很多!
友友发的弹幕
并发限流,控制最大的线程数!
读是read,写是write。
阻塞队列
把这个词分开来理解
阻塞+队列
阻塞队列的四种API
同步队列
线程池
创建线程就要使用线程池了!
线程池的执行,程序完成之后需要关闭结束线程池。
threadpool.shutdown();//放到finally里面。
带cached的遇墙则强型。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test {
public static void main(String[] args) {
ExecutorService threadpool = Executors.newCachedThreadPool();//遇墙则强型
//ExecutorService threadpool = Executors.newSingleThreadExecutor();//单个型
//ExecutorService threadpool = Executors.newFixedThreadPool(5);//单个型
for (int i = 0; i < 100; i++) {
final int tmep = i;
threadpool.execute(()-> {
System.out.println(Thread.currentThread().getName() + " ok");
}
);
}
}
}
单个源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
举个栗子!
四种拒绝策略
7大参数
嗯嗯,一个小总结:
池的最大大小应该如何设置:
- cpu密集型 看自己电脑是几核,最后就是几,去设备管理器里面去找,更好的方式就是通过代码去获取
Runtime,getRuntime().availableprocessors()
- IO密集型,判断程序中十分耗费IO的线程,在这个基础上要大于这个数,一般是两倍。
四大函数式接口
函数式接口:只有一个方法的接口。functionalinterface,简化编程模型,新版本的框架底层大量应用。
- Consumer .foreach
- Function
- Predicate
- Supplier
Function,传入参数T,返回参数R。
new一下,匿名内部类,没有名字的类!
把上面的源代码实例化一下!
只要是函数式接口,就可以采用lambada表达式简化。
断定式接口
消费型接口只输入不返回。
供给型接口只返回不输入。
传说的链式编程!
stream流做运算!
ForkJoin
分支合并,JDK1.7之后,主要就是并行执行任务,提高效率的。
特点:工作窃取。维护的都是双端队列。
早干完活了可以帮别人干。
跟线程池差不多,都是通过。。pool实现。
又来一条插播:
求中间值的时候建议 使用
start + (end-start)/2 可以使用这个式子来求解值。
求和的三种方式:(三种程序员)
stream的并行流计算:
注意:大数据量中使用,采用了分而治之的思想。
异步回调
volatile
volatile是java虚拟机提供的轻量级的同步机制。
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM,java内存模型,不存在的东西,概念,约定。
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立即刷回主存;拷贝的方式修改,完成之后再把值弄回去。
- 线程加锁前,必须读取主存中的最新值到工作内存中;
- 加锁和解锁是同一把锁。
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
(1)不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
(4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
(5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
很可能出现一个问题:
程序不知道主内存的值已经修改了!!!
结果:输出1,但是程序还是不会停下来,这个时候就要用到volatile!
保证可见性
不保证原子性
如果不加lock和sychronized,怎么保证原子性?
使用原子类,
避免指令重排
CAS
Java层面:
对于unsafe类的研究
对于加一的底层探究:
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那就执行操作,如果不是就一直循环。(do-while)cpu并发原语
缺点:
1.循环会耗时
2.一次只能保证一个共享变量的原子性
3.存在ABA问题
CAS:ABA问题
狸猫换太子
就是拿到的东西看着没有变,实际上是变了!诸如下图的情况发生。
其实就是乐观锁、悲观锁的问题。什么是乐观锁,什么是悲观锁,解决并发控制的问题。
那怎么解决这个问题呢?需要采用带版本号的原子操作!
这样!
给一个时间戳,相当于版本号。
自旋锁
我们来自定义一个锁测试:
死锁
定义:
以上是关于JUC的主要内容,如果未能解决你的问题,请参考以下文章