关于同步的一些原理

关于线程并发的一些底层原理记录

进程:进程是是程序的一次执行过程,是程序在执行过程中分配和管理资源的基本单位。
线程:是CPU调度的最小单位。它比进程更小,一个进程可以包含多个线程,线程会共享进程的堆和方法区资源。每个线程也有自己的程序计数器,虚拟机栈和本地方法栈。
一个java程序的运行是main线程和其他线程同时运行。

因为多线程运行:
原因:单核时代:多线程主要提高单进程利用CPU和IO的效率。一个线程被IO阻塞,其他线程可以继续使用CPU。
多核时代:多线程主要为了提升利用多核CPU的能力。

进程之间通信:不同进程之间如何交换传播信息

1.管道:无名管道,半双工的。只用于父子进程和兄弟进程之间
有名管道:在数据读出时,FIFO管道同时清除数据,并且先进先出,可用于任何进程。
2.信号:比较复杂的通信方式,用于通知接收进程某一事件已经发生。
3.信号量:是一个计数器,用来控制多个进程对共享资源的访问,通常作为一种锁机制,防止某进程在访问共享资源时,其他进程也访问该资源。
4.消息队列:由消息组成的链表,存放在内核中,由消息队列标识符标识。其优势对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,可以接受特定类型的消息
5.共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
6.套接字:一种通信机制,可用于不同机器之间的进程间通信,例如客户端/服务器

进程之间如何同步

1.临界区:多线程串行化来访问公共资源,速度快,适合控制数据访问。但是只适合同步本进程内的线程。
2.互斥量:拥有互斥对象 的线程才具有访问资源的权限。不仅在同一应用程序中不同线程中实现资源的安全共享,还可以在不同应用程序的线程之间实现对资源的共享。
3.信号量:控制一个具有有限数量用户资源而设计。允许多个资源在同一时刻访问同一资源,但是有最大线程数目。
4.事件:用来通知线程有一些事情发生,从而启动后继任务的开始。

线程同步:

1.临界区:拥有临界区的线程可以访问资源,其他线程访问则被挂起,直到临界区线程释放
2.事件:允许一个线程在处理完一个任务后,主动唤醒另一个线程
3.互斥量:允许在进程之间使用
4.信号量:通过一个计数器来限制可以使用某共享资源的线程数目。

线程与并发:

多线程并发就是为了能够提高程序的执行效率,及运行速度
但是多线程编程会产一个问题:并发的线程安全的问题。

因此多线程访问共享资源的时候,需要同步(串行),来保证变量的唯一性和准确性。

synchronized 关键字:

保证原子性,可见性,有序性

原子性:保证语句块内操作时原子的
可见性:unlock之前必须把此变量同步回主内存
有序性:通过一个变量在同一时刻只允许一条线程对其进行lock操作

synchronized关键字修饰的方法或代码块在任意时刻只有一个线程执行。
主要有:
1.修饰实例方法,作用域当前对象实例加锁,需要获得当前对象实例的锁
2.修饰静态方法:给当前类加锁,作用域类的所有对象实例,要获得当前类的锁。
3.修饰代码块:指定加锁对象,给指定对象/类加锁。sychoronized(this|object),synchronized(类.class)

底层原理:

属于JVM层面
synchronized同步语句块的实现:使用的Monitorenter和moniterexit指令,代表同步代码块的开始位置和结束位置
当执行monitorenter指令时,线程试图获取锁,及获取对象监视器monitor的持有权。如果锁的计数器为0,则表示可以获取,获取锁之后将锁计数器+1。
在执行 monitorexit 指令后,将计数器-1。当锁计数器为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized修饰方法: 隐式的,无需通过字节码指令控制,实现在方法调用和返回操作中。
ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

不过两者的本质都是对对象监视器 monitor 的获取。监视器锁本质为操作系统的mutex lock(互斥锁),因此线程之间的切换需要从用户态和内核态,因此其效率低下。这种依赖于mutex lock实现的锁称之为重量级锁。

JVM对synchronized的优化

偏向锁 --> 轻量级锁 --> 重量级锁

只可升级不可降级

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。
当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置为1(表示指向当前进程):如果没有,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前进程。

CAS原理:

Compare and Swap,即比较再交换
CAS算法实现了一种乐观锁.而synchronized时悲观锁
使用场景:java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

乐观锁与悲观锁:

乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现即版本控制+CAS); 乐观锁比较适用于读多写少的情况(多读场景)

悲观锁:
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作。

原理:CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

轻量级锁:

偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁.
即当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

自旋:
JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。(java里可以通过cas实现)
从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

重量级锁:

自旋失败,很大概率 再一次自选也是失败,因此直接升级成重量级锁,进行线程阻塞,减少cpu消耗。
当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。

在这里插入图片描述

synchronized和reentrantLock的区别

1.都是可重入的

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

2.syn是依赖于JVM,Reent依赖于API

如上面分析的
ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)

3.reent增加了一些新的功能

(1)等待可中断:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
(2):公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
(3):选择性通知:synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,但是被通知的线程由JVM选择。ReentrantLock类借助于Condition实例可以实现选择性通知。

volatile关键字

CPU中存在cpu高速缓存:

CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题

当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
在这里插入图片描述

在这里插入图片描述

此外:还避免了指令重排优化

vol通过多加了lock addl指令,构建了内存屏障,lock后的指令不能重排到内存屏障的前面。例子:双锁检测实现单例模式

ThreadLocal

ThreadLocal类主要解决的就是让每个线程绑定自己的值,创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题.

通过一个ThreadLocalMap存储每个线程的副本和值
在这里插入图片描述
3.4. ThreadLocal 内存泄露问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

AQS原理

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

在这里插入图片描述
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

AQS 定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如 CountDownLatch、Semaphore、 CyclicBarrier、ReadWriteLock 。

AQS 底层使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

AQS 组件总结
Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。


CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 CountDownLatch。

public class CountDownLatchExample1 {
    // 处理文件的数量
    private static final int threadCount = 6;

    public static void main(String[] args) throws InterruptedException {
        // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建)
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -> {
                try {
                    //处理文件的业务操作
                    //......
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //表示一个文件已经被完成
                    countDownLatch.countDown();
                }

            });
        }
        countDownLatch.await();
        threadPool.shutdown();
        System.out.println("finish");
    }
}

题外话:接之前的spring

SpringBoot自动配置:

@EnableAutoConfiguration
1.@AutoConfigurationPackage:将mainappliaction所在包下的所有组件导入

2.Imprt(AutoConfigurationImportSelector.class)
根据getCandidateConfigurations:获取候选配置,获取所有自动配置集合:configurations,以AutoConfiguration结尾
3.利用工厂加载,得到所有组件
4.从META-INF/spring.factories位置来加载一个文件。

每个自动配置类中按照条件装配规则@Conditional,按需加载
生效的配置类就会给容器中年装配很多组件。


总结:
xxxxxAutoConfiguration —>装配组件—>xxxxProperties拿值 —>从appliaction.properties拿值(用户自己配置)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值