java并发变成的艺术知识点摘要理解

本文深入讲解Java并发编程的核心概念和技术,包括线程间通信、同步机制、锁的使用及实现原理,同时介绍了Java并发容器、框架及工具类的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程间的通信

  • 共享内存
    访问内存中的公共状态----隐式通信
  • 消息传递
    线程之间没有公共状态-通过发送消息,来显示通信

线程之间的同步

  • 共享内存方式,需要手动指定不同线程发生顺序 – 显式
  • 消息传递,消息的发送必须再消息接收之前,所以是,同步是—隐式的。

内存抽象

  • 堆: 实例,静态域,数组
  • 栈:
  • 内存屏障

从源代码到指令序列的重排序

  • 1 编译器优化重排序
  • 2 指令级并行重排序
  • 3 内存系统重排序

happens-before JDK5 后,使用happens-before

规则

  • 同一个线程的中的任意操作 happends-before 该线程后续操作
  • 一个锁的解锁 先于 随后对这个锁的加锁
  • volatile 的写,先于后续对volatile的渎
  • 传递性
  • 注 happens-before 指前一个操作按照顺序排在后面一个操作之前,且结果对后一个可见。。。并不一定前一个先于后一个被执行。

数据依赖性

两个操作访问同一个变量,一个为写操作就存在数据依赖性(写后读,写后写,读后写)
重排序会导致结果的改变。所以编译器和处理器在重排序会遵守数据依赖性,即不会对此重排序
注:(只针对单核CPU且单线程)

as-if-serial (不管怎么重排序,执行结果不能被改变)

int a =1; 
int b = 2;
int c=a+b;

waitNotify 经典范式

flag 是一个存储在内存中的VOLITALE 标志,,或者可以是对象中的一个属性,主要是flag要对两个线程都可见

  • 通知方
synchronized(对象){
    改变flag
    对象.notifyAll()
}
  • 接收方
synchronized(){
  if (!flag){
     对象.wait()
  }  
  逻辑代码

}

即两个线程之间通过共享内存中的变量flag进行通信

管道输入/输出流(piped)

类似文件读写的字节流,字符流,,,但是有一点不同,,它的传输媒介是内存,主要应用于不同线程之间的通信

面向字节
  • PipedOutputStream
  • PipedInputStream
面向字符
  • PipedWriter
  • PipedReader
    pip类型的流要先绑定即,使用connect方法(否则访问该流的时候会抛出异常)。

join 方法

thread2 需要等待thread1 结束以后再停止,那么先thread1.join() 然后thread2 停止

java中的各种锁

Lock 接口,有以下方法

  • void lock() // 阻塞 获取锁,调用该方法当前线程将会获取锁,获得以后从该方法返回
  • void lockInterruptibly() //阻塞 可中断的获取锁,在获取锁的过程中,可以响应中断
  • boolean tryLock() 非阻塞的获取锁,调用以后立马返回,获取到锁返回true,失败返回false
  • tryLock可以填入超时参数,(超时,中断,获取到锁)会返回
  • void unlock() 释放锁
  • Condition newCondition() 获取等待通知组件,组件和当前锁绑定,只有获得了锁才能调用组件的wait方法释放锁

同步器 构建锁的基础框架

内置一个int型的变量表示同步状态
内置FIFO 队列完成要获取锁的线程的排队

同步器的使用

需要继承同步器,并重写抽象方法

1 使用同步器的

  • getState 获取状态
  • setState 设置状态
  • compareAndSetState(int expect,int update) CAS方法设置当前状态可以保证设置的原子性

2 重写方法

独占式

  • boolean tryAcquire(int arg)//独占式获取状态
  • booblean tryRelease(int arg) //释放同步状态

共享式

  • int tryAcquireShared(int arg) //返回大于等于0 获取成功
  • boolean tryRelaseShared(int arg) //释放同步状态
  • boolean isHeldExclusively() // 同步状态是否被当前状态独占

  • 独占锁:同一时刻只能由一个线程访问锁的状态也就
  • 共享锁,多个线程可以同时获取锁状态,同时访问资源
    例子: 文件读写,,读操作是共享式,写操作是独占式

同步方法基本分三类

  • 独占式的获取释放锁状态
  • 共享式获取和释放锁状态
  • 查看队列中的线程情况

队列同步器的实现

  • 同步队列: 里面存放的是获取同步状态失败的线程(Node)

  • 什么是Node: Node=等待状态+prev+next+nextWaiter+thread

  • 同步队列的首节点释放锁以后,从首届点位置出列,原后继节点变首节点 遵循FIFO原则

  • 缓存队列Condition:

重入锁ReentrantLock

一个已经获取到锁的线程,没有释放锁的前提下任然可以获取到锁

重入的实现:
在获取锁的时候做了两步判断
1,锁是否可用
2,不可用,,获取锁的线程是不是当前的线程。
是 —> 返回true state ++
否 -> 返回false 获取锁失败

公平和非公平的实现:
tryAcquire
在获取锁状态时候对了一个判断,当前节点是否有前驱节点
是 -> 说明前驱节点比当前节点更早的请求锁,等待前驱节点释放以后才能执行该线程
否 -> 当前节点就是首节点,执行

非公平锁是默认的(线程切换少,有更大的吞吐量,但是可能会线程饥饿)

读写锁(ReentrantReadWriteLock)

之前的Mutex 和 ReentrantLock 都是排他锁也就是同一时刻只允许一个线程加锁。

特性 :

  • 支持公平性和非公平性

  • 支持重进入

  • 支持降级 级别 写锁 > 读锁 写锁可以降级为读锁

  • 读锁-写锁,写锁-写锁,之间是互斥的,,

  • 读锁之间是共享的

ReentrantReadWriteLock 是ReadWriteLock的实现类。

ReentrantReadWriteLock的同步器的状态state
独占锁 state boolean true 获取锁成功 false 获取锁失败
共享锁 state int >=0 获取锁成功 < 0获取锁失败

读写锁 state int 高16位 读状态 低16 位

可降级(重要特性)两大特点

  • 可以提高并发
  • 可以保证线程的安全
        while (true) {
            try {
                writeLock.lock();    //1 加写锁
                System.out.println(">------>>>>>-------->>>>加写锁>------>>>>>-------->>>>加锁完成,当前线程为" + Thread.currentThread().getName());

                Thread.sleep(3000);
                System.out.println("---------write--------,我是-------------------------------------------------------------------------" + Thread.currentThread().getName());
                
                // 此处写更新数据的业务逻辑 3
                
                System.out.println("------------->>>>>-------->>>>加读锁>------>>>>>-------->>>>加锁完成");
                readLock.lock();    //2 加读锁
                System.out.println("--------<<<<-----<<<<<解写锁<<<<<-------<<<<<------------<<<<---------");

                // 从此刻开始 其它的读线程可以加读锁读取数据  因而提高了并发      但是,读锁并没有释放,所以其它的线程不能加写锁,因而保证了数据安全,,后面的业务逻辑5处,保证了数据不会被其它线程更新
                writeLock.unlock();  // 4 解写锁,,
                
                //....... 此处业务逻辑  5
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("<<<<-----<<<<<解读锁<<<<<-------<<<<<释放------------<<<<,当前线程为" + Thread.currentThread().getName());
                readLock.unlock();// 6 解读锁

            }
        }

降级

即支持 获取写锁->获取读锁->释放写锁->释放读锁

升级,不支持

不支持 获取读锁->获取写锁->释放读锁

JAVA并发容器和框架

ConcurrentHashMap 线程安全且高效的HasMap

hashMap 线程不安全的两点

  • put 操作 两个线程同时put,key冲突导致最后执行put的元素覆盖掉之前的
  • 扩容的时候

hashTable的低效率

  • hasTable内部使用synchronized 效率低,在put的时候不能get

锁分段技术

在hashTable的基础上,将数据分段存储,分段加锁,,,段与段之间相互不影响。当对一段执行put时候,,,其它段的put和get均可以操作

  • segment 锁
  • hashEntry 数组,存放的数据 一个锁对应若干个hashEntry
ConcurrentLinkQueue 线程安全的队列
  • head 节点
  • tail 节点
    在ConcurrentLinkQueue 队列中,,tail并不总是指向尾节点,同理head也并不总是指向头节点。。
入队
  • 先去看自己的tail节点是否是尾节点,
    如果是尾节点,用cas方法插入元素,成功-ok,失败-重新插入(说明有其他元素刚插入)
  • tail节点不是尾节点,通过循环查询,找到尾节点,在尾节点cas插入元素,并将tail指向尾节点
出队列

类似于入队,不过操作的是头节点。

线程安全是通过CAS方法实现的入队和出队
高效—hop ,因为并不是每次都会更新tail节点,

BlockingQueue 阻塞队列

阻塞队列 是线程安全的队列
两个特点

  • 队列元素为满时候,继续插入元素的线程会阻塞
  • 队列元素为空的时候,获取元素的线程会阻塞
    三类方法
方法抛出异常返回特殊值一直阻塞超时推出
插入方法add()offer()put()offer(e,time,unit)
移除方法remove()poll()take()poll(e,time,unit)
检查方法element()peek()不可用,不可用

无界队列永远返回true,JDK7 提供的7个阻塞队列

  • ArrayBlockingQueue() 数组结构 有界阻塞队列
  • LinkedBlockingQueue 链表结构 有界阻塞
  • PriorityBlockingQueue 支持优先级无界阻塞
  • DelayQueue 使用优先级队列实现的无界阻塞
  • SyncchronousQueue 一个不存储元素的阻塞队列
  • LinkedTransferQueue 链表结构无界阻塞
  • LinkedBlockingDeque 链表结构双向阻塞

Fork/join框架(工作窃取算法)

Fork - 切分任务

join - 合并子任务的执行结果

工作窃取 - 一个线程从其它线程的任务队列里面获取任务并执行

Fork/Join框架基本思想是将一个大任务分成若干个小任务,分在若干个双端队列里面,并给每个队列开设单独的线程。 工作窃取算法,在执行完自己队列里面的任务会从其它队列的尾部获取任务执行。

任务分割

ForkJoinTask - 分解任务

两个子类(继承它)

  • RecursiveAction 用于没有返回结果的任务
  • RecursiveTask 用于有返回结果的任务
任务执行

ForkJoinPool 执行任务

java中的13个原子操作类

原子方式更新基本类型
  • AtomicBoolean 原子更新boolean类型
  • AtomicInteger 原子更新整形
  • AtomicLong 原子更新长整形
    三个类几乎提供相同的方法
<!-- AtomicInteger 类 -->

         int addAndGet(int data) //将括号里面的值与 AtomicInteger 里面的值原子性相加并返回结果
         boolean compareAndSet(int expert,int update)   // 如果输入的等于预期的,,将该值设置为输入的值
         int getAndIncrement()  // 以原子方式将当前值+1    返回的是+1   之前的值
         void lazySet( int new value)   // 最终会设置成新的值,,,但是使用该方法以后,其它线程可能在一小段时间内读到的还是之前的值
         int getAndSet(int new value)     //以原子方式设置为newValue的值 并返回旧值
         int get()   // 拿到值

原子更新CAS实现 : 比如 要将 1 设置成2 cas的两个参数 except(预期值)即为 1 update 为 2 再 cas的时候会先去get() 当前值是否是预期值1 是,说明没有被其它线程修改过,设置为2 返回true, 如果不是1 则说明被其它线程修改过了,不进行设置返回false

我的理解: 原子类的cas方法基本都有UnSafe 类实现

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

UnSafe 类只有三种cas方法 (AtomicBoolean 是将boolean转化为int 类型,然后使用int类型的cas方法)

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
原子更新数组
  • AtomicIntegerArray 原子更新整形数组里面的元素
  • AtomicLongArray 原子更新长整形数组里面的元素
  • AtomicReferenceArray 原子更新引用数组里面的元素
  • AtomicIntegerArray 原子的方式更新数组的整形
原子更新引用类型
  • AutomicReference: 原子更新引用类型
  • AtomicReferenceFieldUpdater: 原子更新引用类型里面的字段
  • AtomicMarkableReference: 原子更新带有标记的引用类型
原子更新字段类
  • AtomicIntegerFileldUpdater 原子更新整形字段的更新器
  • AtomicLongFileldUpdater: 原子更新长整形字段的更新器
  • AtomicStampedReference:原子更新带有版本的引用类型

JAVA中的并发工具类

CountDownLatch 等待多线程完成

  传统的让当前线程等待其它线程执行完使用join
join的工作原理是,不停的去查询线程是否存活,如果其它线程存活,则让当前线程永远等待wait(0), 其它线程结束以后 使用notifyAll 因为notifyAll 封装再JVM里面,所以看不到。

  使用步骤

  • 1 构造方法参数设置点数。。
    static CountDownLatch count = new CountDownLatch(2);  
  • 2 每个线程执行完成以后调用countDown()方法使得点数-1
  • 3 等待线程中使用await()方法阻塞,当点数变成0的时候,会解阻塞,继续向下执行
CyclicBarrier 同步屏障

相当于创建了一个屏障,线程任务执行完成到达屏障调用await()方法通知屏障我已经到达,当满足某个条件的时候,屏障会打开,程序得以继续执行。 这个条件同样由构造方法的参数指定。

static CyclicBarrier cyclicBarrier = new CyclicBarrier(4);  // 参数设置 点
  // 高级用法,所有线程到达先执行methodFlag()自定义的线程,需要实现runable接口,重写run方法
static CyclicBarrier cyclicBarrier2 = new CyclicBarrier(4, new MethodFlag());

CyclicBarrier 和 CountDownLatch 区别

  • CyclicBarrier 构造函数多了一个重载形式,第二个参数可以指定,所有线程到达屏障后优先执行的方法
  • CyclicBarrier 可以重复使用,使用reset() CountDownLatch 只能用一次
    ,就像 CountDownLatch 是一次性班车,人满了20个发车,只发一次。
    CyclicBarrier 是可以重复使用的,每20个人发一车然后reset 然后等到20个再发一车。
Semaphore 控制并发线程数量

控制并发的线程的数量,,多个线程读取 和少个线程处理做适配(100个线程从内存读取数据,然后写入数据库,数据库只允许最大10个连接,100个线程的读取可以同时进行,但是写入的时候一次只有10个线程可以执行,其它的需要阻塞)

使用步骤

  • 构造方法参数指定允许的最大数量
  • 在需要控制线程数量的处理逻辑之前使用Semple的accquire()方法获取许可证,之后使用relese() 释放许可证。
public class SemaphoreDemo {
    static Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {
        Task task = new Task();
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(task, "task-" + i);
            thread.start();
        }
    }

    static class Task implements Runnable {

        @Override
        public void run() {

            // 此处不需要控制线程数量的逻辑处理
            System.out.println("读取完成  准备写入--我是Thread---" + Thread.currentThread().getName());
            try {
                semaphore.acquire();   // 获取许可证
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //  此处写需要控制线程数量的逻辑处理
            System.out.println("写入中---我是Thread---" + Thread.currentThread().getName());
            semaphore.release();      // 释放许可证
        }
    }

}
Exchanger 线程之间交换数据
  • 只能用于两个线程的交换,如果有多个线程执行交换
  • 那么按照线程发生的顺序两两交换。
  • 两个线程交换数据,一个线程执行了exchange,另一个没有执行,该线程会阻塞,等待另一个线程执行。
  • 如果一个线程阻塞一致没有交换对象。那么该线程会一致阻塞,用exchange(dataB,5, TimeUnit timeUtil) 这个方法,交换,可以设置超时时间,超时没有交换对象会报异常

使用步骤

  • 1 建立 Exchange 对象
     private static final Exchanger<String> exchange = new Exchanger<>();
  • 2 在需要交换的地方使用exchange()方法交换数据
    static class JobA implements Runnable {

        @Override
        public void run() {
            String dataA = "dataA";

            System.out.println("我是线程---" + Thread.currentThread().getName());
            try {
                System.out.println("jobA 拿到了--" + exchange.exchange(dataA));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class JobB implements Runnable {

        @Override
        public void run() {
            String dataB = "dataB";
            System.out.println("我是线程---" + Thread.currentThread().getName());
            try {
//                exchange.exchange(dataB)
                System.out.println("jobB 拿到了--" + exchange.exchange(dataB,5, TimeUnit.SECONDS));
            } catch (InterruptedException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }

线程池

三大优点

  • 降低资源消耗(免去了创建和销毁线程的消耗)
  • 增加响应速度 (直接执行任务,而不用等待创建线程)
  • 提高线程的可管理性(控制线程的数量)

当一个异步任务来了之后

  • 1 判断核心线程池是否有空闲,否->分配线程执行任务,是->下一步
  • 2 判断等待队列是否已满,否-> 将异步任务放入等待队列, 是-> 下一步
  • 3 判断当前线程池中线程数量是否大于最大线程池数数量,否创建(或分配线程)执行任务,是-> 交由饱和策略。

刚开始我很迷惑,为什么 顺序是1->2->3 而不是1->3->2。
直到看到线程池创建线程的过程, 因为创建线程需要先获取全局锁,而获取全局锁将会是一个严重的性能瓶颈,所以尽量的避免获取全局锁,,而添加到队列不需要获取全局锁,所以将添加到队列放在了第二步。 绝大部分任务都是在第二步中被放入了等待队列。

使用步骤
1 ThreadPoolExecuter创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,10,1, SECONDS,  new ArrayBlockingQueue<>(5));
必要参数
  • 核心线程池数量
    (这里创建的核心线程池,默认刚开始是没有创建线程的,等工作任务来了之后,才开始创建线程,当线程数小于核心线程池大小时候,来的任务都会创建新线程,即使其它线程空闲。直到线程数量=核心线程池的大小,这一步也称为预热,当然也可以通过)

  • 最大线程池大小:线程池中允许的最大线程数量,(等待队列满了以后,再来的任务会创建新线程,,线程空闲以后,时间超过aliveTime会被销毁,,如果等待队列是无限队列,那么这个值是没有意义的,因为任务会一致添加到等待队列中)

  • aliveTime 线程空闲以后保持存活的时间,(任务频繁的时候,适当增大该值,可以提高效率,但是任务如果是不频繁的那种,线程长时间空闲,会增加系统消耗

  • TimeUnit 时间工具类,只要是给前面的时间指定单位

  • BlockingQueue 等待队列,通常以下几种

    ArrayBlockQueue 数组对列,FIFO ,有界

    LinkBlockingQueue 链表队列,FIFO, 有界

    SynchronousQueue 不存储元素的对列(一次只能进一个,必须被消费了,才能继续进队列)
    PriorityBlockingQueue 一个具有优先级的无限阻塞队列

可选参数

ThreadFactory

  • ThreadFactory: 用于设置创建线程的工厂,可以给线程起一些有意义的名字
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

饱和策略(默认的是直接抛出异常)

    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
  • AbortPolicy : 直接抛出异常
  • CallerRuhnsPolicy 只用调用者的线程来执行任务
  • DiscardOldestPolicy 丢弃队列里最近的一个任务并执行当前任务
  • DiscardPolicy 不处理,丢弃掉

同样也可以实现RejectedExecutionHander 接口自定义策略

2 创建任务

简单 实现Runable接口,重写润方法

3 提交任务给线程池

submit()或者execute()方法

线程池的关闭

shutdown和shutdownNow

同:都是通过interrupt方法去中断线程(如果一个线程不能响应中断的话,那么线程任务可能永远无法中止)
关闭线程后isShutdown 就会返回true
所有任务都关闭以后,isTerminated方法会返回true

异:
shutdown: 将线程池状态设置为SHUTDOWN,然后尝试中断空闲的线程(通常调用这个)
shutdownNow: 将线程池的状态设置为SHUTDOWN ,然后尝试中断所有线程(如果不要求线程任务必须执行完的话也可以使用这个)

线程池的监控

executor.getTaskCount(); // 线程池需要执行的任务数量

executor.getCompletedTaskCount();// 已经执行的任务

executor.getLargestPoolSize(); // 曾经最大 (可以知道线程池是否满过)

executor.getPoolSize(); // 线程池的大小,只增不减??? >
coreSize+queueSize 之后创建的线程

executor.getActiveCount();// 获取活动的线程数

Executer框架

  • java的线程即是工作单元,也是执行单元。

工作单元(任务具体)包括:Runable Callable。

执行单元(任务的执行): Execute 框架

Execute 框架的结构(3大部分)
  • 任务:实现runable或者callable接口(工具类Executors可以将runable封装为callable对象)
  • 任务的执行:Executor->ExecuterService->(ThreadPoolExecuter,ScheduledThreadPoolExecutor)
  • 异步计算的结果:Future—>FutureTask
成员
  • ThreadPoolExecutor // 创建线程池的类
  • ScheduledThreadPoolExecutor // 执行周期任务的线程池
  • Future // 以及其实现类FutureTask 用来表示异步计算的结果,当我们把实现runable接口,或者callable接口的实现类(submit)方法提交给ThreadPoolExecutor执行的时候,会返回一个Future对象
  • Runable 不可以返回结果,可被执行
  • Callable 可以返回结果,可被执行

runable 可以包装成Callable两种包装方法:

  • public static Callable callable(Runnable task) // 假设返回对象Callable1

  • 2 public static Callable callable(Runnable task, T result) // 假设返回对象Callable2

两种包装方法的区别是
第一种 使用submit 提交给ThreadpoolExecute 提交的时候返回结果为null,第二种会返回结果result对象。

  • 总结:
    一个Executor的工具类,默认的创建线程池的有三种(原理上也是使用ThreadPoolExecuter创建的线程池,不过一些参数设置为了固定值),

  • 1 FixedThreadPool // 可重用固定线程数的线程池

  • 2 singleThreadPool // 单线程的线程
    与上面的唯一差别是线程数量为1

  • 3 cacheThreadPool // 会根据需要创建新线程的线程池

  • 4 ScheduledThreadPoolExecutor 继承自ThreadPoolExecuter //主要用来再指定延迟之后执行任务,或者定期任务,,可以在构造函数中指定多个后台线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值