并发编程学习心得

并发安全的唯一条件:存在且只有写后读生效

git也是一种并发·

并发出现的问题,原子性、可见性、顺序性

缓存带来的可见性问题,用volatile合理的禁用缓存

原子性是线程间切换抢占共享资源带来的问题 加锁解决

顺序性是指令重排序

volatile

能保障可见性

1、可见性:能保证在多线程的情况下,其他线程能读取到这个线程修改后的数据,相当于加了内存屏障。也就是能保障写后读,可见性正是依靠缓存一致性、内存屏障等机制协同运作来保证的

除了偏向锁,JVM实现锁的方式底层都用了CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

2、volatile是轻量级锁,因为volatile只能保证读的安全,但是不能保证写的安全性。默认读写都是不安全的,这里只是加了上读的保障,所以当volatile修饰的变量,我们在多线程下使用for循环进行i++操作,得到的结果是不对的,这是线程不安全的

如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度

3、volatile只能保障读的结果正确

**4、volatile能防止指令重排序 ** 在单例模式那里有体现

重排序是又CPU流水线技术导致的

​ 1、volatile修饰变量的时候,防止指令重排序在代码方面防止的代码是前后的

​ 2、 防止一行代码的指令重排序

  • 分配内存空间。
  • 初始化对象。
  • 将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  • 分配内存空间。
  • 将内存空间的地址赋值给对应的引用。
  • 初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来

volatile原理

有volatile变量修饰的共享变量进行写操作的时候,JVM就会向处理器发送一条Lock add的指令。相当于一个内存屏障

StoreLoad 屏障
执行顺序: Store1—> StoreLoad—>Load2
确保Load2和后续的Load指令读取之前,Store1的数据对其他处理器是可见的。

Lock前缀的指令在多核处理器下会引发了两件事情,1、CPU操作完数据后,放入高速缓存的缓存行(本地内存),然后再把缓存行里面的数据放回到主内存中。

2)这个写回内存的操作会锁住这片缓存,使在其他处理器里缓存了该内存地址的数据无效。就是比如有多个线程都加载了同一个数据(带着内存地址),当一个线程获取修改这个数据,这个线程的处理器会锁住这片内存, 独占这片内存,使在其他CPU里缓存了该内存地址的数据无效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。

锁缓存的意思就是多个线程都获取这个变量了,当一个线程操作这个变量的时候,其他的线程缓存了该内存地址的数据都会无效,不让操作这个变量了,·目的是为了写后读,不锁总线是因为总线是CPU通向内存的唯一通道,开销太大

volatile优化

追加字节到64MB

一个一个对象的引用占4个字节(地址占32位,4字节),它追加了15个变量(共占60个字节),再加上父类的value变量,一共64个字节

新增LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的头节点和尾节点,如果不填充,头尾结点有可能加载到同一个缓存行,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队出队效率。追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定

那么是不是在使用volatile变量时都应该追加到64字节?

当缓存行非64字节宽的处理器。

共享变量不会被频繁地写的时候不用加载到64MB

为什么volatile是轻量级锁,能保障读的安全?

这段代码和下面的代码时效果是等价的,就是volatile修饰的变量相当于给get和set方法加上了synchronize锁,就是当进行set写操作的时候,锁住了这个变量,也就是锁住了缓存行,别的线程不能读取,只有当写完,别的线程才能get()读取,保障了写后的数据能正确读到。

但这只是效果上的等价,因为volatile锁住的是缓存行和总线,而synchronize锁存在于对象头中,锁住的是内存

class VolatileFeaturesExample {
	volatile long vl = 0L; // 使用volatile声明64位的long型变量 
    
	public void set(long l) {
		vl = l; // 单个volatile变量的写 
	}
	public void getAndIncrement () {
		vl++; // 复合(多个)volatile变量的读/写 
	}
	public long get() { 
		return vl; // 单个volatile变量的读 
	} 
}
class VolatileFeaturesExample { 
    long vl = 0L; // 64位的long型普通变量
    public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步 
        vl = l; 
    }
    public void getAndIncrement () {  // 普通方法调用      
        long temp = get();  // 调用已同步的读方法       
        temp += 1L; // 普通写操作         
        set(temp);  // 调用已同步的写方法      
    }
    public synchronized long get() {  // 对单个的普通变量的读用同一个锁同步
        return vl; 
    } 
}

缓存命中

先从缓存找,缓存没有就会去内存中查询,从缓存中查询查到了就叫缓存命中。缓存就是把数据存到离我们更近的地方,

写命中:当一个值从内存加载到CPU的高速缓存的缓存行中,然后CPU从缓存行调取并修改数据,修改数据后需要根据内存地址再加载到CPU高速缓存中,如果此时CPU高速缓存中仍然存在这个值的内存地址,这就叫写命中,写命中针对的是在高速缓存中的操作

写缺失:当CPU修改完一个值后需要再次根据内存地址把值加载回内存中,但如果内存中这个值的内存区域被垃圾回收了,不存在了,这就叫写缺失,写缺失针对的是在内存中的操作

嗅探技术:处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充

CAS:字义:比较并且交换

CAS是一种无锁的非阻塞算法的实现。

​ 就是在多线程运行的时候,在执行的任何阶段都有可能导致线程的切换,最终导致结果被覆盖的问题,导致并发

​ 解决方案:就用到了CAS,

​ 1、加版本号,当线程运行的时候,对比版本号,版本号一致,版本号+1,然后如果线程切换后,但是版本号对不上,也不会执行,当再次轮到这个线程后,版本号对上了,才会继续执行,这是最稳定的

​ 2、不加版本号,就是当线程执行后,记录初始的值,然后执行线程,准备更新,当初始值对上了的时候,更新。然后当切换线程后,往回更新,初始值对不上的时候,把当前的线程执行的结果赋值给这个线程,然后继续切换线程去执行,只有当记录的初始值对上了之后,才会改变,然后更改初始值,但是会有ABA问题,但是性能比加版本号的好,但是在业务方面有影响


**java锁(volatile,synchronize)的底层是CAS,**除了偏向锁(只有一个线程),JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

CAS锁的底层是操作系统的总线锁或者缓存锁,但是java没有办法操作总线锁和缓存锁,只能是java底层通过处理器提供的LOCK信号调用操作系统的总线锁和缓存锁,

CAS内部实现原理: 两个线程t1,t2都读到了变量s=0和记录它的版本号变量v=1,每次操作更改的时候先对比版本号,版本号一致后再进行更改。CPU和内存之间的交互是通过总线进行的,总线的宽度36-41位,一次只能传输一个变量,所以变量s和记录它的版本号变量v是不能同时传输到CPU的,这样分批次传输的话,可能版本号v传输后,总线让给了其他的变量,没有让s继续传输,这样就会导致出错。 所以CAS操作的时候,底层其实也是给总线或者缓存行加锁了,能让变量和它所携带的版本号陆续通过总线,所以CAS操作离不开操作系统的总线锁和缓存锁


缺点

面试:ABA问题:

不加版本号的会有ABA问题。当一个线程执行完后,切换线程的时候又被这个线程抢到了,然后这个线程执行后又改回了原来的值。然后再切换线程,这时这个线程不知道这个值被其他改变过了。一些需要做记录的就会受到影响。ABA解决方案就是加版本号

循环开销大 :CAS循环(自旋)自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,

​ 但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。或者把多个变量合 并成一个变量操作

可以通过这两个方式解决这个问题:
​ 1、使用互斥锁来保证原子性;
​ 2、将多个变量封装成对象,通过AtomicReference来保证原子性 。

重排序

CPU流水线导致指令重排序

CPU流水线:为了就是节约电路来回切换所浪费的时间,假设5个功能执行10次,电路回来切换就切换了50次,但是如果每个功能都一次性执行完,就只需要执行五次,这样效率大大提高。

volatile和final能防止指令重排序

内存屏障

读写能分出四种排列组合

读后读 读后写 写后写 写后读。。其中所有的处理器都支持写后读的规则

屏障 ,加了屏障之后禁止指令重排序,屏障就是某种约定,见到某个标记就禁止重排序,约定的专业术语其实就是协议。

内存屏障具体实现就是加volatile、final,synchronize,它们不能防止指令重排序,但是synchronize能保证结果是正确的。有读后读屏障,读后写屏障,写后写屏障,写后读屏障。加上哪种屏障就能保障禁止指令重排序。

StoreLoad Barriers写后读屏障是一个“全能型”的屏障,所以一般在多线程下,保障写后读,多线程下就能得到正确的结果

int f= 3;int s= f; int h = u; 后面两步属于读读操作

int h = 8; int g = 7; 这属于是写写操作

final

final和volatile能防止指令重排序,1、防止上下行代码的指令的重排序

​ 2、防止单行代码的重排序,产生句柄、分配内存空间,初始化对象、再指向内存空间的顺序固定,不会出现先指向了内存,后初始化了对象,也就是有了对象,但是还没指向内存空间出现null的情况

阻止构造函数的溢出

synchronized

概念

非公平锁、悲观锁、独占锁

volatile的当一个线程抢占一个变量的时候,其他的线程也可以读,但是不能进行写操作。保障的是可见性,通过主内存进行一个线程间的通信

synchronize是重量级锁,当一个线程抢占到一个变量的时候,其他的线程连读都不能读,其他线程会进入到阻塞队列中,这样的话就不能并发

执行了,保证线程间的同步,让他们一个一个的执行,释放锁后会给其他线程发送通知。主要也是保障写后读,也可以保障原子性

synchronized还会创建-一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证操作的内存可见性,同时也使得

这个锁的线程的所有操作都happens-before 于随后获得这个锁的线程的操作。完全可以替代volatile

并发安全的唯一条件:存在且只有写后读生效

都能锁什么

synchronized 只能对引用类型加锁,对基本类型不管用

可以锁变量,但是不能修饰变量

同步(锁住)普通方法—锁是当前实例对象(锁实例对象)

对于静态方法—锁是整个程序,也就是Class对象(锁住类)

对于同步方法块,锁是Synchonized括号里配置的对象。

注意:

synchronized锁只能锁引用类型的变量

synchronized(引用类型变量){代码块}—代码块执行完毕才 会释放这个变量的锁

加锁:当此线程对资源锁住的时候,别的线程不能对这个资源进行读写。

synchronized锁,即便让出了cpu(到了时间片进行上下文切换)也不会丢掉锁,下次切换回来的时候还是存在,只有代码块执行完才会丢掉。

synchronize锁住的是内存,就比如对象所占的内存,而volatile锁住的是缓存行。

修饰非静态方法时会对对象加锁,锁住的只是这个对象中被synchronize修饰的资源

修饰静态的方法会对**类加锁,**锁住的只是这个类中被synchronize修饰的资源

对象锁之间是有冲突的,不能同时执行同一个对象的方法类锁之间也是有冲突的,同一个类的静态方法不能同时执行,即使是对象调用的静态方法,它仍然属于类锁

类锁和对象锁互不干扰,类锁是在方法区加锁,对象锁是在堆中加锁

加锁的和不加锁也互不影响

synchronize锁的原理

⽽⽤synchronized的关键是建⽴⼀个监视器monitor的对象,任何对象都有一个monitor,当且一个monitor被持有后,它将处于锁定状

态,这个monitor可以是要修改的变量,也可以是其他⾃⼰认为合适的对象(⽅法),然后通过给这个monitor加锁来实现线程安全,

JVM基于进入和退出Monitor(监视器锁)对象来实现方法同步和代码块同步,是使用monitorentermonitorexit指令实现的,大概意思就是一

段指令,有起始位置**(monitorenter)和结束位置(monitorexit**),在这个范围内的指令都被加锁。

进入Monitor和结束Monitor的流程

  • 如果monitor的进入数为0,则该线程进入monitor,然后将里面的count参数设置为 1,该线程即为 monitor的所有者。
  • 如果其他线程己经占用了monitor,则该线程进入阻塞状态,直到 monitor的进入数为 0,再重新尝试获取 monitor 的所有权。

synchronize底层是AQS,AQS中有CAS自旋

每个线程在获得这个锁之后,要执⾏完加载到释放的一个过程。才会释放它得到的锁。这样就实现了所谓的线程安全

synchronized锁住的位置

synchronized用的锁是存在Java对象头里的,锁住的内存,volatile锁住的是缓存行

我们对一个对象加锁,不能直接加锁,一般是通过对方法或者代码块加锁然后进而对 对象加锁 。

它的状态的变化和对象头里的Mark Word相关,比如偏向锁的实现是通过控制对象Mark Word的标志位(markword的初始状态1|01)来实现的我们访问对象头

比如里面的4bit,代表着对象的年龄,4bit表示有着2^4种也就是16种变化,所以说一般年轻代最多活15岁

synchronized优化

(1)减少synchronized的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。单位时间内执行的线程就会变多,等待的线程变少。

(2)降低 synchronized锁的粒度

将一个锁拆分为多个锁提高并发度,尽量不要用类名点class来创建锁。

(4)读写分离

读取时不加锁,写入和删除时加锁

synchronized和Lock的区别?

Lock是显示锁,需要手动开启和关闭。synchronized是隐士锁,可以自动释放锁。
Lock是一个接口,是JDK实现的。synchronized是一个关键字,是依赖JVM实现的。
Lock是可中断锁,synchronized是不可中断锁,需要线程执行完才能释放锁。
发生异常时,Lock不会主动释放占有的锁,必须通过unlock进行手动释放,因此可能引发死锁。synchronized在发生异常时会自动释放占有的锁,不会出现死锁的情况。
Lock可以判断锁的状态,synchronized不可以判断锁的状态。
Lock实现锁的类型是可重入锁、公平锁。synchronized实现锁的类型是可重入锁,非公平锁。
Lock适用于大量同步代码块的场景,synchronized适用于少量同步代码块的场景。

为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁

为了线程安全吧,wait会释放对象锁,sleep不会”,既然要释放锁,那必然要先获取锁。

也是和synchronized获取Monitor(监视器)和释放Monitor的流程有关

当多个线程同时访问同步代码块时,首先会进入到EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,同时count加1,若发现之前的owner的值就是指向当前线程的,recursions也需要加1。如果CAS尝试获取锁失败,则进入到EntryList中。
当获取锁的线程调用wait()方法,则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒。
当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1。当recursions的值为0时,说明线程已经释放了锁。

这就是为什么wait()、notify()等方法要在同步方法或同步代码块中来执行呢,这里就能找到原因,是因为wait()、notify()方法需要借助ObjectMonitor对象内部方法来完成。

synchronize和lock的区别

1、synchronize是java中的关键字,而Lock是接口,它下面有很多的实现类。
2、synchronize会自动释放锁,而lock需要‘手动’释放。
3、synchronize不知道线程有没有获取到锁,而lock能知道。
4、synchronize是非公平锁,而lock可以是公平锁,也可以是非公平锁。
5、synchronize等待不中断,而lock等待可中断。
6、synchronize可以锁对象、类、代码块,而lock锁住的是代码块。

由于Lock是由JDK实现的,所以不像synchronize锁的获取和释放都是由JVM控制的,Lock的获取和释放都需要手动进行,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生

ReentrantLock

ReentrantLock

可重入锁又称之为递归锁,当一个线程获取对象锁之后,这个线程可以再次加锁,而其他的线程是不可以的。A线程加锁,调用B线程,B线程也加锁,B线程里调用A线程,递归调用。

**加几次锁,就必须释放几次锁。**递归一次加一次锁,state变量的值就+1,每次释放一个锁,state变量的值就-1,直到记录值变为0,才释放锁。

可重入锁的意义之一在于防止死锁

可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁,能自己控制锁的开关

底层实现原理:依赖AQS中volatile修饰的state变量,

ReentrantLock中,它对AQS的state状态值定义为线程获取该锁的重入次数,state状态值为0表示当前没有被任何线程持有,state状态值为1表示被其他线程持有,因为支持可重入,如果是持有锁的线程,再次获取同一把锁,直接成功,并且state状态值+1,线程释放锁state状态值-1,同理重入多次锁的线程,需要释放相应的次数。

ReentrantLockLock的实现都是基于Sync组件来做的,

Sync承包了所有事情,因为Sync上有AbstractQueuedSynchronizer下有NonfairSyncFairSync两小弟可差遣,所以成ReentrantLock的利器也合情合理。

ReentrantLock分为公平锁和非公平锁,一般用非公平锁

​ 公平:对于任务调度需要排队

​ 调用公平锁的lock方法

​ 调用AQS的acquire方法 尝试获取资源

​ 调用ReentrantLock的tyrAcquire方法,

​ 非公平:不需要排队,可能需要多写代码,体现在锁释放后,在阻塞队列中的线程竞争锁的时候

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread(); //获取当前线程
    int c = getState();//获取锁的状态
    if (c == 0) {  //当没有锁时
        if (isFirst(current)  &&  //判断是不是第一个线程,因为是公平锁,所以需要判断是不是按序加锁
            compareAndSetState(0, acquires)) { //CAS比较并且交换
            setExclusiveOwnerThread(current);//设置锁
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) { //如果有锁的话,就是可重入锁,可重入锁就是在加锁时判断当前线程是否是已经获												取到锁的线程,如果是的话,锁的次数会+1。需要注意的是既然多次加锁,就需要多次释放锁
        int nextc = c + acquires;	
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

AQS

什么是AQS

reentrantLock和synchronize的底层是AQS,AQS的实现中包含CAS

AQS(抽象队列同步器)是一个框架,是队列同步器,提供了一种实现阻塞锁和依赖FIFO等待队列的同步器,

AQS的核心由一个先进先出的同步队列(CLH)和一个volatile修饰的state变量组成。volatile的int类型的state表示同步状态,通过内置的FIFO(先进先出)队列CLH完成资源获取的排队工作,将资源封装为Node,通过CAS改变state值,

AQS可以通过CAS对state变量进行修改,队列同步器提供了getState和setState和CAS方法,一般来说,state为0时表示无锁状态,state大于0时表示有线程获得锁。

从代码上看,如果我们调用lock方法是,触发Acquire方法,该方法又会去调用tryAcquire方法以cas的方式尝试获取锁,如果获取失败,就调用addWaiter方法把当前线程包装为Node对象添加到阻塞队列中。然后调用acquireQueued方法通过自旋去获取锁。

CLH队列数据结构

:一个以Node为节点实现的链表的队列

当前线程获取同步状态失败,同步器将当前线程机等待状态等信息构造成一个Node节点加入队列,放在队尾,同步器重新设置尾节点

加入队列后,会阻塞当前线程同步状态被释放并且

同步器重新设置首节点,同步器唤醒等待队列中第一个节点,让其再次获取同步状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cOI6TxXa-1670228377073)(…/img/image-20220902170532260.png)]

队列中节点状态值(waitStatus,只能为以下值) 线程获取或释放锁的本质是去修改AQS内部那个可以表征同步状态的变量的值

//常量:表示节点的线程是已被取消的
    static final int CANCELLED =  1;
    //常量:表示当前节点的后继节点的线程需要被唤醒
    static final int SIGNAL    = -1;
    //常量:表示线程正在等待某个条件
    static final int CONDITION = -2;
    //常量:表示下一个共享模式的节点应该无条件的传播下去
    static final int PROPAGATE = -3;

线程获取或释放锁的本质是去修改AQS内部那个可以表征同步状态的变量的值。比如说,我们创建一个ReentrantLock的实例,此时该锁实例内部的状态的值为0,表征它还没有被任何线程所持有。当多个线程同时调用它的lock()方法获取锁时,它们的本质操作其实就是将该锁实例的同步状态变量的值由0修改为1,第1个抢到这个操作执行的线程就成功获取了锁,后续执行操作的线程就会看到状态变量的值已经为1了,即表明该锁已经被其他线程获取,它们抢占锁失败了。这些抢占锁失败的线程会被AQS放入到CHL队列里面去维护起来

AQS设计模式

AQS同步器的设计是基于模板方法模式的**,如果需要自定义同步器一般的方式是这样**

1.使用者继承AQS并重写指定的方法。(这些重 写方法很简单,无非是对于共享资源state的获取和释放)

2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

子类只需要通过重写以下方法来控制AQS内部的一个叫做state的同步变量:

protected boolean tryAcquire(int arg)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
    
protected boolean tryRelease(int arg)//独占方式。尝试释放资源,成功则返回true,失败则返回false。

protected int tryAcquireShared(int arg)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
    
protected boolean tryReleaseShared(int arg)//共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

protected boolean isHeldExclusively()//调用该方法的线程是否持有独占锁,一般用到了condition的时候才需要实现此方法

在重写这些方法时,如果想要使用state同步变量,必须使用AQS内部提供的以下方法来控制:比如CAS

/** 返回同步状态的当前值(此操作具有volatile变量的读语义) **/
protected final int getState() {  //方法被final修饰,不允许被重写
        return state;
}
 /** 设置同步状态的值(此操作具有volatile变量的写语义) **/
protected final void setState(int newState) { //方法被final修饰,不允许被重写
        state = newState;
}
/**
 * 原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(此操作具有volatile变量的读写语义)
 * @return  成功返回true,失败返回false,意味着当操作进行时同步状态的当前值不是expect
**/
protected final boolean compareAndSetState(int expect, int update) { //方法被final修饰,不允许被重写
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS支持抢占独占锁和共享锁:

独占锁:同一个时刻只能被一个线程占有,ReentrantLockReentrantReadWriteLock.WriteLock,分为公平和非公平

​ 独占锁需要实现的方法是 tryAcquire(int)、tryRelease(int)

​ 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

​ 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadWriteLock.ReadLockCountDownLatchCyclicBarrierSemaphore

共享锁需要实现的方法是 tryAcquireShared(int)、tryReleaseShared(int)

AQS的所有子类中,要么使用了它的独占锁,要么使用了它的共享锁,不会同时使用它的两个锁。

锁的类型

乐观锁和悲观锁

悲观锁一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了

synchronizedReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。

乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据

​ 乐观锁可以使用版本号机制CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的

独占锁和共享锁

独占锁

独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

JDK中的synchronizedjava.util.concurrent(JUC)包中Lock的实现类就是独占锁。

共享锁

共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。

互斥锁和读写锁

互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待

读写锁是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。

读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。

公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁

非公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。

可重入锁

可重入锁又称之为递归锁,当一个线程获取对象锁之后,这个线程可以再次加锁,而其他的线程是不可以的。A线程加锁,调用B线程,B线程里调用A线程,递归调用。

**加几次锁,就必须释放几次锁。**递归一次加一次锁,记录的值就+1,每次释放一个锁,记录值就-1,直到记录值变为0,才释放锁。

可重入锁的意义之一在于防止死锁

Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。

自旋锁

自旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。

分段锁

分段锁 是一种锁的设计,并不是具体的一种锁。

分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

JDK1.7中的ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成,即
ConcurrentHashMap把哈希桶切分成小数组(Segment) ,每个小数组有n个HashEntry组成。
其中,Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色; HashEntry
用于存储键值对数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vCpsgL8w-1670228377074)(…/img/image-20220906194823391.png)]

锁升级(四种)

低并发轻量锁,高并发重量级锁,无并发偏向锁

随着竞争情况逐渐升级。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,说的都是synchronized的状态,这几个状态会。锁可以升级但不能降级

无锁 0|01
偏向锁 1|01
轻量级锁 00
重量级锁 10

无锁

就是乐观锁,markword的初始状态0|01

偏向锁

偏向锁适合没有并发的时候,就一个线程

当一个线程访问的时候并不知道有没有其他线程,所以加锁是安全的,但是需要提前做充足准备,准备阻塞队列等等,这对CPU的消耗是非常大

的,所以就设置偏向锁当一个线程设置偏向锁的时候,它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,

不存在多线程竞争的情况,那么线程是不需要重复获取锁的。

实现原理

偏向锁的获取流程:

1、检查对象头中Mark Word(**markword的初始状态1|01)**是否为可偏向状态,如果不是则直接升级为轻量级锁。

2、如果是,判断Mark Work中的线程ID判断threadId是否与其线程id一致,如果是,则执行同步代码块。就没有必要去做太多的准备了,省去CPU和内存的消耗

3、如果不是,则进行CAS操作竞争锁,如果竞争到锁,则将Mark Work中的线程ID设为当前线程ID,执行同步代码块。

​ 当其他线程尝试竞争偏向锁时,等待一个安全点,持有偏向锁的线程就会释放锁,把对象头设置成无锁状态或者重新偏向其他线程

4、如果竞争失败,升级为轻量级锁。

轻量级锁

当低并发下真正出现竞争了,就需要使用轻量级锁了,当一个线程获取了一个变量,其他的线程就不能竞争了,等待这个线程释放。

但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。

优点就是线程不会阻塞,但是当线程释放了,其他线程怎么能马上知道这个线程释放了呢?需要自旋锁,就是死循环每隔一段时间过来访问,然后能第一时间竞争变量。

缺点当一个线程执行的时候,其他线程一直自旋就会浪费CPU的资源,导致需要执行的任务执行起来很慢,所以自旋只适合低并发,不适合高并发

加锁原理

轻量级锁加锁的的前提是无锁或者不能带有偏向条件。

1、无锁可以直接加锁,关于markword的初始状态0|01
1 在当前线程的栈帧中创建锁记录
2 将锁对象中的markword复制到锁记录中
3 通过CAS方式将markword设置成指向锁记录的指针。
2、有锁状态下
1如果是当前线程持有的轻量级锁,说明是可重入锁,由于每次获取轻量级锁都会创建一个锁记录,所以,除第一次锁记录存储markword外,后面均设置为null。
2如果不是当前线程持有的锁,说明出现锁竞争,可能需要锁升级。
解锁
使用CAS原子的将锁记录中的markword复制到锁对象中,如果成功,则代表解锁成功。如果失败则膨胀成重量级锁之后在解锁。
关于锁重入解锁
如果判断Displaced Mark Word == null,则代表是可重入锁的解锁,makword不需要复制到锁对象中

重量级锁

如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞,重量级锁有阻塞队列,就不需要自旋消耗了,一开始线程都在就绪队里进行竞争,竞争失败会进入到阻塞队列进行等待,为的就是不浪费CPU的资源,因为CPU会执行就绪队列的线程。

缺点就是线程阻塞,需要的时间就会长一些

死锁

死锁产生的原因

java并发那本书有一个例子,有两个线程,线程1和线程2,线程1中用synchronized 锁住了变量A,然后睡眠2s,然后锁变量B

线程2中用synchronized 锁住变量B,然后再在里面锁住变量A,

线程1中睡眠的2s大概率会切换到线程2中,但是线程2中B释放后再去操作A的时候,因为线程1锁着A呢,就只能等待线程1释放锁,

而线程1释放完A后,去操作B变量,B变量又让线程1的锁着呢,两个线程就互相等待对方释放锁,就造成了死锁

多个线程争夺同一资源而导致的一种局面,没有外界推动就无法解除

如何防止死锁

可以通过dump线程查看那个线程出了问题

·避免一个线程同时获取多个锁。

·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

银行家算法

双重校验锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1h058ZH3-1670228377075)(C:\Users\86185\AppData\Roaming\Typora\typora-user-images\image-20221026232605213.png)]

private volatile static Singleton singlleton;
//volatile(防止指令重排序+可见性(保证其他线程读到正确的数据)--原因:可能导致对象还没创建成功(只分配了空间地址,没有数据)就返回句柄,空指针异常
private Singleton(){}//私有化构造方法
public static Singleton getInstance() {
        if (instance == null) {//线程1,2同时到达,均通过(instance == null)判断。
                                // 线程1进入下面的同步块,线程2被阻塞
            synchronized (Singleton.class) {
                if (instance == null) {//线程1执行发现instance为null,初始化实例后,释放锁。
                    // 线程2进入同步块,此次instance已经被初始化。无法通过if条件,避免多次重复初始化。
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

执行双重检测是因为,如果多个线程通过了第一次检测,此时因为synchronized,其中一个线程会首先通过了第二次检测并实例化了对象,剩余的线程不会再重复实例化对象。这样,除了初始化的时候会加锁,后续的调用都是直接返回,解决了多余的性能消耗。

synchroniz不能防止指令重排序,instance = new Instance () ;可能会发生指令重排序,

  1. 在堆中开辟对象所需空间,分配地址
  2. 根据类加载的初始化顺序进行初始化
  3. 将内存地址返回给栈中的引用变量

但是句柄的地址没有传给属性,就是另一个线程会访问到Instance 仍然为空,,这样就需要继续走第一层if。

ThreadLock

变量值的共享可以使用public static的形式,所有线程都使用同一个变量,

JDK中的ThreadLocal类解决了每一个线程都有自己的共享变量

CountDownLatch

CountDownLatch允许一个或者多个线程去等待其他线程完成操作。

相当于是一个倒数的计数器阀门(),初始化时阀门关闭,指定计数的数量,当数量倒数减到0时阀门打开,被阻塞线程被唤醒。

线程执行完一个latch就减一(countDown()),减到0后就可以执行别的线程了

CountDownLatch接收一个int型参数,表示要等待的工作线程的个数。

CountDownLatch 提供了一些方法:

方法													说明
await()								使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。
await(long timeout, TimeUnit unit)	带超时时间的await()。
countDown()							使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。
getCount()							获得latch的数值。

原理

CountDownLatch的Sync是实现了AQS,重写了 tryAcquireSharedtryReleaseShared ,都是在AQS的框架上来实现的队列。

CountDownLatch是AQS的共享模式的实现,其内部也有一个静态内部类Sync继承了AbstractQueuedSynchronizer。

当我们通过构造函数创建CountDownLatch对象时,其实是指定了AQS的同步状态state的值,所以state在CountDownLatch代表的即使计数器的个数

看过Sync的源码我们可以明白CountDownLatch是共享模式的加锁和解锁方式,await()表示获取操作,countDown()表示释放操作。

state为0则表示可以加锁,不等于0的时候则线程会调用AQS提供的doAcquireSharedInterruptibly加入同步队列。每次解锁都只释放一个同步器状态,如果计数器为0的时候则会唤醒同步队列中的等待线程。

面试java有没有线程安全的类

在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字,但是有一些类是线程安全的,不用加

安全的类:

1、原子类Atomicxxx开头的都是线程安全的,它也是java.util.concurrent.atomic包下

Atomic

操作不可中断,就是符合原子性

Atomic按锁的类型来区分应该是属于乐观锁,也就是无锁

AtomicInteger类主要利用CAS和volatile和native 方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

java有一个Atomic目录,Atomic是线程安全的,基本类型有包装类,也有线程安全的类,

AtomicBoolean, AtomicInteger,AtomicLong,AtomicLongArray,

​ Atomicxxx 是通过Unsafe 类的native⽅法实现线程安全的

可以保障在对变量修饰的时候,起到上锁同样的效果,在多线程下进行i++,++i等操作得到正确的结果

2、java.util.concurrent这个包下面的类都是线程安全的

最常⽤的就是ConcurrentHashMap,当然还有ConcurrentSkipListSetConcurrentSkipListMap等等。
ConcurrentHashMap使⽤了⼀种完全不同的加锁策略来提供更⾼的并发性和伸缩性。ConcurrentHashMap并不是将每个⽅法都在同⼀个锁上同步并使得每次只能有⼀个线程访问容器,⽽是使⽤⼀种粒度更细的加锁机制——分段锁来实现更⼤程度的共享
在这种机制中,任意数量的读取线程可以并发访问Map,执⾏读取操作的线程和执⾏写⼊操作的线程可以并发地访问Map,并且⼀定数量的写⼊线程可以并发地修改Map,这使得在并发环境下吞吐量更⾼,⽽在单线程环境中只损失⾮常⼩的性能

3、通过synchronized 关键字给⽅法加上内置锁来实现线程安全

Vector,Stack,HashTable,StringBuffer,Timer,TimerTask

还有一些不变类,例如**StringIntegerLocalDate**,它们的所有成员变量都是final多线程同时访问时只能读不能写,这些不变类也是线程安全的。

4、BlockingQueue 和BlockingDeque接口的实现类
BlockingDeque接⼝继承了BlockingQueue接⼝,
BlockingQueue 接⼝的实现类有ArrayBlockingQueue ,LinkedBlockingQueue ,PriorityBlockingQueue ⽽BlockingDeque接⼝的实现类有LinkedBlockingDeque
BlockingQueue和BlockingDeque 都是通过使⽤定义为final的ReentrantLock作为类属性显式加锁实现同步的

5、 CopyOnWriteArrayListCopyOnWriteArraySet
CopyOnWriteArraySet的内部实现是在其类内部声明⼀个final的CopyOnWriteArrayList属性,并在调⽤其构造函数时实例化该CopyOnWriteArrayList,CopyOnWriteArrayList采⽤的是显式地加上ReentrantLock实现同步,⽽CopyOnWriteArrayList容器的线程安全性在于在每次修改时都会创建并重新发布⼀个新的容器副本,从⽽实现可变性。

6、ThreadPoolExecutor
ThreadPoolExecutor也是使⽤了ReentrantLock显式加锁同步

7、Collections中的synchronizedCollection(Collection c)⽅法可将⼀个集合变为线程安全,其内部通过synchronized关键字加锁同步

concurrent包

什么叫线程安全的类

普通的类在需要加锁才能保障写后读,在多线程下不会并发。线程安全的类不用加锁就能保障多线程下的安全

一个线程安全的类:首先属性得是private修饰的,因为首先一个属性如果只用volatile修饰的话,不能保障多线程下安全,synchronize又不能修饰属性,所以首先属性是私有的。。。线程安全的类,我们只能调用它的方法,所以它的方法或者代码块是加锁的,不加锁也是底层的CAS来操作。静态方法用到类锁,非静态用到的对象锁。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是基于,声明共享变量为volatile。 然后,使用CAS的原子条件更新来实现线程之间的同步的方式来实现的

原子操作

处理器如何实现原子操作?

原子操作,要么全部成功,要么全部失败,假设多线程情况,一个任务十个步骤,每个线程都有这个任务,也就是十个线程操作同一共享变量,如何保障原子性?

要保障十个线程的任务的十个步骤要么都成功,要么都失败?

1、加锁,加锁后一个线程失败,只需要回滚这个线程,相对于日志来说会节省CPU的浪费率,虽然一个线程执行其他线程需要等待,但是CPU利用率比较高。操作系统本身就是分时操作系统,就是在轮流执行,影响并不大。

2、日志记录,十个线程都对这个任务操作,当某一结点出错了,就把这之前的步骤按照日志回滚,但是回滚会浪费CPU的利用率

3、所以保障原子操作离不开锁机制

在Java中如何实现原子操作。

**java锁(volatile,synchronize)的底层是CAS,**除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

CAS锁的底层是操作系统的总线锁或者缓存锁

1、使用锁

2、循环CAS(自旋):比较版本号并且交换,一次CAS比较操作不一定能对上版本号,循环多次成功为止。boolean suc = atomicI.compareAndSet(i, ++i)

CAS一定是调用操作系统的CAS,并且加上总线锁或者缓存锁,java不好实现,

CAS内部实现原理: 两个线程t1,t2都读到了变量s=0和记录它的版本号变量v=1,每次操作更改的时候先对比版本号,版本号一致后再进行更改。CPU和内存之间的交互是通过总线进行的,总线的宽度36-41位,一次只能传输一个变量,所以变量s和记录它的版本号变量v是不能同时传输到CPU的,这样分批次传输的话,可能版本号v传输后,总线让给了其他的变量,没有让s继续传输,这样就会导致出错。 所以CAS操作的时候,底层其实也是给总线或者缓存行加锁了,能让变量和它所携带的版本号陆续通过总线,所以CAS操作离不开操作系统的总线锁和缓存锁

但是java没有办法操作总线锁和缓存锁,只能是java底层通过处理器提供的LOCK信号调用操作系统的总线锁和缓存锁,

扩展知识:一个CPU内部可以有多个核

操作系统的总线锁和缓存锁

锁缓存的意思就是多个线程都获取这个变量了,当一个线程操作这个变量的时候,其他的线程的变量都会被锁住,不让操作这个变量了,保障缓存一致性。当其他线程回写已被锁定的缓存行的数据时,会使缓存行无效·目的是为了写后读,配合volatile使用,声明volatile后才有缓存锁定

总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。不锁总线是因为总线是CPU通向内存的唯一通道,开销太大。力度要远大于缓存锁

有两种情况不使用缓存锁定

第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。

第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定

Happens-Before

从JDK 5开始,JSR-133使用happens-before的概念来阐述操作之间的内存可见性。如果一

个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个 操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前

·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。保障了写后读

总线事务

事务:一系列的成功或者失败叫做事务,事务需要保障原子性,一个事务包含两个及两个以上的步骤

**对于未同步或未正确同步的多线程程序(未加锁,指令重排序了)**能保障单线程的单个操作是正确的,是指物理层面的单次交互正确,

但是在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销,比如long、double64位,如果进行CPU 和内存的交互,总线一次能传输36-41位,也就是有36-41根导线,传输36-41个电压信号,所以一个long或者double类型需要拆成两个32位传输两次才能传输过去,在单线程下这是没问题的,但是在多线程下,线程间的切换就可能会产生问题,假设两个线程分别有long类型的数据,写回内存时,第一个线程传输高32位的数据,然后可能就切换线程了,然后另一个线程又传输了低32位的数据,这可能导致结果的错误,

事务回滚

按照快照还原

总线事务

数据通过总线在处理器和内存之间传递,处理器和内存之间的数据传递的一系列事务叫做总线事务。其中的读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写,任务需要排队通过总线,总线和内存靠电压信号传输,一个导线不能同时传递两个电压信号。所以只能排队访问内存。

并发

前端占带宽,请求数据啥的都占据CPU和内存,所以前后端分离

ajax前端访问后端不允许跨域,但是后端可以设置允许跨域

请求量过大的时候,前端集群,主域名映射到nginx上,然后nginx去分发请求,nginx承受不住,再加nginx集群,DNS解析,一个域名对应多个IP

双十一抢购,假设只有几万个商品,可能几百万人抢购,然后可能一个人前端点击10次,就会有上千万的请求,两种策略,一种就是你点击了一次,按钮变灰,然后写个定时器,两秒后再让点击,第二种就是前端拦截多余的点击,只允许通过一次请求,这样自己的前4端直接拦截90%的流量,这样就不会打到我们系统的服务器上

想通过写脚本,抓包的方式进行一个抢购是不可行的,因为前端在规定时间点想后端发送请求之前,请求的地址都是隐藏的,不会透露请求路径

务:一系列的成功或者失败叫做事务,事务需要保障原子性,一个事务包含两个及两个以上的步骤

**对于未同步或未正确同步的多线程程序(未加锁,指令重排序了)**能保障单线程的单个操作是正确的,是指物理层面的单次交互正确,

但是在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销,比如long、double64位,如果进行CPU 和内存的交互,总线一次能传输36-41位,也就是有36-41根导线,传输36-41个电压信号,所以一个long或者double类型需要拆成两个32位传输两次才能传输过去,在单线程下这是没问题的,但是在多线程下,线程间的切换就可能会产生问题,假设两个线程分别有long类型的数据,写回内存时,第一个线程传输高32位的数据,然后可能就切换线程了,然后另一个线程又传输了低32位的数据,这可能导致结果的错误,

事务回滚

按照快照还原

总线事务

数据通过总线在处理器和内存之间传递,处理器和内存之间的数据传递的一系列事务叫做总线事务。其中的读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写,任务需要排队通过总线,总线和内存靠电压信号传输,一个导线不能同时传递两个电压信号。所以只能排队访问内存。

并发

前端占带宽,请求数据啥的都占据CPU和内存,所以前后端分离

ajax前端访问后端不允许跨域,但是后端可以设置允许跨域

请求量过大的时候,前端集群,主域名映射到nginx上,然后nginx去分发请求,nginx承受不住,再加nginx集群,DNS解析,一个域名对应多个IP

双十一抢购,假设只有几万个商品,可能几百万人抢购,然后可能一个人前端点击10次,就会有上千万的请求,两种策略,一种就是你点击了一次,按钮变灰,然后写个定时器,两秒后再让点击,第二种就是前端拦截多余的点击,只允许通过一次请求,这样自己的前4端直接拦截90%的流量,这样就不会打到我们系统的服务器上

想通过写脚本,抓包的方式进行一个抢购是不可行的,因为前端在规定时间点想后端发送请求之前,请求的地址都是隐藏的,不会透露请求路径

双十一抢购的时候把请求过来的数据先放到消息队列中,让消息队列顶一两秒,或者顶不住了把剩余的请求直接丢掉,比如抢一万个商品,就要一万个请求,剩下的请求都丢弃

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、锁、同步等。学习Java中的并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少锁竞争、避免死锁等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值