多线程知识点

volatile 关键字

解决 工作内存(高速缓存)于 主内存(主物理内存)的数据不一致问题,是java虚拟机中提供的一种轻量型的synchronized(同步)机制。

JVM虚拟机中定义了一种内存模型(JMM),用于定义程序中变量的访问规则,存在缓存一致性问题和指令重排问题。

  • 原子性:对于基本类型的读写操作,要么一起成功,要么一起失败,不可被中断。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了该变量的值,其他线程可以立即获得修改的值。
  • 有序性:程序的执行顺序按照代码的编写顺序执行。

特点:

  • 可见性:
    • 使用volatile修饰的成员变量或静态成员变量,在值被修改后会立即写入主内存,保证其他线程使用该变量时,去主内存中获取的是最新值。
    • 使用volatile修饰的变量,当一个线程去修改该值时,其他线程工作缓存中的该变量的缓存值会失效,重新到主内存中获取。
  • 不具有原子性
  • 有序性:禁止指令重排,通过插入内存屏障(lock)禁止内存屏障前后重排指令,内存屏障强制刷出各种cpu的缓存数据,cpu上的线程能读取到这些数据的最新版本。

指令重排:JVM虚拟机中为提高执行效率,不一定会按照代码的编写顺序执行,但保证输出结果于按代码顺序执行的结果一致。处理器进行指令重排会跟据指令之间数据的依赖性。
指令重排不会影响单线程的结果,但会影响多线程并发的正确性。

解决volatile的原子性问题方法:

  • 使用JUC中的原子类(AtmoicXxxx),不需要使用同步手段进行并发控制。
  • 使用synchronized修饰,强制同步机制。

CAS(CompareAndSet)

compareaAndSet:比较并交换,指定内存中的值于某个值进行比较,成功,则进行值的修改,返回true,否则不修改,返回false。compareAndSet(int expect, int update)
CAS中有三个操作数,内存值V,旧的预期值A,要修改的值B,工作内存中的值与主内存中的值比较。
CAS从内存领域来看,属于一种乐观锁,当比较成功则进行交换,否则一直循环比较直至成功。

CAS 属于cpu并发原语,是一条cpu原子操作,不会造成数据不一致问题。Unsafe是CAS的核心类,使用本地narive方法访问,直接操作特定内存数据。

并发原语:并发原语的执行必须是连续的不可中断的。
原子操作可以看作是最小的执行单位,该操作不会被打断,且同一时刻只能有一个线程可以修改内存值,保证线程安全。

CAS缺点:

  • 循环时间长,开销大。
  • 只保证一个共享变量的原子操作。
  • 存在ABA问题。

ABA问题

ABA问题:CAS在进行值的比较并交换过程中,忽略了中间过程中值的变换。
例如:线程1 获取到一个值A,这时线程1 发生阻塞,线程2 拿到同样的值A,将其修改为值B,又改回为值A,线程2 操作结束,线程1 阻塞结束,进行CAS操作,发现期望值与当前值相同,进行修改操作,未发现值A 发生过修改。(A-> B ->A)的过程。

解决ABA问题方法:使用AtomicStampReference一个带有版本号(类似时间戳)的原子类,在每次变更值,都更新版本号,CAS比较时除了比较期望值外,还需要比较该值的版本号。(原子引用+ 修改版本号)

compareAndSet(V   expectedReference,	//期望值
                                 V   newReference,	//更新值
                                 int expectedStamp,	//期望的版本号
                                 int newStamp)	//更新的版本号

集合类线程不安全

当我们使用ArrayListHashMap等集合类时,在单线程情况下不会出现读写错误,但在并发线程中,当并发量达到一定数量时,会出现并发修改异常错误java.util.ConcurrentModificationException
因为并发争抢修改,当一个线程正在写入时,另一个线程过来争抢,导致数据不一致异常。

解决方法:

  1. 使用集合类中的具有线程安全的集合类型,如List中的vector是线程安全的,Map中的HashTable是线程安全的。
  2. 使用Collections集合类的工具类,其中有为集合加锁的方法synchronizedxxx()
  3. 写时复制(CopyOnWrite),当往一个容器中添加元素时,不直接往当前容器中添加,而是将当前容器进行复制,获取到一个新的容器,往新容器中添加元素,添加完元素后,将原容器的引用 指向新容器。这样做的好处就是,时一中读写分离的思想,可以对原容器进行并发的读操作,不需要加锁,因为原容器中没有写操作。

Vector自带线程互斥(synchronized),当多个线程进行读写时会抛出异常,且效率较低。ArrayList允许多个线程读写。
HashSet是set类中的集合类无序的、不能重复、允许为null,hashSet的底层是hashMap,hashSet的add操作中,只需要关注key值,value值是固定的。
Collections是一个工具类,提供了一系列的静态方法,其中包含了对集合中的元素的排序、搜索和线程安全等方法的操作。Coolection是一个集合接口,为集合类提供最大化的统一操作方式。

阻塞队列和同步队列

阻塞队列

阻塞队列(BlockingQueue):创建BlockingQueue时需要声明队列大小,当队列为空时,线程无法获取队列的元素,线程需要等待队列不为空。当队列满时,无法再往队列中添加元素,线程需要等待队列可用。
阻塞队列主要应用场景:生产者-消费者模式,提供安全高效的数据传输。

阻塞队列中的四种处理方法:

  • 队列满时,使用add(e)添加元素会抛出IllegalStateException异常,队列为空时,使用remove会抛出IllegalStateException异常。
  • 队列满时,使用offer(e)添加元素,返回false,队列为空时,使用poll,返回null。
  • 队列为空或满队列情况下,put(e)、take线程都处于等待状态。
方法\处理方式会抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
检查方法element()peek()不可用不可用

同步队列

同步队列(SynchronousQueue):是一个没有缓存区的BlockingQueue,其操作只能是进一个取一个,否则会阻塞。
无法使用peek()来查看队列中是否有元素,也无法遍历队列,队列头元素是一个排队插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲到队列中。

ReentrantLock和synchronized的区别:

  1. synchronized是关键字属于JVM层面,而ReentrantLock是具体类API层面的锁。
  2. 使用方法:
    synchronized不需要手动释放锁,当执行完系统会自动让线程释放对锁的占用。
    ReentrantLock需要手动释放锁,若没有主动释放锁,可能会产生死锁。
  3. 等待是否可中断:
    synchronized 不可中断,除非抛出异常或正常运行完成。
    ReentrantLock 可中断,可以通过设置超时方法、调用interrupt()方法中断。
  4. 加锁是否公平:
    synchronized 非公平锁。
    ReentrantLock 两者都可以,默认是非公平锁,通过构造方法传入boolean值,true为公平锁,false为非公平锁。
  5. 锁绑定多个条件Condition:
    synchronized 无法绑定条件,只能随机唤醒一个线程或唤醒全部线程。
    ReentrantLock 可以实现分组唤醒线程、精确唤醒线程。

各种锁

较详细内容:点这里

可重入锁

可重入锁(ReentrantLock):线程可以进入任何一个它已经拥有锁,所同步着的代码块。
ReentrantLock可以是公平锁也可以是非公平锁,公平锁时,会生成一个等待队列,每个线程根据先后顺序获取锁。非公平锁时,每个线程每次获取锁都需要竞争,不管先后顺序。

理解可参考:点链接

独占锁

该锁一次只能被一个线程所持有。

共享锁

该锁可以被多个线程所持有。

自旋锁

尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,减少线程上下文切换的消耗,但消耗cpu。

读写锁

读写锁(ReentrantReadWriteLock):多个线程同时读取一个资源没有任何问题,满足了并发量,读取共享资源可以同时进行,但是,如果一个线程想去写共享资源,那么就不能再有其它线程对共享资源进行读或写操作。
小结:读-读->能,读-写->不能,写-写->不能

线程池

线程池主要控制运行的线程数量,处理过程中,将任务放入到队列中,然后在线程创建后,启动这些任务。如果线程数量超过了最大数量,超出的线程则排队等候,等待其他线程完成,再从队列中获取执行。
特点:线程复用、控制最大并发数、管理线程。

  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。

产生死锁的原因:

  1. 系统资源不足。
  2. 进程运行推进的顺序不合适。
  3. 资源分配不当。

解决方案:

  1. jps命令定位进程号。
  2. jstack找到死锁进程查看。

java中线程的实现通过Executor框架,该框架中主要的几个类为Executor、Executors、ExecutorServer、ThreadPoolExecutor。

三种线程池实现:

  • newFixedThreadPool(int N):一池N线程,N手动输入。
  • newSingleThreadExecutor():一池一线程。
  • newCachedThreadPool():一次n线程,n为程序根据情况自动创建。

线程池中的7大参数:

public ThreadPoolExecutor(int corePoolSize,	//1.线程池中的常驻核心线程数,当corePoolSize达到最大值时,会将多余的线程放到缓存队列中
                              int maximumPoolSize, //2.线程池能够容纳同时执行的最大线程数,此值必须大于等于1
                              long keepAliveTime, //3.多余的空闲线程存活时间,当线程池中的数量超过corePoolSize时,空闲时间超过keepAliveTime值时,多余的线程会被销毁,直到剩下corePoolSize个线程为止。
                              TimeUnit unit,	//4.keepAliveTime的时间单位
                              BlockingQueue<Runnable> workQueue, //5.任务队列,被提交的但尚未被执行,排队等候
                              ThreadFactory threadFactory, //6.线程工厂,用于创建线程池中的线程,一般采用默认
                              RejectedExecutionHandler handler) //7.拒绝策略,当队列满了,并且工作线程数达到最大值(maximumPoolSize)时,不再接收任务
  1. corePoolSize:线程池中的常驻核心线程数,当corePoolSize达到最大值时,会将多余的线程放到缓存队列中。
  2. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1。
  3. keepAliveTime:多余的空闲线程存活时间,当线程池中的数量超过corePoolSize时,空闲时间超过keepAliveTime值时,多余的线程会被销毁,直到剩下corePoolSize个线程为止。
  4. unit:4.keepAliveTime的时间单位。
  5. workQueue:任务队列,被提交的但尚未被执行,排队等候。
  6. threadFactory:线程工厂,用于创建线程池中的线程,一般采用默认。
  7. handler:拒绝策略,当队列满了,并且工作线程数达到最大值(maximumPoolSize)时,不再接收任务。

4种拒绝策略

在这里插入图片描述

  1. AbortPolicy(默认):直接抛出异常RejectedExecutionException,阻止程序正常运行。
  2. CallerRunsPolicy:“调用者运行”一种调节机制,将任何回退给调用者,不会抛出异常,也不会抛弃任务。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务。
  4. DiscardPolicy:直接抛弃任务,不予任务处理也不抛出异常,如果允许任务丢失,这是最好的一种方案。

合理配置线程池(maximumPoolSize)

  1. 根据CPU密集型:该任务需要大量运算,而没有阻塞,CPU一直全速运行。
    特点:配置尽可能少的线程数量
    计算方式:cpu核数+1个线程 = 线程池中线程数量
  2. 根据IO密集型:并不是一直执行任务,该任务需要大量的IO。
    特点:配置尽可能多的线程数量。
    计算方式:cpu核数/1-阻塞系数(0.8~0.9)
    优缺点:单线程上运行IO密集型的任务会导致,大量的cpu运算能力浪费在等待上。
    IO密集型适用于多线程,可大大加速程序的运行,即使是单核cpu,这种加速主要是利用了被浪费掉的阻塞时间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值