欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 高考 > Java并发编程

Java并发编程

2025/9/16 10:22:53 来源:https://blog.csdn.net/fxbsdl/article/details/147396711  浏览:    关键词:Java并发编程

        在我们学习并发编程之前就得先知道线程是什么

什么是线程?

        线程是cpu的执行最小单元,是进程中一个独立的任务,一个进程中可以有多个线程

重点区别进程:进程是操作系统分配资源的最小单位。

Java创建线程的方式?

1、继承Thread类

2、实现Runable接口

3、实现Callable接口

4、使用线程池创建

线程中常用到的方法:

run () -- 线程执行的任务写在这一个方法中

start() --启动线程

sleep () -- 线程睡眠制定的时间,是Thread类中的提供的静态方法,通过类名去调用

wait() -- 线程等待,通过notify()唤醒线程或者notifyAll()唤醒所有线程,wait是Object类中的 方法

join() -- 等待线程结束执行

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

线程状态

新建状态:new Thread()

就绪状态:start() 等待操作系统的调用

运行状态:获得cpu的执行权

阻塞状态:sleep()、wait()、输入、等待获取锁;sleep时间到,调用完join()的线程,输入完毕、获取到同步锁,唤醒等就会从阻塞状态进入就绪状态

死亡或销毁:线程任务执行完了,或者出现异常了

什么是多线程?

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

        多线程访问共享数据,就会出现线程安全。

解决线程安全问题:

原子类:java.util.concurrent.atomic包下的一系列原子类,如AtomicInteger,通过CAS(comper And Swap)确保操作原子性

加锁

synchronized锁:

1、同步方法,方法加锁,确保线程一个一个执行

2、同步代码块,静态方法中,同步对象是类的class对象、非静态方法中,同步对象是this

public void increment() {// 同步代码块,同一时刻只有一个线程能进入该代码块synchronized (同步对象) {count++;}}

ReentrantLock锁:是Java提供的可重入锁,相比synchronized更加灵活。

 public void increment() {// 获取锁lock.lock();try {count++;} finally {// 释放锁lock.unlock();}}

使用线程安全的集合类:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteSet

 线程通信

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

wait、notify、notifyAll,以上只能在同步代码块中使用,调用的对象只能是锁对象

wait和sleep的区别:

wait是object类中的方法,只能等待线程唤醒,等待时可以释放锁

sleep是Thread类中的方法,休眠时间结束后自动唤醒,进入就绪状态,休眠期间不会释放锁,其他线程进不来。

并发编程

前提是多线程场景

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

多线程存在的问题:多个线程同时访问共享数据(买票、抢购、秒杀等场景)用户同时向后端发送多个请求。多个请求同时访问数据会出现问题,并发编程就是让多个请求并发的执行(在一个时间段内,线程依次的执行)

并发执行:在计算机层面,是指在一个时间段内,多个线程依次执行的意思

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

并发编程的核心问题

1、不可见性

        由于Java内存模型(JMM Java Memory Model)分为主内存和工作内存(缓存区),所有的共享变量存储在主内存中,当线程要对主内存的数据进行操作时,首先要将变量加载到工作内存中才能进行操作,这样就产生了不可见性,当两个线程同时操作一个数据时,一个线程修改了数据,另一个线程是不知到的。

 2、乱序性

        编译器为了进一步优化,在cpu执行指令是,有的指令需要等待数据的加载,此时会将后面某些指令提前执行,这样指令的顺序就被打乱了,这种就会出现乱序性。

        乱序执行也是有基本原则的,两条直接有关系的指令是不会被打乱的。

3、非原子性

        操作系统中线程的切换带来非原子性问题。

        cpu的执行在指令层面是原子性的,但是在高级语言中,一条语句往往需要被编成成多条指令执行,这样在多线程情况下,切换线程执行时也会造成指令切换,线程切换等导致了非原子性问题。

总结:Java的内存模型导致了不可见性,编译器优化导致了乱序性,前程切换导致了非原子性

volatile关键字

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

1、解决不可见性

        两个线程同时执行时,一个线程改变了数据,另一个线程是立即可见的

2、解决乱序性

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

如何解决非原子性?

加锁:synchronzied和ReentrantLock加锁解决非原子性

加锁可以解决不可见性和乱序性

原子类

概念:原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。

原子类的原子性是通过volatile+CAS实现原子操作的,以AtomicInteger为例子:

AtomicInteger类中的value是volatile关键字修饰的,保证了value的内存可见性,这也为后面的CAS实现提高了基础。

低并发情况下:使用 AtomicInteger。

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

CAS说是乐观锁的一种实现,他采用的是自旋的思想,是一种轻量级的锁机制。(实际并不是锁,而是实现了的锁效果)

思想:每次判断预期值和内存中的值是不是相同的,如果不相同则说明该内存值已经被其他线程修改了,因此需要拿到改最新值作为预期值,重新判断,而该线程不断的循环判断是否该内存值已经被其他线程修改了,这就是自旋思想。

当一个线程A从内存中读取一个值(期望值),在工作内存中进行改变,往主内存写之前先从内存中读取这个值进行对比,如果第一次拿到的值(期望值)和主内存中读取的值不一样,说明其他线程已经修改了该值,线程A需要重新操作值,再次重复之前的操作,如果相同,线程A将更新后的值写到主内存中。

CAS适合在线程数较少的情况下,由于不加锁,线程不会阻塞,所有线程一直尝试对变量进行操作,效率高于加锁。但是线程数量加多时,所有线程一直自旋,尝试操作,cpu工作量会增大。

CAS可能出现ABA问题:

若一个变量的值从 A 变为 B,再从 B 变回 A,此时 CAS 操作会认为该变量的值没有发生变化,但实际上它已经经历了变化。在 Java 中,可以使用 AtomicStampedReference 类来解决 ABA 问题。

AtomicStampedReference 类是带版本号的原子类来区分是否值发生了改变

AtomicStampedReference stampedReference = new AtomicStampedReference(100, 0);//0就是版本号

锁的分类

首先Java中有很多锁的名词,这些并不是全是指锁,有的指锁的特性,有的指锁的设计,有的指锁的状态。

1、乐观锁/悲观锁

乐观锁:认为在多线程操作时,不加锁不会出现问题的,例如原子类:采用volatile+CAS的思想,称为乐观锁。

悲观锁:认为在多线程操作时,不加锁是会出现问题的,是一种加锁的实现。例如synchronized和ReentrantLock都属于锁,称为悲观锁。

2、可重入锁

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

 public synchronized void setA(){System.out.println("进入到外层方法");setB();//如果不可重入setB就不会被执行}public synchronized void setB(){System.out.println("进入到内层方法");}

如果不可重入,就会导致无法进入内层方法,就会导致死锁。

3、读写锁

ReentrantReadWriteLock类,实现读写锁,有读锁也有写锁

读读不互斥:多个线程都是进入读锁,此时没有其他线程进入写锁,那么多个线程同时进入到读锁区域(提高了读的效率),一旦有线程进行写操作,那么读操作就不能进入。

读写互斥写写互斥

public class RRW{private int data;private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();public void set(int data) throws InterruptedException {lock.writeLock().lock();//获取写锁System.out.println(Thread.currentThread().getName()+"获取写锁");try {Thread.sleep(2000);System.out.println(Thread.currentThread().getName()+"准备写入数据");this.data=data;System.out.println(Thread.currentThread().getName()+"写入"+this.data);}finally {System.out.println(Thread.currentThread().getName()+"释放写锁");lock.writeLock().unlock();//关闭写锁}}public void get() throws InterruptedException {lock.readLock().lock();//获取读锁System.out.println(Thread.currentThread().getName()+"获取读锁");try {Thread.sleep(2000);System.out.println(Thread.currentThread().getName()+"准备读数据");System.out.println(Thread.currentThread().getName()+"读数据"+this.data);}finally {System.out.println(Thread.currentThread().getName()+"释放读锁");lock.readLock().unlock();//关闭读锁}}}

适合多个线程读,少量线程写的场景,提高了吞吐量和并发性

4、共享锁/独占锁

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

读写锁中的写锁就是一个独占锁synchronizedReentrantLock都属于独占锁,一次只允许一个线程进入。

5、分段锁

也不是一种实际的锁,是一种锁的实现思想,用于将数据分段,给每个分段都会单独加一个锁,将锁的粒度细化,提高效率。

例如:ConcurrentHashMap的实现,由于ConcurrentHashMap的底层哈希表有16个空间,可以利用每一个位置上的第一个节点当作锁,这样就可以由不同的线程操作不同的位置,只是同一个位置不可以有多个线程操作,如果第一个位置位null时,就采用CAS机制

 6、自旋锁

自旋锁也不是一种实际的锁,是通过不断自旋重试的方式抢锁,在低并发的场景下效率较高。

线程尝试不断的获取锁,当第一次取不到时,线程不阻塞,会尝试继续获取锁,有可能后面尝试几次后,有其他线程释放锁了,此时可以获取锁,当尝试获取一定次数后(默认是10次,可以通过参数设置),仍然没有获取到锁,就会进入阻塞状态。

synchronized就是一种自旋锁,在并发量低的情况下就适合自旋锁

自旋是比较消耗cpu的,因为需要不断的自旋循环重试,不会释放cpu的

优点
  • 减少上下文切换开销:由于线程在等待锁的过程中不会进入阻塞状态,避免了线程上下文切换带来的时间和资源开销,在锁被持有的时间较短时,性能表现较好。
  • 响应速度快:一旦锁被释放,自旋的线程可以立即获得锁并继续执行,无需等待操作系统的调度,响应速度更快。
缺点
  • CPU 资源浪费:在自旋等待期间,线程会持续占用 CPU 资源进行无意义的循环检查,当锁被持有的时间较长时,会导致大量的 CPU 资源被浪费,降低系统的整体性能。
  • 优先级反转问题:如果低优先级的线程持有锁,高优先级的线程需要等待低优先级线程释放锁,这可能导致高优先级线程的任务无法及时执行,出现优先级反转的情况。
  • 使用场景

    自旋锁适用于以下场景:

  • 锁持有时间短:当临界区代码执行时间较短,锁被持有的时间也相应较短时,使用自旋锁可以避免线程上下文切换的开销,提高性能。
  • 多核处理器:在多核处理器系统中,多个线程可以同时运行,一个线程自旋等待时,其他线程可以继续在其他 CPU 核心上执行,从而减少了等待时间。

7、公平锁和非公平锁

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

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

四种锁状态

Java中为了synchronized进行优化,提供了四种锁状态(这四种锁状态都不是Java语言中的锁,而是JVM为了提高锁的获取和释放效率,而做的优化),在synchronized锁实现时,在同步对象可以记录锁的状态,锁的状态是通过对象监视器对象头中的字段来表明的。

1、无锁状态:就是本来的状态

2、偏向锁状态:一段同步代码块一直有一个线程执行,那么在锁对象中记录下线程信息,可以直接获得锁。

3、轻量级锁状态:当锁状态为偏向锁时,还有其他线程访问,锁状态升级为轻量级锁,线程不阻塞,采用自旋方式获取锁。

4、重量级锁状态:当锁状态为轻量级锁时,如果有大量的线程到来,大量的线程自旋,锁状态升级为重量级锁,自旋的线程会进入阻塞状态,由操作系统去调度。

对象结构

        在Hotspot虚拟机中,对象在内存中的布局为三块区域:对象头实例数据对其填充;对象头是实现synchronized的锁状态的基础,synchronizd的使用,锁对象存储在对象投中,是轻量级锁和偏向锁的关键。

        对象头中一块区域为Mark Word,用于存储对象自身运行时的数据,如:HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向锁的ID等等。

 synchronized锁实现

synchronized是一个关键字,实现同步,还需要我们提供一个锁同步对象,记录锁状态,记录线程信息。

synchronized锁实现是隐式的,可以修饰方法,也可以修饰代码块,底层实现是依赖字节码指令的。

修饰方法时,会在方法上添加一个ACC_SYNCHRONIZED标志,依赖底层的监视器实现锁的控制

修饰代码快时,为同步代码块添加一个monitorenter指令进行监视,执行结束,执行monitorenxit指令,有线程进入,计数器加1,线程执行结束计数器减1

AQS

AQS的全称:AbstractQueuedSynchronizer,在java.util.concurrent.locks包下,是实现线程同步的框架。

抽象同步队列,是并发编程中许多实现类的基础,例如:ReentrantLock底层就用了AQS

AQS类中维护了一个状态,    private volatile int state;
当有线程获取时,首先获取状态变量,以ReentrantLock为例,
if state==0 当前线程可进入 state++,大于 0 表示锁已被持有,且重入次数为 state 的值。
线程可以通过 CAS(Compare-And-Swap)操作来原子性地修改 state 的值,
以此来实现对共享资源的控制。还有一个内部类 节点static final class Node{//重点信息volatile node prev;volatile Node next;volatile Thread thread;//存储等待的线程}private transient volatile Node head;队列头private transient volatile Node tail;队列尾维护了一个双向链表FIFO 双向队列:当多个线程竞争同一个同步资源时,
那些没有获取到资源的线程会被封装成一个节点(Node)加入到这个队列中进行等待。
队列中的线程会按照先进先出的顺序依次尝试获取资源

ReentrantLock底层实现

ReentrantLock是Java.util.concurrent.locks包下的类,实现了Lock接口,比synchronized加锁更加灵活

 类结构

ReentrantLock底层实现两种方式:

1、默认是非公平锁实现。2、通过参数控制实现公平锁

当线程刚进去时,先去插队,如果插队不成功,则走正常流程排队取锁

JUC常用类

前提复习HashMap:

        HashMap是双列集合,实现了Map接口,是键值对存储,键不能重复,值可以重复,只能存储一个为null的键,键是无序的,是线程不安全的,多线程操作会抛出异常的(ConcurrentModificationException,并发修改异常) ,判断键是否重复:hashcode()和equails()

数据结构:

1、哈希表,默认长度是16。

2、链表长度>8,哈希表长度<64,哈希表扩容(哈希表长度为原来的2倍,哈希表的负载因子为0.75),哈希表长度>64,转为红黑树。

3、红黑树

复习HashTable:

        线程安全的,但是是给方法上加了synchronized,效率低。

ConcurrentHashMap

        线程安全的,但是也是HashTable的线程安全实现是不同的。其没有给方法上加锁,而是给哈希表的每一个位置加锁,将键的粒度细化,提高了锁的并发效率。

如何细化锁的?

不是专门的分段锁,而是采用每一个位置上的第一个节点Node对象作为锁对象

 使用CAS+synchronized实现线程安全:当哈希表上还没有Node对象是,此时有多个线程操作,就采用CAS机制进行比较判断,如果某个位置上已经有了Node对象,那么直接使用node对象作为键即可。

和HashTable有一个共同点都不支持 键null和值为null

 不能put null是因为,无法分辨key是否没有找到为null还是有key值为null,这在多线程里面是含糊不清的,所以就不让为null。

二者都是支持并发的,这样会有一个问题,当通过get(key)获取对应的value是,如果获取到的是null时,无法判断是put(key,value)时为null,还是就就key就没有做个映射。

CopyOnWriteArrayList

ArrayList:数组链表,线程不安全的

Vevtor:数据链表,线程安全的,是给方法加锁的synchronized,效率低,同时也给get()方法加了锁。

CopyOnWriteArrayList:

        将读写效率进一步提升,读操作get()是不加锁的,只给可以改变数据的方法(add、set、remove)加锁(通过ReentrantLock加锁),而且为写操作时不影响读操作,操作前将数组进行复制,在副本上进行操作,写完之后将副本重新赋值到原数组中,所以到底只有写写是互斥的,读写、读读不互斥,适用于读操作多,写操作少场景。

CopyOnWriteArraySet

CopyOnWriteArraySet是基于CopyOnWriteArrayList实现的,不能存储重复数据

辅助类CountDownLatch

        CountDownLatch允许一个线程等待其他线程执行完毕之后在执行。底层实现是通过AQS来完成的,创建CountDownLatch对象时指定初始值是要等待线程的数量,每当一个线程执行完毕之后,AQS内部的sate就-1,当sate=0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

池的概念

字符串常量池

String s1="abc";
String s2="abc";
s1==s2 // true

Integer自动装箱,缓存了-128~127

Integer i1=100;
Integer i2=100;
System.out.println(  i1==i2);// true

数据库连接池

阿里巴巴Druid数据库连接池

帮我们缓存一定数量的链接对象,放在池子里用完还会到池子

中,减少了对象的频繁创建和销毁的实际开销

线程池

        为了减少频繁的创建和销毁线程,jdk5开始引入线程池,建议使用ThreadPoolExecutor类来创建线程,提高效率

ThreadPoolExecutor类

重点是构造器

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) 

 7个参数

corePoolSize:核心线程池中的数量(初始化的数量)5

maximumPoolSize:线程池的最大数量10(包含核心线程池的),(再次创建5个)

keepAliveTime:空闲线程存活的时间,当核心线程池中的线程足以应付任务时,非核心线程池的线程在指定空闲时间到期后就会销毁。

unit:时间单位

workQueue:5 等待队列,当核心线程池中的线程都在使用时,会先将等待的任务放到队列中,如果队列满了,才会创建新的线程(非核心线程池中的线程)

threadFactory:线程工厂,用来创建线程池的线程。

handler:拒绝理任务时的策略,有四种拒绝策略

线程池的工作流程:

        当大量的任务到来时,先判断核心线程池中的线程是否都忙着,有空闲的,直接让核心线程中的线程执行任务,没有空闲的 ,判断等待队列是否已满,如果没有满,则加入等待队列,如果已满,判断非核心线程池中的线程是否都忙着,如果有空闲的,没满,交由非核心线程池中的线程执行,如果非核心线程池也满了,那么就是要拒绝策略处理。

线程池中的四拒绝策略 

1、AbortPolicy  抛异常策略

 2、CallerRunsPolicy:由提交任务的线程执行,例如在mian线程提交,则由mian线程执行拒绝的任务

 3、DiscardOldestPolicy:丢弃等待时间最长的任务

 4、DiscardPolicy:丢弃最后来的任务。

 关闭线程池

executor.shutdown();//不在接收新的任务,
会把线程池中还有等待队列中已有的任务执行完,在停止
executor.shutdownNow();//立即停止,队列中等待的任务不在执行

线程池中的等待队列

ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,创建时必须设置长度,按FIFO排序量。

LinkedBlockingQueue:基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置是一个最大长度为Integer.MAX.VLAUE。

ThreadLocal

本地线程变量,为每一个线程提供一个变量副本,只在当前线程中使用,相互是隔离的。

底层结构:

为每一个线程对象创建ThreadLocalMap对象,赋给Thread类中的threadLocals,用threadLocals存储每个线程的变量。

set()方法

 get()方法

 

对象引用

1、强引用

Object obj=new Object();

obj.hashCode();

obj=null;

对象如果有强引用关联那么肯定是不能被回收的

2、软引用

Object o1=new Object();

SoftReference<Object> softReference=new SoftReference<Object>(o1);

softReference类包裹的对象,当内存充足时,不会被回收,当内存不足时,即便有引用指向,也会被回收。

3、弱引用

WeakReference类包裹的对象,只要发生垃圾回收,该对象都会被回收掉,不管内存是否充足

Object o1=new Object();

WeakReference<Object> weakReference=new WeakReference<Object>(o1);

ThreadLocal 被弱引用管理 static class Entry extends WeakReference<ThreadLocal<?>>(){}

当发生垃圾回收时,被回收掉,value还和外界保持引用关系,不能被回收掉

4、虚引用

PhantomReference类包裹的对象,随时可以被回收,通过虚引用对象跟踪对象回收的状态

ThreadLocal内存泄漏 

readLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,这样就会导致ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,只有 thead 线程退出以后,value 的强引用链条才会断掉。但如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

ThreadLocal 正确的使用方法

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

版权声明:

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

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

热搜词