欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 高考 > 【进阶】Java并发编程超详讲解!!!

【进阶】Java并发编程超详讲解!!!

2025/2/25 12:41:16 来源:https://blog.csdn.net/2301_78168249/article/details/145639419  浏览:    关键词:【进阶】Java并发编程超详讲解!!!

线程复习

什么是线程?

进程是操作系统分配资源的最小单位

线程是cpu执行的最小单元

线程是进程中一个独立的任务,一个进程中可以有多个线程

java创建线程方式有几种?

目前讲了三种

继承 Thread类

实现 Runnable 接口

实现 Callable 接口

线程中常用的方法

run() 线程要执行的任务写在run()

start() 启动线程

sleep() 线程休眠指定的时间

join() 等待线程结束

yiled() 线程让步 主动让出cpu执行权

线程状态

new Thread() 新建状态

start() 就绪状态

获得到了cpu执行权 运行状态

sleep wait 输入 等待获取锁 阻塞状态

任务执行完了、出现异常 死亡/销毁

多线程

一个线程中,可以创建多个线程,执行多个任务,提高程序运行效率

写一个多线程程序,去分解读取一个较大的文件

多个线程 访问同一个共享数据. --线程安全问题

解决线程安全问题:

加锁

synchronized

修饰代码块

synchronized(同步对象){ }

修饰方法

静态方法 同步对象是类的Class对象

非静态方法 同步对象是this

ReentrantLock 类

synchronized 是关键字 实现就靠底层指令实现 既可以修饰代码块,也可以修饰方法 隐式加锁和释放锁

ReentrantLock 是类 靠java代码控制

只能修饰代码块 手动加锁 和释放锁

线程通信

多个线程,在同步的情况下,互相牵制执行

wait() 线程等待

notify() 唤醒等待的线程(优先级高的)

notifyAll() 唤醒所有等待的线程

wait()和sleep()区别

wait()是Object类中的方法 等待后需要别的线程唤醒 等待后可以释放锁

sleep()是Thread类中的方法 休眠时间到了以后,可以自动唤醒 不会释放锁

Java并发编程

何为并发编程?

前提就是多线程场景

多线程优点:在一个进程中,可以有多个线程,同时执行不同的任务,提高程序响应的速度,提高了cpu的利用率,同时压榨硬件的剩余价值。

多线程也是有问题的:多个线程同时访问共享数据

卖票,抢购,秒杀…… 用户同时向后端发请求

好多请求同时访问数据会出现问题 并发编程,就是要让这些同时到来的请求,并发的执行(在一个时间段内,一个一个依次执行)。

并发执行:既有同时的意思,但是在计算机领域中,又有依次交替执行的意思

并行执行:在一个时间节点上,真正同时执行

线程的三个主要问题

1、不可见性

JMM java 内存模型

由于java内存模型 分为主内存和工作内存(缓存区),这一设计思想也是来源于cpu高速缓存的。

所有的变量都存储在主内存中,当线程要对主内存中的数据操作时,首先要将主内存中的数据加载到线程的工作内存中,才能进行操作,这样就会产生不可见性,两个线程同时操作一个数据时,其中一个线程在自己的工作内存中修改了数据,而另一个线程是不知道的。

2、乱序性

为了进一步优化,在cpu执行指令时,有的指令需要等待数据的加载,此时会先将后面的某些指令提前执行,这种情况就会出现乱序性,(乱序执行时,也是有基本的原则的,两条直接有关系的语句不能打乱执行的)

int a =5; int b =10; int c =a+b; 最后int c = a+b;不能放在a,b声明前

3、非原子性

线程切换执行,会带来非原子性问题,

cpu的执行在指令层面时原子性,但是高级语言一条语句,往往需要被编译程多条指令执行

这样多线程场景下,切换执行时,也会造成指令的执行不是原子性的

总结:

java内存模型的缓存区,导致了不可见性

编译器优化导致了乱序性

线程的切换执行导致了指令执行的非原子性

volatile关键字

volatile关键字修饰的变量,有两个作用

1.两个线程同时执行,一个线程修改了变量值,另一个线程是立即可见的

2.修饰的变量,编译器是不能对其进行重新排序的

volatile关键字 就可以解决不可见性和乱序性两个问题

但是不能解决非原子性问题

非原子性 只能通过加锁的方式解决

synchronized 和 ReentrantLock

原子类

java中的++操作,在多线程情况下,是非原子性的,想要让其实现原子操作,必须要对其进行加锁。

java中对于++操作,除了加锁,还提供另外一种实现原子操作的方案。

使用java中提供的一些原子类来实现

CAS

CAS(Compare-And-Swap):比较并交换

是一种无锁实现(不加锁),

是如何不加锁,在多线程实现变量的原子性?

采用自旋思想实现

AtomicInteger

incrementAndGet() 不加锁实现++操作

采用CAS思想(不加锁,自旋)

当线程A第一次操作时,先从主内存将数据加载到工作内存,可以把这次拿到的值称为预期值,

在工作内存中对其进行改变,改变后,将新值写回主内存时,再次比较主内存中的值,与拿到的预期值比较,

如果第一次拿到的值与最新的主内存中的值相同,说明没有其他线程修改,直接将线程A更新后的值写回主内存,

如果第一次拿到的值与最新的主内存中的值不相同,说明已经有其他线程修改过了,

那么线程A的值就作废了,需要重新从主内存中读取值,再次重复之前的操作

这种做法适合用于线程数量较少的情况,由于不加锁,线程都不会阻塞,所有线程一直尝试对变量进行修改操作,效率高于加锁

但是线程数量比较多,所有线程一直自旋,尝试操作,会导致cpu工作量很大

还有可能出现ABA问题:

可以使用带版本号的原子类

  
AtomicStampedReference stampedReference = new AtomicStampedReference(100, 0);new Thread(() -> {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);stampedReference.compareAndSet(101,100, stampedReference.getStamp(),stampedReference.getStamp() + 1);//2}).start();
new Thread(() -> {int stamp = stampedReference.getStamp();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}boolean result = stampedReference.compareAndSet(100, 101, stamp, stamp + 1);System.out.println(String.format("  >>> 修改 stampedReference :: %s ", result));}).start();
}

0

0->1 1->0

java中的锁分类

乐观锁/悲观锁

悲观锁:认为在多线程操作时,不加锁是会出现问题的,是一种加锁的实现,

synchronized 和 ReentrantLock 都属于锁,称为悲观锁

乐观锁:认为在多线程操作时,不加锁也是不会出现问题的,例如原子类,采用不加锁,自旋的方式实现。

称为乐观锁

可重入锁

可重入锁,又名递归锁,当一个线程进入到外层方法获得锁时,仍然可以进入到内层方法,而且内层方法和外层方法使用的是同一把锁。

如果不可重入,就会导致进入不了内存方法,导致死锁

读写锁

ReentrantReadWriteLock

实现读写锁

有读锁 也有写锁

读读不互斥 多个线程都是进入读锁,此时没有现成进入写锁 ,那么就是多个读线程同时进入到锁区域

一旦有写操作进行,那么读操作就不能进入

读写互斥 写写互斥

共享锁/独占锁

读写锁中的 读锁就是共享锁 ,在没有写的情况下,可以有多个读线程进入到读锁代码块

读写锁中的写锁 synchronized 和 ReentrantLock 都属于独占锁,一次只允许一个线程进入到锁代码块

分段锁

分段锁也不是一种实际的锁,是一种实现思想,将锁的粒度细化,提高效率

例如 ConcurrentHashMap的视线,

由于ConcurrentHashMap底层哈希表有16个空间,可以用每一个位置上第一个节点当做锁,这样可以同时由不同的线程操作不同的位置,只是同一个位置多个线程不能同时操作

自旋锁

自旋锁也不是实际的锁,是通过不断自旋重试的方式进行操作的,在低并发的场景下效率较高

公平锁/非公平锁

非公平锁:不分先来后到,谁先抢到,谁先执行 synchronized 就是非公平锁

公平锁:可以做到按照请求顺序分配锁,可以进行排队,ReentrantLock底层有两种方式实现 ,默认是非公平的,也可以通过AQS队列实现公平(排队)

偏向锁/轻量级锁/重量级锁

synchronized锁实现时,在同步对象中可以记录锁的状态:

无锁状态:没有任何线程获取锁

偏向锁状态:只有一个线程一直类获取锁,此时会在对象头中记录线程id,id相同可以直接获取锁,锁状态为偏向锁

轻量级锁状态:当锁状态为偏向锁时。继续有其他线程过来获取锁,锁状态升级为轻量级锁,

线程不会进入到阻塞状态,一直自旋获得锁。

重量级锁状态:当锁状态为轻量级锁时,线程数量持续增多,且县城自旋次数到一定数量时,锁状态升级

为重量级锁,线程会进入到阻塞状态,等待操作系统调度执行。

不同的锁状态也是java对synchronized锁进行的优化。

对象头 利用对象头记录有无线程进入到同步代码块 记录状态

synchronized 锁实现

synchronized锁实现是隐式的,可以修饰方法,也可以修饰代码块

底层实现是需要依赖字节码指令的,

修饰方式时,会在方法上添加一个ACC_SYNCHRONIZED标志,依赖底层的监视器实现锁的控制,有程序进入,计数器加一,线程执行结束,计数器减一

修饰代码块时,为同步代码块添加monitorenter指令,进行监视,执行结束,会执行monitorexit指令。

有线程进入,计数器加一,线程执行结束,计数器减一

AQS

AQS 的全称为( AbstractQueuedSynchronizer ) , 这个类在java.util.concurrent.locks包下面。

AbstractQueuedSynchronizer 抽象同步队列,

是并发编程中许多实现类的基础,例如ReentrantLock底层就是用到了AQS。

//AbstractQueuedSynchronizer   
AQS类中维护了一个状态  private volatile int state;
​
还维护了一个内部类 static final class Node{ volatile Node prev;volatile Node next;volatile Thread thread;//存储等待的线程
}
​
private transient volatile Node head;
​
private transient volatile Node tail;
​
维护了一个双向链表

ReentrantLock实现

类结构

ReentrantLock底层实现有两种方式:

1.默认的 是非公平实现

2.公平实现

//无参构造方法public ReentrantLock() {sync = new NonfairSync();//非公平锁实现}
​/*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

正常获得锁的方法

重点!!!

public class ReentrantLockDemo{ReentrantLock lock = new ReenTrantLock(true);public void test(){/*调用lock()方法后,该线程先是尝试去获取一把锁,如果没获得到,底层就通过addWaiter(Node.EXCLUSIVE)把自己加入到队列里面去,在队列里面是一个acquireQueued()死循环,一直判断前一个节点是否为头结点,直到前一个节点是头结点,底层会调用tryAcquire()又再次去抢锁是有公平锁和非公平锁两个方法实现的(NofairSync和FairSync)非公平锁一开始会先抢一次锁,也就是插队,根据判断预期的状态码和当前状态码是否一致,如果一致,则可以插队并把自己set进去,否则,掉入acquire()走正常获得锁的方法。*/lock.lock();//也是一直自旋的System.out.println("同步代码块");lock.unlock();}}


JUC

HashMap

你能讲一下HashhMap吗?

双列集合 实现Map接口 键值对存储

键是不能重复的 值可以重复,

只能存储一个为Null的键,

键是无序的,是线程不安全的

键是如何判断是否重复

hashCode() 根据内容是否重复计算出一个hash值,比较整数hash值较为方便 (有可能内容不一样但是值相同,所以不安全)和 equals()

用到的一些结构

1.哈希表 默认长度16 哈希每次扩容为原来的2倍,哈希表的负载因子为0.75

2.链表 链表长度>=8 且 哈希表长度>=64 才会转红黑树 否则会先扩容哈希表

3.红黑树

讲讲添加一个元素的过程

先用该元素的key计算出hash值,然后得到index (要插入的位置),如果该位置为空,new一个Node存放,如果不为空的话,是链表就挂载在该元素的下面,是红黑树就加到树里面去,链表加到一定条件,哈希表就会扩容,或者把链表转为红黑树

HashMap不能有多个线程同时操作,如果有,则会抛出java.util.ConcurrentModificationException(并发修改异常)

线程安全:Hashtable 给操作的方法都添加了synchronized 但是并发效率低

ConcurrentHashMap

ConcurrentHashMap也是线程安全的,但是与Hashtable实现线程安全的方式不同,它没有直接给方法加锁,

它是给哈希表的每一个位置加锁,将锁的粒度细化了,提高了并发效率

如何细化锁:不使用专门的分段锁了,而是采用每一个位置上的第一个节点Node对象,作为锁对象

使用CAS+synchronized实现线程安全

当哈希表的某个位置上还没有Node对象时,如果此时有多个线程操作,采用cas机制进行比较判断

如果某一个位置上已经有了Node对象,那么直接使用Node对象作为锁即可

ConcurrentHashMap 和 Hashtable 都不能存储为null的键和为null的值

为了消除歧义 因为他们都是在多线程场景下使用的,返回null时,不能分辨出是key的值为null,还是没有这个key,返回的null

ArrayList/Vector

ArrayList 数组列表 线程不安全的

Vector 数组列表 线程安全的 是直接在方法上加的锁,效率低

get方法也加了锁,如果有多个线程读操作,也只能一个一个读,效率低

CopyOnWriteArrayList

CopyOnWriteArrayList:

将读写的并发进一步提升了,

读操作(get())是完全不加锁的,只给能改变数据的方法(add,set,remove)进行了加锁,而且为了操作时,不影响读操作,操作前先将数组进行拷贝,在副本上修改,修改之后,将副本重新复制到底层数组。

做到了只有写写是互斥的,读写,读读都不互斥,

适用于 , 读操作多,写操作少场景

CountDownLatch

CountDownLatch 辅助类 递减计数器 使一个线程 等待其他线程执行结束后再执行 相当于一个线程计数器,是一个递减的计数器 先指定一个数量,当有一个线程执行结束后就减一 直到为0 关闭计数器 这样线程就可以执行了

线程池

池的概念

字符串常量池

String a = "abc";

String b = "abc";

a==b;//true

IntegerCache.cache -128 --127 进行缓存

数据库连接池 为了避免重复创建连接对象和销毁连接对象,事先,先创建若干个连接对象

池就是一个缓冲,可以事先准备好一些数据,用的时候直接只用即可,提高效率

线程池

为什么要用线程池?

有时,有许多的任务需要执行,而且每个任务都比较短,这种场景下,需要大量的创建线程,这样一来创建的开销就变大了。

可以事先创建一部分线程,不销毁,有任务时提交给线程去执行,执行完后不结束线程,避免了频繁创建线程。

在jdk5之后java中就提供了线程池的实现类

线程池优点:

重复利用线程,降低线程创建和销毁带来的资源消耗

统一管理线程,线程的创建和销毁都由线程池进行管理

提高响应速度,线程创建已经完成,任务来到可直接处理,省去了创建时间

ThreadPoolExecutor 类

Java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类, 因此如果要透彻地了解Java 中的线程池,必须先了解这个类。

ThreadPoolExecutor 继承了AbstractExecutorService 类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

构造器中各个参数的含义
corePool:

核心线程池数量 5 创建ThreadPoolExecutor 对象后,其实线程数量是0,有任务到来时,才会创建新的线程放到线程池,直到核心线程池数量达到设定的值。

可以直接调用prestartAllCoreThreads()或者prestartCoreThread(),创建线程池对象之后,就可以立即创建线程。

maximumPoolSize:线程池最大线程数 10

keepAliveTime:非核心线程池中的线程,在没有任务执行时,保持空闲多久后销毁,时间到期后,可以销毁空闲的线程

unit:keepAliveTime时间单位

workQueue:一个阻塞队列,存放等待执行的任务

threadFactory:线程工厂,主要用来创建线程

handler:表示拒绝执行任务时的策略(拒绝策略)

线程池执行过程

当任务到达时,首先在核心线程池创建线程执行任务,如果核心线程池未满,那么直接让核心线程池线程执行,如果核心线程池已经满了,那么就将任务存放到队列中,等待执行

当任务继续提交过来时,如果队列也已经放满了,就看非核心线程池中的线程有没有达到最大线程数量。

如果已经达到最大,并且没有空闲线程,那么就采取某种拒绝策略.

等待队列

ArrayBlockingQueue 给定队列的容量

LinkedBolckingQUeue

public class Test {public static void main(String[] args) {/* MyTask myTask1 = new MyTask(1);创建任务对象Thread thread = new Thread(myTask1);/./创建线程对象  提交任务thread.start(); 启动线程*/
​/*通过线程池执行任务*/ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());for(int i=1;i<=8;i++){MyTask myTask = new MyTask(i);//executor.execute(myTask);//添加任务到线程池Future<?> submit = executor.submit(myTask);}executor.shutdown();//executor.shutdownNow();}
}
 

线程池中的拒绝策略

构造方法的中最后的参数RejectedExecutionHandler 用于指定线程池的拒绝策略。当请求任务不断的过来,而系统此时又处理不过来的时候,我们就需要采 取对应的策略是拒绝服务。

默认有四种类型:

AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。

CallerRunsPolicy策略:让提交任务的线程去执行 例如mian线程。

DiscardOldestPolicy 策略:丢弃等待时间最长的一个任务,把新来的任务添加进去。

DiscardPolicy 策略:该策略丢弃无法处理的任务,不予任何处理。

提交任务

execute():  提交任务 但是不能接收返回值
submit(): 提交任务 可以接收返回值

关闭线程池

shutdownNow():立即关闭线程池,还有未执行的线程,也会被中断。

shutdown(): 关闭时,会把已经提交的线程池中的线程执行完。

ThreadLocal

ThreadLocal 叫做线程变量,意思是ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

为每一个线程创建ThreadLocalMap对象,在ThreadLocalMap对象中存储现成自己的变量副本。

public class ThreadLocalDemo {//创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭private  static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {return 0;}};public static void main(String[] args) {new Thread(){@Overridepublic void run() {localNum.set(1);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}localNum.set(localNum.get()+10);System.out.println(Thread.currentThread().getName()+":"+localNum.get());//11}}.start();
​new Thread(){@Overridepublic void run() {localNum.set(3);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}localNum.set(localNum.get()+20);System.out.println(Thread.currentThread().getName()+":"+localNum.get());//23}}.start();
​System.out.println(Thread.currentThread().getName()+":"+localNum.get());//0}
}

对象的四种引用

强引用

有引用指向对象

软引用

被SoftReference对象管理的对象,在内存不够时,先不回收被SoftReference管理的对象,先进性一次垃圾回收,当垃圾回收后,如果内存够用了,就不回收SoftReference管理的对象,如果回收后内存还不够,才会被回收

弱引用

被WeakReference管理的对象,只要遇到一次gc就会被回收

虚引用

Threadlocal与弱引用WeakReference有关系,那么在垃圾回收时,会把键回收了,但是值还存在强引用,不能回收,就会造成内存泄漏问题。

如何解决?需要手动remove();释放空间

每次使用完ThreadLocal 都调用它的remove()方法清除数据。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词