并发编程之并发基础

**重量级锁:**要进入一个同步、线程安全的方法时,是需要先获取这个方法的锁的,退出这个方法的时候,则会释放锁。如果尝试获取锁的时候无法获得,说明有别的线程正持有这个锁,此时我们当前的线程会进入到阻塞状态,等待锁的释放,然后持有锁的线程会把我们从阻塞状态唤醒,我们当前线程再去获取锁。所谓重量级锁就是,获取不到就让当前线程进入阻塞状态的锁。

**自旋锁:**自旋锁不像重量级锁那样,拿不到立刻进入阻塞状态,而是当前线程进行类似于空循环,等待一段时间尝试拿锁。如果循环到一定次数还是拿不到锁,那么就会进入阻塞状态。循环等待的次数可以设置。

**自适应自旋锁:**自旋锁,每个线程尝试获取锁的时候,循环等待的次数是固定的,循环等待完事就进入阻塞状态。但是自适应自旋锁是自己决定循环等待的次数,它会根据之前的经验进行判断,如果之前经常能够获取到这个锁,说明此次拿锁的概率还是比较大的,那么循环等待的次数就会比较大,反之如果是第一次拿这个锁或者平时很少能拿到,循环等待次数就会比较小,减少CPU的消耗。

**轻量级锁:**上面三种锁的共同特点是进入方法之前拿锁,退出方法释放锁。但是如果根本没有线程和他们竞争锁,加锁这个消耗时间和资源的动作就会增加开销,轻量级锁认为当前方法很少会有其他线程来执行,所以它只是给了一个标记,使用CAS机制【保证改变状态这个操作的原子性】,将方法标记为已经有线程在执行或者没有线程执行。当然如果此时有别的线程来竞争了,就会升级为重量级锁了。所以轻量级锁适用于很少出现多个线程竞争一个锁的情况,即多个线程总是错开时间片来获取锁的情况。

**偏向锁:**偏向锁认为轻量级锁每次进入一个方法需要CAS来改变状态,退出后再次进入还是需要CAS比较麻烦,所以偏向锁认为自始自终只有一个线程来执行当前方法,它第一次进入方法时会使用CAS改变状态,然后记录当前线程ID,当前线程退出这个方法,此时不会改变当前方法的状态,然后再次进入的时候判断线程ID是自己的话以及状态是有人在执行的话,就会直接进入方法执行。如果遇到多线程进入方法,其他线程看到线程ID不是自己,此时就会升级为轻量级锁了。所以偏向锁适用于始终只有一个线程执行一个方法的情况。

**悲观锁和乐观锁:**悲观锁就是认为进入方法之前必须先加一个锁,防止多线程,例如重量级锁、自旋锁和自适应自旋锁,乐观锁认为不加锁也没事,等出现了冲突再想办法解决比如说CAS机制和轻量级锁,不会马上加锁

volatile
可见性在多线程环境下,某个共享变量如果被其中一个线程修改了,其他线程能够立刻知道这个变量被修改了,当其他线程读到这个变量时,会去内存中读取而不是从自己的工作空间中读取。

当变量声明为volatile时,在编译为指令的时候会加上一行指令,在寄存器执行一个➕0的空操作,指令前面会有一个lock前缀,处理器遇到lock指令时不会再锁住总线[锁住的话影响多处理器的执行效率],而是会检查数据所在的内存区域,如果该数据是在处理器的内部缓存中,则会锁定此缓存区域,处理完后把缓存写回到主存中,并且会利用缓存一致性协议来保证其他处理器中的缓存数据的一致性。关于缓存一致性,线程所在的处理器会一直在总线上嗅探其内部缓存中变量的内存地址在其他处理器的操作情况,一旦嗅探到了某个处理器打算修改其内存地址的值,而该内存地址也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址无效,所以当该处理器要访问该数据的时候,就会发现自己缓存的数据无效,就会去主存中访问。

另外,volatile保证了有序性,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它后执行。单线程环境中JVM会对我们的代码进行编译优化,再不影响最终结果的前提下,可能会进行指令重排。在多线程环境中,指令重排可能会导致线程安全问题。

class Singleton {
    private static Singleton instance;

    static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 可能的指令重排
                }
            }
        }
        return instance;
    }
}

在这个例子中,如果没有适当的内存屏障(如volatile关键字),new Singleton() 操作可能会被重排,导致 instance 引用在 Singleton 对象完全构造完成之前就对其他线程可见。这可能导致其他线程看到一个尚未完全构造的对象。

synchronized
通过对一个对象进行加锁来实现同步,如下面代码。

synchronized(lockObject){
    //代码   
}

对一个方法进行synchronized声明,进而对一个方法进行加锁来实现同步。如下面代码

public synchornized void test(){
    //代码
}

实际上,两种方法都是对对象加锁,第二种是对实例对象或者Class对象加锁。

被加锁的对象称为锁对象,Java对象在内存中存储结构中有一个mark work,里面存储着锁信息,锁对象刚创建的时候,锁信息中表明它的偏向锁没有生效,直到线程执行到临界区[同步代码块中],会利用CAS,将线程ID插入到markword中,修改偏向锁的标志为,表明偏向锁生效,经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。

当有两个线程来竞争锁的话,偏向锁就会失效,锁膨胀,升级为轻量级锁。首先,偏向锁失效,进行锁撤销,修改markword。如果是自旋锁的话,超过我们设置的循环等待次数就会升级为重量级锁,另外,在遇到长时间的锁竞争、频繁的锁持有者变化或线程数超过处理器数等情况时,会升级为重量级锁。

轻量级锁膨胀之后,也是修改markword的标志位,升级为重量级锁,重量级锁依赖于对象内部的monitor锁,monitor依赖操作系统的互斥锁。当系统检查到锁是重量级锁,就会把想要获得锁的线程进行阻塞,此时阻塞的线程不会消耗cpu,但是阻塞或者唤醒一个线程时,都需要操作系统帮忙,需要用户态内核态之间的转换,转换的开销很大。

从用户态切换到内核态:在用户态执行的线程需要请求操作系统的协助,以便在内核态执行某些操作,比如等待锁的释放。这个过程需要通过一些特定的系统调用(如Linux中的syscall)来触发上下文切换,将线程从用户态切换到内核态。在内核态中,操作系统可以执行一些敏感的任务和操作,比如管理硬件资源和线程的调度。

阻塞等待:一旦线程进入内核态,它会被放置在等待队列中,等待锁的持有者释放锁。在等待期间,线程处于阻塞状态,不会占用CPU时间。

唤醒:当锁的持有者释放了锁,操作系统会从等待队列中选择一个或多个等待的线程唤醒,并将它们移动回用户态。这个唤醒操作也涉及到上下文切换,因为线程需要从内核态切换回用户态。

CAS
CAS它依赖于底层硬件的原子指令,这些指令可以确保在一个时钟周期内执行比较和更新操作,而不会被中断。保证原子执行。
由于CAS操作是原子的,所以可以保证只有一个线程能够成功修改共享变量,其他线程会根据比较的结果进行相应的重试或者放弃操作。这可以避免使用锁造成的上下文切换和开销,提高了并发性能。
Java中也有CAS的Atomic原子类。

虽然这种 CAS 的机制能够保证increment() 方法,但依然有一些问题,例如,当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,之后又马上把 i 的值减 1,然后,线程 A 执行第三步,这个时候线程 A 是认为并没有人修改过 i 的值,因为 i 的值并没有发生改变。而这,就是我们平常说的ABA问题。

对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。

为了解决这个 ABA 的问题,我们可以引入版本控制,例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不同,这样,我们就可以预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就可以进行版本控制了。通过使用版本号或标记,CAS操作不仅会比较值是否相等,还会比较版本号或标记是否一致,从而更安全地处理共享变量的更新。

死锁:

public class DeadLock {
    public static void main(String[] args) {
        Object resource1 = new Object();
        Object resource2 = new Object();

        Thread t1 = new Thread(()->{
            synchronized(resource1){
                System.out.println("t1 holding resource1");
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("t1 waiting for resource2");
                synchronized(resource2){
                    System.out.println("t1 get resource2");
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized(resource2){
                System.out.println("t2 holding resource2");
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("t2 waiting for resource1");
                synchronized(resource1){
                    System.out.println("t1 get resource1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

线程池:
在这里插入图片描述

在这里插入图片描述

  1. 线程池中的线程是越多越好吗?
    线程池是一组可用于执行任务的线程,以提高应用程序的性能和资源利用率。

当我们考虑线程池大小时,要考虑的因素,包括CPU核心数、内存限制、可用的硬件资源等。当然线程池的大小应该根据任务类型而变化。例如,对于计算密集型任务,可以选择较少的线程,而对于IO密集型任务,可能需要更多的线程。

而如果需要更快的响应时间,可能需要增加线程池大小以处理更多的并发请求。

线程池大小通常不是一成不变的,而应该采用动态调整的策略,根据实际负载和系统资源来进行调整。可以提到一些常见的线程池调整策略,如自适应调整、动态增加或减少线程等。

线程池的管理不仅仅是选择初始大小,还包括监控和调优。可以使用性能测试和监控工具来确定最佳的线程池大小,并随着时间的推移进行调整。

在配置线程池大小时需要注意避免死锁和饥饿的情况,例如,确保任务的依赖关系和线程池的工作方式不会导致线程池不稳定。

订单处理任务是IO密集型任务,因为它们需要与数据库交互和发送电子邮件等IO操作。通常,IO密集型任务可能需要较多的线程以便在IO操作等待期间不浪费CPU资源。因此,你可以考虑设置线程池大小为核心数的2倍,即16个线程。电商平台对订单处理的响应时间要求非常高,希望订单能够尽快处理完毕。由于订单数量可能会波动,你可以实现动态线程池大小调整的策略。当订单数量增加时,可以动态增加线程池大小,以应对高峰时期的订单处理需求,然后在订单量减少时缩减线程池大小,以释放资源。

在线社交媒体分析工具,该工具用于分析社交媒体平台上的大量帖子和评论,以获取有关特定主题的见解。帖子分析任务是CPU密集型任务,因为它们需要进行复杂的文本分析。在这种情况下,你可以选择配置线程池的大小为CPU核心数的1.5倍,即24个线程。

  1. 为什么线程池可以加快速度?
    线程重用:线程池会在初始化时创建一定数量的线程,并将它们保持在池中。当有任务需要执行时,线程池会分配一个可用的线程来执行任务,而不是每次都创建一个新线程。这避免了线程的频繁创建和销毁,减少了系统开销。

线程池大小控制:线程池可以根据应用程序的需求和系统资源来配置合适的线程池大小。通过控制线程池的大小,可以有效地平衡并发执行的任务数量和系统资源的使用,避免资源过度消耗。

避免资源竞争:在多线程环境下,线程之间可能会竞争资源(如CPU时间、内存等),导致性能下降。线程池可以通过合理分配任务和线程,以减少资源竞争,提高系统的并发性能。

任务排队:线程池通常具有任务队列,用于存储等待执行的任务。当所有线程都在执行任务时,新的任务会被放入队列中等待执行。这可以确保任务不会被丢失,并且能够按照先进先出的顺序有序执行。

减少上下文切换:线程池中的线程通常会重复执行多个任务,减少了线程切换的频率。线程切换是一项开销较大的操作,线程池通过减少线程的创建和销毁,降低了上下文切换的开销,提高了性能。

控制资源占用:线程池可以限制同时执行的任务数量,防止资源被过多占用,从而确保系统的稳定性。这对于避免资源耗尽和系统崩溃非常重要。

AQS:
AQS(AbstractQueuedSynchronizer)是Java中用于构建锁和同步器的基础框架,它提供了一种灵活的方式来实现各种同步机制。AQS是Java并发包(java.util.concurrent)中很多同步类的基础,包括ReentrantLockSemaphoreCountDownLatch等。

AQS的核心思想是通过一个队列来管理等待获取共享资源的线程,以及通过状态来管理共享资源的访问。AQS的关键方法包括acquirerelease,它们分别用于获取和释放共享资源。

AQS的主要特点和概念包括:

  1. 状态(State):AQS维护一个状态变量,表示共享资源的状态。状态可以是任意整数值,具体含义由具体的同步器类来定义。例如,对于ReentrantLock,状态表示锁的持有次数。

  2. 独占锁(Exclusive Lock):AQS支持独占锁,其中只有一个线程可以同时获得锁。ReentrantLock就是一个典型的独占锁。

  3. 共享锁(Shared Lock):AQS还支持共享锁,其中多个线程可以同时获得锁。SemaphoreCountDownLatch是共享锁的例子。

  4. 等待队列(Wait Queue):AQS使用一个FIFO队列来管理等待获取资源的线程。等待队列中的线程会按照先进先出的顺序获取资源。

  5. acquire方法acquire方法用于获取共享资源,它可以被独占锁和共享锁使用,具体实现由同步器类决定。acquire方法通常包括自旋和阻塞两种获取资源的方式。

  6. release方法release方法用于释放共享资源,它也可以被独占锁和共享锁使用。释放资源后,会唤醒等待队列中的某个线程,让它可以继续执行。

  7. AQS提供的核心方法:AQS还提供了一些核心方法,如tryAcquiretryReleasehasQueuedThreads等,可以供同步器类重写,以实现自定义的同步逻辑。

总之,AQS是Java中实现锁和同步机制的核心框架,它提供了强大的工具,帮助开发人员构建高效且灵活的同步组件。通过使用AQS,开发人员可以更容易地构建自定义的同步器,满足各种多线程编程需求。

ReenTrantLock:
可重入锁:可重入锁允许同一个线程多次获得同一个锁,而不会出现死锁。这是通过记录锁的持有次数来实现的。每次成功获取锁时,持有计数会加1,每次释放锁时,计数会减1。只有当持有计数降为0时,其他线程才能获得锁。

公平锁:做个比喻就是在银行门口等待的人,先来的,等下可以先获取到锁来办理事情。即在锁的获取上,是按照时间的顺序公平获取的。

非公平锁:和公平锁相反,慢来的也有可能先获取到锁。

在 ReenTrantLock 中,通过调用 lock() 方法来获取锁,调用 unlock() 方法来释放锁的机制进行代码块的同步。AQS 使用一个整型的 volatile 变量(名为 state)来维护同步状态,而这个变量的操作是靠 CAS 机制来保证他的原子性。

默认情况下,ReenTrantLock 使用的是非公平锁,我们也可以通过构造器指定是否要公平锁。

CountDownLatch:
一个计数器被初始化为一个正整数,当某个操作完成时,计数器的值减一,当计数器的值变为零时,等待的线程被唤醒,可以继续执行。

await():当一个或多个线程需要等待某个操作完成时,它们可以调用await()方法来等待。如果计数器的值不为零,await()方法将会阻塞线程,直到计数器的值变为零为止。

countDown():当某个操作完成时,可以调用countDown()方法来减少计数器的值。通常在执行完成的地方调用此方法。每次调用countDown()都会将计数器的值减一。依靠 CAS 机制来实现计数减一。

CyclicBarrier:
Barrier 是阻拦的意思,他和 CountDownLatch 有点类似,当指定的线程数都执行到某个位置的时候,他才会继续往下执行。主要特点是可以让几个线程相互等待,就像被一道围栏给阻塞了一样,等到人齐了,在一同往下执行。

Semaphore:
semaphore 是信号量的意思,通过这个类,可以控制控制某个资源最多可以被几个线程所持有。例如我们平时去银行办理一些事情,银行只有 3 个窗口,那么最多可以有 3 个人在办理事情了,其他人只能等待别人办理好才能上去办理。对于这种需求,就可以使用这个 semaphore 线程类了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值