【java并发】Java并发多线程巩固加强(你必须要了解的重点!!)

目录

进程和线程的区别,进程间是如何通信的

什么是线程上下文切换

什么是死锁

死锁的必要条件

Synchronized和lock的区别

什么是AQS锁

为什么AQS使用的是一个双向链表

有哪些常见的AQS锁

sleep()和wait()的区别

yield()和join的区别

线程池的七大参数

Java内存模型(JMM)

保证并发安全的三大特性?

volatile关键字

单例模式双重校验锁变量为什么使用 volatile 修饰?

线程使用方式

ThreadLocal原理

什么是CAS锁

Synchronized锁的原理和优化

如何根据CPU核心数来设计线程池线程数量

cpu密集型(涉及到计算操作时)

IO密集型(涉及到一些读取文件等操作)

AutomaticInteger的使用场景


进程和线程的区别,进程间是如何通信的

进程:系统运行的基本单位,进程在运行过程中是相互独立的,但是线程之间运行可以互相影响

线程:独立运行的最小单位,一个进程包含多个线程并且它们共享同一进程内的系统资源

进程之间通过管道、共享内存、信号量机制、消息队列来进行通信

什么是线程上下文切换

当一个线程被剥夺CPU的使用权时,切换到另外一个线程执行

什么是死锁

死锁指的是多个线程在执行过程中,因资源竞争造成的相互等待的情况

死锁的必要条件

死锁需要满足以下四个条件

  1. 互斥条件:至少有一个资源同时只能被一个进程(线程)占用,即该资源在一段时间内只能为一个进程(线程)所独占。当一个进程(线程)占用了该资源后,其他进程(线程)不能再访问这个资源,只能等待该进程(线程)释放该资源。
  2. 请求与保持条件:一个进程(线程)可以请求获取额外的资源,同时保持已经占有的资源不释放。
  3. 不可剥夺条件:已经分配给进程或者线程的资源,不可以强制性的被剥夺
  4. 循环等待条件:在相互等待资源的过程中,形成了一个闭环

想要预防死锁,只需要破坏其中一个条件即可,比如使用定时锁、尽量让线程用相同的加锁顺序,还可以用银行家算法可以预防死锁 

Synchronized和lock的区别

(1)synchronized是一个关键字,lock是一个类

(2)synchronized在发生异常的时候可以自动释放掉锁,lock则需要手动释放锁

(3)synchronized是可重入锁、非公平锁、不可中断锁

        lock的ReentrantLock是可重入锁、可中断锁,(可以是公平锁,也可以设定为不公平锁)

(4)synchronized是JVM通过监视器实现的,Lock是通过AQS实现的

什么是AQS锁

        AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个抽象类,位于java.util.concurrent.locks包中。AQS提供了一种实现锁和同步器的框架,可用于构建各种类型的同步工具,如ReentrantLock、CountDownLatch、Semaphore等。

        AQS内部通过一个FIFO(先进先出)的队列来管理等待获取同步状态的线程,并提供了一组基本方法来支持子类实现对共享资源的安全访问控制。AQS的核心思想是使用一个整型变量(state)表示共享资源的状态,并通过CAS(Compare and Swap)操作来保证对该变量的原子性操作。

AQS主要提供了以下几个关键方法供子类实现:

  1. acquire(int arg):尝试获取同步状态,如果获取成功则直接返回,否则将当前线程加入等待队列中。

  2. release(int arg):释放同步状态,如果释放后唤醒了等待队列中的其他线程,则通知它们重新尝试获取同步状态。

  3. tryAcquire(int arg):尝试独占方式获取同步状态,成功返回true,失败返回false。

  4. tryRelease(int arg):尝试独占方式释放同步状态,成功返回true,失败返回false。

为什么AQS使用的是一个双向链表

因为有一些线程可能会出现中断的情况,出现这种情况之后就需要从同步阻塞队列中删除掉,这个时候使用双向链表方便删除中间的节点

有哪些常见的AQS锁

AQS分为独占锁和共享锁

ReentrantLock(独占锁):可重入(一个线程在持有锁的情况下,可以再次获取同一个锁而不会发生死锁或其他异常情况——就是可以在一个实现功能的递归函数中重复获取同一个锁),可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队

Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。

应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票

CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。(等待多个子线程完成某个任务,然后再继续执行主线程的下一步操作)

应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行

CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值(一组线程互相等待,直到所有线程都达到某个公共屏障点,然后继续执行下一步操作。)

sleep()和wait()的区别

(1)wait()是Object的方法,sleep()是Thread类的方法

(2)wait()会释放掉锁,sleep()不会释放掉锁

(3)wait()要在同步方法或同步代码块中执行,sleep()方法没有限制

(4)wait()要用notify()或notifyAll()唤醒,sleep()自动唤醒

yield()和join的区别

yield()调用之后线程进入就绪状态

A线程中调用B线程的join(),则B执行完之前A进入阻塞状态

线程池的七大参数

  1. corePoolSize(核心线程数):线程池中最小的线程数量。即使没有任务需要执行,核心线程也会一直存在。核心线程数通常是线程池能够同时执行的最大任务数。

  2. maximumPoolSize(最大线程数):线程池中允许存在的最大线程数量。当任务数量超过核心线程数,并且工作队列已满时,线程池可以创建新的线程,但不会超过最大线程数。

  3. keepAliveTime(线程空闲时间):当线程池中的线程数量超过核心线程数时,多余的空闲线程在被终止之前等待新任务的最长时间。超过这个时间后,多余的线程将被终止,从而减少资源消耗。

  4. unit(时间单位):用于指定keepAliveTime的时间单位,例如毫秒、秒、分钟等。

  5. workQueue(工作队列):用于存储提交的任务的队列。当线程池中的线程都在执行任务时,新任务将被添加到工作队列中等待执行。

  6. threadFactory(线程工厂):用于创建新线程的工厂。可以自定义线程的名称、优先级、是否守护线程等属性。

  7. handler(拒绝策略):当线程池已经达到最大线程数并且工作队列已满时,新任务无法加入线程池的处理方式。常见的拒绝策略有直接抛出异常、丢弃最早的任务、丢弃最新的任务以及在调用者线程中执行任务。

Java内存模型(JMM)

Java内存模型屏蔽了各种硬件和操作系统的内存访问差异,实现java程序在各平台之下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量到主存中读取

         具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的

由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题

保证并发安全的三大特性?

 原子性:一次或多次操作在执行期间不被其他线程影响

可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道

有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排

volatile关键字

        保证变量的可见性和有序性,不保证原子性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。


单例模式双重校验锁变量为什么使用 volatile 修饰?

        当使用双重校验锁(double-checked locking)实现单例模式时,我们需要使用volatile修饰变量。这是因为在多线程环境下,如果不使用volatile修饰变量,可能会导致一个未完全初始化的实例对象被返回。

        具体来说,当一个线程第一次访问单例对象时,如果实例对象还没有被初始化,那么该线程会进入同步块,然后再次检查实例对象是否已经被初始化。但是由于JVM存在指令重排序的优化机制,可能会出现以下情况:

  1. 线程A进入同步块,创建了一个尚未初始化的实例对象,并将引用指向这个对象。
  2. JVM执行指令重排序,将第3步的初始化操作提前到第2步之前,即在实例对象分配内存后就将引用指向了这块内存,但尚未进行初始化。
  3. 线程B也访问单例对象,由于实例对象引用非空,直接返回该对象。
  4. 线程A完成对实例对象的初始化。

这样,线程B拿到的实例对象是未完全初始化的,可能会导致错误的结果。

通过使用volatile修饰变量,可以禁止指令重排序,保证在第2步之前,实例对象已经完全初始化。这样,在线程B访问单例对象时,能够获取到一个正确初始化的实例对象。

线程使用方式

(1)继承Thread类

public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建并启动线程
MyThread thread = new MyThread();
thread.start();

(2)实现Runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();

(3)实现 Callable 接口(带有返回值):

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 线程执行的代码逻辑,可以返回一个结果
        return "Hello, World!";
    }
}

// 创建并启动线程
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread thread = new Thread(task);
thread.start();

// 获取线程的返回结果
String result = task.get();

(4)使用线程池创建线程:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);

// 提交任务给线程池执行
executor.execute(new MyRunnable());

// 关闭线程池
executor.shutdown();

ThreadLocal原理

        原理是为每个线程创建变量副本,不同线程之间不可见,保证线程安全。每个线程内部都维护了一个Map,key为ThreadLocal的实例,value为要保存的副本

        但是使用ThreadLocal会存在内存泄露的问题,因为可以为弱引用,value为强引用,每次gc的时候key都会回收,而value不会被回收。所以为了解决内存泄露的问题,可以每次使用完后删除value或者使用static修饰ThreadLocal,可以随时获取value

什么是CAS锁

        CAS锁是一种乐观锁机制,一个线程在修改变量的时候,需要去判断当前值是否与预期值是一致的,如果与期望值一致则修改成功,不一致则证明其他线程做出了修改,则当前线程的修改失败。

        CAS操作包括三个参数:内存地址(变量)、期望值和新值。CAS指令执行时,将内存地址的值与期望值进行比较,如果相等,则使用新值更新内存地址的值;如果不相等,则操作失败,不做任何修改。CAS操作是以原子方式完成的,即在执行过程中不会被其他线程中断。

CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是

(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作 

(2)CAS频繁失败导致CPU开销大

(3)ABA问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决
 

Synchronized锁的原理和优化

        Synchronized是通过对象头的markword来标明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,性能开销成本比较高,这种锁也就叫做重量级锁。

在JDK1.6之后也引入了偏向锁、轻量级锁来对其优化

偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁

轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会升级为轻量级锁。线程会通过CAS方式获取锁,不会阻塞,提高性能,

重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低

注意,锁只能升级不能降级
 

如何根据CPU核心数来设计线程池线程数量

cpu密集型(涉及到计算操作时)

  • cpu密集型指的是线程处理任务时,cpu参与计算的时间比较多,这种情况下,如果设置的线程数过多,会增加上下文的切换次数,带来额外的开销
  • 线程数的设定公式是:线程数=(cpu核心数+1)

IO密集型(涉及到一些读取文件等操作)

IO密集型是指在处理任务时,IO过程所占用的时间较多,在这种情况下,线程数的计算方法可以分为两种:

  • 第一种配置方式:由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程。
  • 配置公式:CPU核数 * 2。
  • 第二种配置方式:IO密集型时,大部分线程都阻塞,故需要多配置线程数。
  • 配置公式:CPU核数 / (1 – 阻塞系数)(0.8~0.9之间)
  • 比如:8核 / (1 – 0.9) = 80个线程数

AutomaticInteger的使用场景

AutomaticInteger是一个提供原子操作的Integer类,使用CAS+volatile实现线程安全的数值操作

因为volatile禁止了jvm的排序优化,所以它不适合在并发量小的时候使用,只适合在一些高并发的场景中使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java线程编程是指在Java语言中使用多个线程来同时执行多个任务,以提高程序的并发性能和响应速度。Java线程编程PDF是一本介绍Java线程编程的PDF文档,其中包含了Java线程编程的基本概念、原理、技术和实践经验。该PDF文档可以帮助读者快速了解Java线程编程的相关知识,并提供实用的编程示例和案例分析,有助于读者掌握Java线程编程的核心技术和方法。 在Java线程编程PDF中,读者可以学习到如何创建和启动线程、线程的状态和生命周期、线程间的通信与同步、线程池的使用、并发容器等相关内容。同时,该PDF文档还介绍了Java中的并发包(concurrent package)的使用和实现原理,以及多线程编程中的常见问题和解决方案。 通过学习Java线程编程PDF,读者可以深入了解Java线程编程的理论和实践,掌握多线程编程的核心知识和技能,提高自己的并发编程能力,为开发高性能、高并发Java应用程序打下坚实的基础。同时,对于已经掌握多线程编程知识的读者来说,该PDF文档也能够帮助他们进一步巩固和扩展自己的多线程编程技能,提升自己的编程水平和竞争力。 总之,Java线程编程PDF是一本全面介绍Java线程编程的优秀文档,对于Java程序员来说具有很高的参考价值,可以帮助他们在多线程编程领域取得更好的成就。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值