一、JUC概述
1. 什么是JUC
JUC是Java.util.concurrent包,在jdk1.5版本首次出现。
2.进程和线程
2.1.进程和线程的区别
- 进程:系统进行资源分配和调度的基本单位。当程序运行时,就是一个进程。(软件实际占用多少空间)
- 线程:系统能够进行运算调度的最小单元。进程的实际运作单位,程序执行的最小单位。一个进程可能会开启多个线程,如在qq中打开资料修改,聊天,这些功能都会开启一个或者多个线程。
2.2.线程的状态
- 线程的状态分五种。
- 分别为:NEW(新建),RUNNABLE(就绪),BLOCKED(阻塞),WAITING(等待),TIME_WAITING(定时等待),TERMINATED(终结)。
- 其中WAITING(等待),TIME_WAITING(定时等待)都为等待状态。
2.3.wait和sleep的异同
- 相同点:都能被interrupted打断。
- 不同点:
来源 | 占锁 | |
---|---|---|
wait | Object方法 | 会释放锁,前提是当前线程占用锁 |
sleep | Thread静态方法 | 不会释放锁,也不需要占用锁 |
2.4.并行和并发和串行
串行:多个操作单个执行。
并行:多个操作同时执行。
并发:同一个时刻,多个线程访问同一个资源。
2.5.锁
- 管程、Monitor、监视器、锁都代表锁。
- 监视器是一种同步机制,保证同一个时间,只有线程访问被监视器的代码或数据。
- JVM同步基于进入与退出,对管程对象的持有与释放来实现。当一个线程持有管程对象,其他线程不能获取持有这个管程对象。
2.6.用户线程与守护线程
用户线程:自定义线程。
守护线程:系统、后台线程。如gc。
代码测试:
class test{
public static void main(String[] args){
Thread aa = new Thread(()->{
System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().isDaemon);
while(true){
}
},"AA");
//aa.setDaemon(true);//设置守护线程,默认为false
aa.start();//
System.out.println(Thread.currentThread().getName()+"Over");
//输出结果1:main线程已经结束,程序依然运行,输出AA:flase。jvm仍然存活
//输出结果2:设置守护线程后,主线程结束,守护线程也结束。jvm结束。
}
}
运行过程问题:设置守护线程后仍然输出了AA,怀疑是系统调度的问题,使用sleep后解决。
二、Lock接口
1. synchronized关键字复习
synchronized:java关键字,同步锁。
修饰范围:代码块、方法、静态方法、类。
作用范围:锁住某个括号范围,锁住某个对象。
2.多线程固定编程步骤(上)
2.1.常见线程创建方法
主要是实现run()方法。(记住线程创建后调用start()方法启动)
Thread类继承:
//lamda表达式
new Thread(()->{});
Runnable接口:函数式接口,只有一个run()方法
new Thread(new Runnable(){
public void run(){
...
}
});
2.2.步骤
- 创建资源类,实现属性和操作方法。
- 创建多线程,调用资源类的方法。
卖票实例(sync实现)
//1.建立资源,实现属性和操作方法
class Tickets {
//票数
private int number = 30;
//操作方法(添加锁的方式)
public synchronized void sale() {
//判断是否有票
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ":买票成功" + number-- + "剩下:" + number);
try {
Thread.sleep(5);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//第二步:创建多个线程,调用资源类的操作方法
public class SaleTicket {
public static void main(String[] args) {
//创建Ticket对象
Tickets ticket = new Tickets();
//创建多线程来进行卖票
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
//调用卖票方式
ticket.sale();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}
},"CC").start();
}
}
2.3.lock接口
lock接口有三个实现类。ReentrantLock:可重入锁,ReentrantReadWriteLock.ReadLock:读锁,ReentrantReadWriteLock.WriteLock:写锁。
如何使用:
例:可重入锁
class xx{
private final ReentrantLock lock = new ReentrantLock();//创建锁对象
public void m(){
lock.lock();//上锁
try{
...//方法体
}finally{
lock.unlock();//解锁
}
}
}
可重入锁特点:可以多次,被上锁,解锁。(持有锁时不能被其他线程上锁)
卖票实例(lock接口实现)
class Tickets{
private int num = 30;
//创建锁对象
//操作方法
public void sale(){
//上锁
lock.lock();
try{
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ":买票成功" + number-- + "剩下:" + number);
Thread.sleep();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();//手动解锁
}
}
}
//2.创建多线程方法与上相同
2.4.lock接口与synchronized关键字的区别
- lock接口不是java语言内置,synchronized是java关键字
- lock是一个类,通过实现类实现同步访问。
- synchronized:当方法或代码块执行完毕后自动释放锁,而lock需要手动解锁。当遇到异常时,synchronized会自动解锁,而lock需要手动解锁,所以需要加到finally中。
- lock可以让等待锁的线程响应中断,而使用synchronized则需要一直等待下去,不能响应中断。
- 通过lock可以知道有没有成功获取到锁,synchronized不行
- lock可以提高多个线程进行读操作的效率
PS:调用start()方法时,是否创建线程由系统决定。
三、线程间的通信
1.多线程编程步骤
- 创建资源类(属性和操作方法)
- 操作方法:判断(根据条件判断是否进行操作)、干活(实际操作)、通知(唤醒)
- 在操作方法的判断中使用:while() 。原因:防止虚假唤醒
- 创建多个线程,调用资源类的操作方法
1.1.实例
实现两个线程对num修改,输出 0 1 0 1 0 1 …
//第一步创建资源类,定义属性与操作方法
class Share {
//属性
private int num = 0;
//操作方法:+1
public synchronized void incr() {
//第二步 判断 干活 通知
while (num != 0) {//判断是否等于0,等于0 就+1,不是0等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果number是0,就+1操作
num++;
System.out.println(Thread.currentThread().getName() + "::" + num);
//通知其他线程
this.notifyAll();
}
public synchronized void decr() {
//第二步 判断 干活 通知
while (num != 1) {//判断是否等于1,等于1进行-1 操作,不等于1等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果number是1,进行-1 操作
num--;
System.out.println(Thread.currentThread().getName() + "::" + num);
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
//第三步:创建多个线程,调用资源类的操作方法
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
share.incr();//+1操作
}
}, "aa").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
share.decr();//-1操作
}
}, "bb").start();
}
}
1.2.虚假唤醒问题
产生条件(原因):wait() 方法在哪儿睡,就在哪儿醒。所以,当线程被唤醒时,上一次已经执行了if()语句的判断,就跳出了这次的判断,直接执行+1/-1操作。
1.3.Condition的初次使用
Condition依赖与lock,可以看作为一个变量
1.3.1.创建Condition实例和方法
Condition condition = lock.newCondition();
condition.await();//被中断或接到信号前一直等待
condition.signal();//唤醒当前线程
condition.signalAll();//唤醒所有线程
实例1:使用Condition实现abcd四个线程的输出0 1 0 1操作
...
实例2:使用Condition实现:输出aa5次,输出bb10次,输出cc15次,共10轮。
实现方案:设置标志位flag
- flag=1 : aa
- flag=2 : bb
- flag=3 : cc
创建三个condition,使用signal()与wait()方法实现线程间的定制化通信。
//第一步 创建资源类
class ShareResource{
//定义标志位
private int flag = 1;//aa:1 bb:2 cc:3
//创建lock锁
private final Lock lock = new ReentrantLock();
//创建三个condition(三个线程)
private final Condition c1 = lock.newCondition();
private final Condition c2 = lock.newCondition();
private final Condition c3 = lock.newCondition();
//打印5次,参数第几轮 aa
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try{
//判断
while(flag!=1){
c1.await();
}
//干活
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"::"+i+"轮数"+loop);
}
//通知
flag = 2;//修改标志位
c2.signal();//通知bb线程
}finally{
//解锁
lock.unlock();
}
}
//打印10次,参数第几轮 bb
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();
try{
//判断
while(flag!=2){
c2.await();
}
//干活
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"::"+i+"轮数"+loop);
}
//通知
flag = 3;//修改标志位
c3.signal();//通知cc线程
}finally{
//解锁
lock.unlock();
}
}
//打印15次,参数第几轮 cc
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();
try{
//判断
while(flag!=3){
c3.await();
}
//干活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"::"+i+"轮数"+loop);
}
//通知
flag = 1;//修改标志位
c1.signal();//通知aa线程
}finally{
//解锁
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
//创建资源实例
ShareResource resource = new ShareResource();
//创建多线程总共10轮
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"aa").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"bb").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"cc").start();
}
}
五、集合的线程安全
1.List不安全的示例
如ArrayList
class test{
public static void main(String[] args){
List<String> list = new ArrayList<>();
//创建多个线程
for(i=0;i<10;i++){
new Thread(()->{
//添加
list.add(UUID.randomUUID().toString().subString(0,8));
//取出
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
//异常:ConcurrentModifyCationException 并发修改异常
2.List线程不安全解决方案
三种方案,Vector,Collections,CopyOnWriteArrayList类
2.1.Vector
- Vector是jdk1.0提出的古老方案,一般不用。
- 底层所有的方法都添加了Synchronized关键字,能保证并发修改不会出现异常,但是性能差,效率低。
- 创建方法:
List<String> list = new Vector<>();
2.2.Collections
- Collections工具w类提供同步方法,创建同步的list。效率低,也不用。
- 创建方法:
List<String> list = Collections.synchronized(new ArrayList<>());
2.3.CopyOnWriteArrayList
- 写时复制技术(支持并发读,独立写)
- add()方法:底层使用重入锁实现
- 当添加时:复制一个比原集合长度多1的集合,把新元素添加进去,最后把新集合当做要用的集合。
- private transient volatile Object[] array;
- 补: Volatile的可见性:被volatile修饰的成员变量被线程修改时,都强迫从主内存中重读该成员变量的值。而且,当成员变量发生改变时,强迫将变化值写回到主内存,这样在任何时刻,两个线程看到的为同一个值。
- 详情见
Volatile和Transient
JAVA并发编程: CAS和AQS
-创建方法:
List<String> list = new CopyOnWriteArrayList<>();
3.HashSet线程不安全解决方案
创建:
Set<String> set = new HashSet<>();
//同样修改出现了并发修改异常
解决方案:JUC提供的CopyOnWriteArraySet方法
4.HashMap不安全解决方案
JUC提供的ConcurrentHashMap<K,V>
底层:采用数组+链表+红黑树
保证线程安全:synchronized+CAS操作(不支持key为null或value为null)
六、锁
1.8种情况的锁
…省略
结果:讨论对锁的范围问题。
- 对于普通方法:当前实例
- 静态同步方法:当前的class
- 同步方法块:sync括号里配置的对象
2.公平锁和非公平锁
表现:在买票实例中,不对A进行sleep,A卖光了所有票,造成BC被饿死的情况。
使用:
final ReentrantLock lock = new ReentrantLock();
//true:公平锁,效率较低
//false:非公平锁(默认无参为非公平锁),线程效率高,但造成线程饿死
synchronized为非公平锁
3.可重入锁(递归锁)
synchronized(隐式)和lock(显式)都为可重入锁。
广义:可重复,可递归调用的锁。在外层使用锁之后,在内层仍然可以获取到锁,并且不发生死锁。
定义:支持一个线程对资源的重复加锁。
实现:
- 线程再次获取到锁:锁需要识别获取线程的锁是否为当前占据锁的线程。(判断)
- 锁的最终释放:线程重复n次获取锁,在n次释放锁后,其他线程能够获取。(设置计数器:获取锁计数自增,释放锁计数自减,为0表示成功释放。每次释放都要判断状态值是否为0,前(n-1)次释放应该都为0)
4.死锁
定义:两个或以上的线程在执行中,因为争夺资源而造成一种相互等待的现象,没有外力干涉将无法继续进行下去。
产生条件:互斥、请求保持、不可剥夺、循环等待
产生原因:系统资源不足、进程推进顺序不合适、资源分配不当
验证死锁:
(1)jps指令 类似 linux:ps -ef
(2)jstack jvm自带栈跟踪的工具
public class DeadLock{
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args){
new Thread(()->{
synchronized(a){
sout("aaa");
sout("want bbb");
synchronized(b){
sout("bbb");
}
}
}).start();
new Thread(()->{
synchronized(b){
sout("bbb");
sout("want aaa");
synchronized(a){
sout("aaa");
}
}
}).start();
}
}
七、Callable接口
- 创建多线程的方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池
1.Runnable与Callable的区别
接口方法:
Callable:V call();返回计算结果,无法抛出异常
Runnable:void run(); 无返回结果
2.使用Callable接口
- Runnable实现类下有:FutureTask
- FutureTask(Callable<> callable)
- FutureTask(Runnable, result)
- FutureTask方法
- V get():获取结果
- boolean isDone:如果完成,返回ture
2.1.FutureTask(未来任务)概述,原理
为某个任务单开启线程,先计算其他。最后汇总结果。
如:
a:1+2+3…+10 b:11+12+…+50 c:50+51+…+60
我们为b单开启一个线程,当需要的时候我们直接使用get获取结果就行。不需要等b重新进行计算。
实例:使用FutureTask<>创建线程
/**
* 比较runnable 与 Callable 接口
*/
class Thread1 implements Runnable{
@Override
public void run() {
}
}
class Thread2 implements Callable{
@Override
public Integer call() throws Exception {
return 200;
}
}
public class Demo1 {
public static void main(String[] args) {
// Thread1 thread1 = new Thread1();
//Runnable
new Thread(new Thread1(),"aa").start();
//Callable
FutureTask<Integer> futureTask1 = new FutureTask<>(new Thread2());
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
return 1024;
});
}
}
八、JUC的辅助类
三大辅助类:
- 减少计数(闭锁 ) :CountDownLatch
- 循环栅栏 : Cyclic Barrier
- 信号灯:Semaphore
1.CountDownLatch
方法:
- await() : 阻塞
- countDown() :计数器 -1。
- 当计数器值为0时,因await()方法阻塞的线程将会被执行
实例
CountDownLatch count = new CountDownLatch(6);//创建计数器对象,设置初始值
for(i=0;i<6;i++){
new Thread(()->{
sout(...getName);
count.countDown();//计数器减1
},String.valueOf(i)).start();
}
count.await();//计数器变为0前,将一直阻塞。记住try-catch
sout("计数器变为0,执行方法");
2.CyclicBarrier
定义:允许一组线程互相等待,到达某个点时不再等待。barrier可以在释放等待线程后重用。
构造方法:
- CyclicBarrier(int parties)
- CyclicBarrier(int parties,Runnable barrierAction)
- 最后一个参数为:达到parties后的行为
方法:await() :到达parties前进行等待。
实例
例子: 七龙珠召唤神龙
CyclicBarrier barrier = new(7,()->{
sout("召唤神龙");
});
for(i=0;i<50;i++){
new Thread(()->{
sout("收集~~");
barrier.await();//记住等待
});
}
3.Semaphore
信号灯:在获取许可前,线程一直被阻塞,除非线程被中断。
方法
- acquire() :获取许可
- acquire(int permits) :获取一定量的许可
- release():释放一个许可
构造方法
Semaphore(int permits)
Semaphore(int permits,boolean fair):公平设置
实例
停车模型,6车3车位
//创建信号量
Semaphore semaphore = new Semaphore(6);
for(int i = 0;i < 6;i++){
new Thread(()->{
try{
//得到许可
semaphore.acquire();
sout(getName()+"抢到了车位");
TimeUnit.SECONDS.sleep(new Rondom().nextInt(5));
sout(getName()+"离开了车位");
}catch(){}
finally{
//释放许可
semaphore.release();
}
},String.valueOf(i)).start();
}
九、JUC读写锁
1.乐观锁与悲观锁
悲观锁: 线程对操作的对象上锁,其他线程只能是阻塞或等待(不支持并发操作)(效率低)
乐观锁: 对修改操作进行版本更新。
修改:
- 先核对版本号是否与数据库一致
- 是,核对前后数据是否进行了更改
- 是,更新数据并更新版本
详情见mysql数据库相关知识。
2.读写锁
读锁:共享锁
写锁:排他锁,独占锁(有写操作都需要等待)
无锁:读写无锁,详见事务隔离。
PS:内部类访问外部数据只能访问常量:加final修饰。
锁降级 rwx
将写锁降级为读锁
jdk8:获取写锁->获取读锁->释放写锁->释放读锁
实现:
//创建读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//锁降级
writeLock.lock();
readLock.lock();
writeLock.unlock();
readLock.unlock();
十、阻塞队列
1.概述
一个共享队列,当队列被放满,或者取空,再次执行put或者take时,线程会被阻塞。
2.分类
1.ArrayBlockingQueue:基于数组实现的阻塞队列(定长)
2.LinkedBlockingQueue:基于链表实现的阻塞队列,大小默认为 integer.MAX_VALUE
3.DelayQueue:延迟队列。
- 只有当指定的延迟时间到了,才能从队列中获取到元素
- 没有大小限制
- 使用优先级队列实现的无界阻塞队列
- priorityBlockingQueue:支持优先级排队
- SynchronousQueue:不存储元素,队列有单个元素
- LinkedTransferQueue:由链表组成的双向阻塞队列
3.核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove(e) | poll | take() | poll(time,unit) |
检查 | element() | peek() | X | X |
十一、线程池
1.概述
线程池维护多个线程。避免短时间内线程频繁的创建和销毁。保证内核充分应用,防止过度调度。
优点:统一管理、减少资源消耗、提升响应速度。
2.使用
通过Executor框架实现,提供Executors工具类。
ExecutorService pool = Executors.newFixedThreadPool(5);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
pool.execute(()->{
...
});
pool.shutDown();
3.分类
一池N线程:Executors.newFixedThreadPool(int);
特点:
- 固定长度线程池
- 线程处于一定量,可以很好控制线程并发量
- 可以重复被使用,在显式关闭之前,将一直存在
- 超过一定量的线程被提交时,需要在队列中等待
一池一线程:Executors.newSingleThreadExecutor();
特点:
- 一次处理一个线程
- 多的等待
可扩容线程池:Executors.newCachedThreadPool();
特点:
- 可扩容,线程数量不断变化
前面三种提供的创建线程池的方法,一般都不使用,都有局限性,一般采用自定义创建线程池。
原因:
- FixedThreadPool和SingleThreadPool允许请求队列的长度为 Integer.MAX_VALUE,可能堆积大量请求,造成OOM。
- CachedThreadPool和SchduledThreadPool允许创建大量线程,造成OOM。
4.自定义线程池
以上提到的创建线程方法,实际都是自定义线程池:new ThreadPoolExecutor
4.1.自定义线程池参数
自定义线程的参数总共有7个参数。
ThreadPoolExecutor( int corePoolSize; //核心线程数量
int maximumPoolSize, //最大线程数
long keepAliveTime, //线程存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadfactory, //线程工厂
RejectedExecutionHandler handler //拒绝策略
)
4.2.底层工作流程
4.3.四种拒绝策略
AbortPolicy(默认):直接抛出异常阻止系统正常运行。
CallerRunsPolicy:“调用者运行”,不抛弃任务也不抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交任务。
DiscardPolicy:默默丢弃无法处理的任务,不予任务任何处理也不抛出异常,如果允许任务丢失,这是最好的策略。
十二、Fork/Join分支合并框架
Fork/join框架可以将一个大的任务拆分为多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果进行输出。
1.使用
创建ForkJoinPool,调用ForkJoinTask<>方法,继承RecursiveTask<>
class Fibo extends RecursiveTask<Integer>{
final int n;
Fibo(int n){
this.n = n;
}
Integer compute(){
if(n<1){
return n;
}
Fibo f1 = new Fibo(n-1);
f1.fork();
Fibo f2 = new Fibo(n-2);
return f2.compute()+f1.join();
}
}
例如:从1+2+…+100
使用二分查找思想。相加的两个数不超过10,超过10拆分。
class Mytask extends Recursive<Integer>{
private static final VALUE = 10;
private int begin;
private int end;
private result;
public MyTask(int begin,int end){
this.begin = begin;
this.end = end;
}
protected Integer compute(){
if((end-begin)<=VALUE){
for(int i=begin;i<=end;i++){
result+=i;
}
}else{
int middle = (begin+end)/2;
Mytask task1 = new Mytask(begin, middle);
Mytask task2 = new Mytask(middle+1,end);
task1.fork();
task2.fork();
result=task1.join()+task2.join();
return result;
}
return result;
}
}
public class test{
public static void main(String[] args){
Mytask task = new Mytask(1,100);
//创建分支合并池
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> ForkJoin = ForkJoinPool.submit(task);
//获取结果
Integer result = forkJoin.get();
//关闭
pool.shutDown();
}
}
十三、异步回调(CompletableFuture)
//无返回值
CompletableFuture<void> future1 = CompletableFuture.runAsync(()->{...});
future1.get();
//有返回值
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(()->{
sout(...);
return 1024;
});
future2.whenComplete((t,u)->{
sout(t+u);// t为返回值,u为异常
}).get();