多线程并发

  • Post author:
  • Post category:其他




多线程



一.volatile


volatile底层原理:


线程对变量进行修改之后,要立刻回写到主内存。

线程对变量读取的时候,要从主内存中读,而不是缓存。


两个特性:



1.可见性:


一个被volatile关键字修饰的变量,那么所有线程都是可见的,也就是说当其中一个线程对变量进行修改的时候,这个新的值对其他线程来说是立即可见的


2.排序性:


是通过内存屏障来实现排序性的,在对volatile修饰的变量进行读写操作时,会在写操作后面加一条store屏障指令,进行读操作时会在读操作前加一条load屏障指令


volatile优化:


通过追加字节来优化性能——>追加了64字节

因为对于大多数处理器来说,比如这个因特尔i7处理器吧,它的这个高速缓存行是64字节的,如果队列的头节点和尾节点都不够64字节的话,可能会影响队列的入队和出队的操作,64字节就避免了队列的头节点和尾节点加载到同一个缓存行这个问题了,使得头尾节点在修改时不会互相锁定。


也有两种场景不追加64字节:


1.缓存行不是64字节的处理器

2.共享变量不会被频繁的写



二.sys


底层原理:


syn是JVM层面的,syn解决的是多个线程之间访问资源的同步性,syn可以保证被它修饰的方法,代码块在任意时刻只能有一个线程执行,在JDK1.5之前,syn属于重量级锁,效率低,因为监视器锁是依赖底层的操作系统实现的,要挂起或者唤醒一个线程都需要操作系统帮忙完成,而操作系统实现线程之间的切换要从用户态转成内核态,时间成本较高,也是效率低的原因,syn锁是存在对象头里的,对象头包括Mark Word(Mark Word里面是线程id,GC年龄,hashcode值)和类型指针,(如果是对象数组的话还记录这数组长度的数据),对象头占两个机器码,如果是数组类型就占3个机器码。

为了减少加锁和释放锁带来的性能消耗JDK1.6之后引入了偏向锁,轻量级锁


锁升级过程:

(锁可以升级但是不可以降级)


1.无锁状态——程序没有锁竞争的时候



2.偏向锁———经常由同一个线程获得锁

(大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,偏向锁会偏向于第一个获得它的线程)



补充偏向锁的执行过程:


1、线程首先检查该对象头的线程ID是否为当前线程;

2、如果对象头的线程ID和当前线程ID一致,则直接执行代码;如果不是当前线程ID,则使用CAS方式替换对象头中的线程ID,如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁。

3、如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码。

4、执行代码完成之后释放锁,把对象头的线程ID修改为空。)


引入偏向锁的目的:


在没有多线程竞争的情况下,尽量减少不必要的轻量级锁执行,因为轻量级锁的加锁和释放锁是要依赖多次CAS操作的,而偏向锁只有在置换线程id的时候依赖一次。


3.轻量级锁——–有线程来参与锁的竞争,但是获取锁的冲突时间很短。


当发现线程获取锁出现冲突时,线程首先会使用自旋的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU,当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了。


4.重量级锁———–有大量的线程参与锁的竞争,冲突性很高。


当获取锁冲突多,时间越长的时候,我们的线程肯定无法继续自旋了,因为这样太消耗CPU了,所以就等前面获取锁的线程释放了锁之后再开启下一轮的锁竞争


锁粗化:


通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。

锁粗化就是说把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。


适应性自旋锁:


如果线程自旋成功了,那么下次自旋次数会加多,因为虚拟机认为既然你这次都成功了,那下次也很有可能成功,就允许自旋等待持续的次数更多,反之也一样


syn三种作用范围:


1.实例方法———-对当前对象实例加锁

2.静态方法———-对当前类加锁

3.代码块————-对指定对象加锁



三.CAS算法

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

锁升级用到了CAS算法



四.锁的区别



(一)syn和ReenTrantLock区别:

1.两者都是可重入锁,都是加锁方式同步,而且都是阻塞式的同步

2.syn是JVM层面的,ReenTrantLock是JDK层面的

3.ReenTrantLock比syn多加了一些功能:


等待可中断

———–正在等待的线程可以选择放弃等待


可实现公平锁

———–可以指定是公平锁(线等待的线程先获得锁)还是非公平锁,syn只能是非公平锁


锁绑定多个条件一

———个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程,而synchronized是要么随机唤醒一个线程要么唤醒全部线程。


ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。



Syn为什么是非公平锁

在这里插入图片描述



(二)syn和volatile区别

1.volatile的性能要比syn好

2.volatile只能用于修饰变量;syn可以修饰实例方法,静态方法,代码块

3.多线程访问下volatile不会发生阻塞;syn可能会发生阻塞

4.volatile保证了数据的可见性不保证原子性;syn保证了可见性和原子性

5.volatile主要用于解决数据在线程间的可见性;syn主要解决的是多个线程之间访问资源的同步性



(三)syn和Lock的区别

1.syn是JVM层面的,是一个关键字;lock是一个接口

2.syn会自动释放锁;lock必须手动释放

3.lock可以让等待锁的线程响应中断;syn不会,线程会一直等待

4.syn可以修饰实例方法,静态方法,代码块;lock是作用于一块范围的

5.lock可以知道线程有没有拿到锁(用trylock方法判断);syn不能

6.lock可以提高多个线程进行读操作的效率(readwritelock实现读写分离)



五.乐观锁悲观锁


悲观锁:


总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁


乐观锁:


总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据




版本号机制和CAS算法实现乐观锁:


1.乐观锁的版本号机制:

在数据表中加一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version的值会+1,比如说当线程A要更新数值时,在读取数据的同时也会读取version值,在提交更新时,只有当刚才读取到的version值和数据库中的version值相等时才更新,否则重试更新操作,直到成功


2.CAS算法


CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。


CAS算法的缺点:


2.不能保证代码块的原子性,只能保证一个共享变量的原子性(使用互斥锁)


3.ABA问题:

如果一个变量V初次读取的时候是A,并且在准备赋值的时候检查它,仍是A,但这不能说明他的值没有被其他线程修改过

在这里插入图片描述



六.AQS

AQS——是除了java自带的synchronized关键字之外的一种锁机制。

它实现了一个先进先出的队列,底层数据结构是一个双向链表


AQS的核心思想:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设为锁定状态,如果被请求的共享资源被占用,就将暂时请求不到的线程加入到队列中

AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。



AQS对资源的共享方式

在这里插入图片描述

在这里插入图片描述



七.创建线程池


1.Executors.newCacheThreadPool():


可缓存线程池,先查看池中有没有以前创建的线程,如果有,那就直接用就可以,如果没有就创建新的线程加入到池中,缓存型线程池通常执行一些生存期很短的异步型任务(当执行当前任务时,上一个任务已经完成了,会复用执行上一个任务的线程,而不用每次创建新的线程)


2.Executors.newFixedThreadPool():


创建一个可重用固定个数的线程池,它是以共享的无界队列的方式来运行这些线程的(这种方式不推荐,因为无界队列就是无限的线程都进去,会造成内存溢出)


3.Executors.newScheduledThread(int n):


创建一个定长的线程池,支持定时和周期性的任务执行(比如说延时执行)


4.Executors.newSingleThreadExecutor():


创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务都按照指定的顺序执行



八.线程池的核心参数


1.CorePoolSize:


核心线程数,核心线程会一直存活,即使没有任务要执行(可以设置allowCoreThreadTimeOut=true,(默认是false)核心线程会关闭)


2.QueueCapacity:


任务队列容量或者说阻塞队列,当核心线程数达到最大时,新任务会放在队列中排队,等待执行


3.MaxPoolSize: 最大线程数


当线程数>=核心线程数,且任务队列已经满了,线程池就会创建新的线程来处理任务;

当线程数=最大线程数时,且任务队列已经满了,线程池就会拒绝处理任务并且抛出异常。


4.KeepAliveTime:


线程空闲时间,当线程在这段时间内没工作就会退出


5.AllowCoreThreadPool:


允许核心线程超时(默认是false,可以手动设置)



九.并发

并发的目的其实就是让我们的程序运行的更快,但是并不是说启动更多的线程就能让我们的程序最大限度的去并发执行


需要考虑到:上下文切换,死锁问题,还有就是可能受限于硬件和软件的资源限制问题


上下文切换:


当前任务执行一个时间片之后会切换到下一个任务,但在切换前它会保存上个任务的状态。比如说我们看一本英文书,然后有个单词不认识,就去翻字典,但是我要记着是哪一页的单词再去查。


如何减少上下文切换:


用CAS算法更新数据:当V=A了就用B替换V,否则不执行操作


无锁并发编程:


将数据的ID按照hash算法取模分段,不同线程去处理不同段的数据


使用最少线程:


这个比较好理解,就是避免创建不需要的线程


使用协程:


在单线程里实现多个任务的调度,在单线程里维持多个任务间的切换


死锁:

会造成系统功能不可用


避免死锁:


1.避免一个线程同时获取多个锁

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

3.可以尝试使用定时锁lock.tryLock(timeout)

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


受限于硬件和软件的资源限制问题:


主要是硬件方面,可以构建集群来并行执行



十.线程池



10.1创建线程的方法

直接继承Thread类 Runnable Callable

在这里插入图片描述

在这里插入图片描述



10.2Runnable和Callable的区别:

Runnable在jdk1.0的时候就有了,Callable是jdk1.5有的,主要区别就是Callable的call()方法可以返回值和抛出异常,run()方法没有这些功能,Callbale可以返回装载有计算结果的Future对象

在这里插入图片描述



10.3使用线程池的原因,具体流程

多线程运行时,系统会不断地创建,销毁这个新线程,这个操作会过度的消耗系统资源,这时就会用到线程池,因为线程池就是创建一些线程,线程执行完任务不会死亡而是再次回到线程池,成为空闲状态


具体流程:


任务—>先判断核心线程数是否已满(如果没满就创建一个工作线程来执行这个任务)——->如果满了的话就判断任务队列是否已满(如果没满,就把这个任务放到队列里,等着被执行)——–>如果满了就判断整个线程池是否已满(如果没满那就创建新的线程工作)——–>如果满了就交给拒绝服务策略


4种拒绝服务策略:


1.直接抛出异常(默认情况)

2.只用调用者所在的线程来完成任务

3.丢弃队列里最旧的一个任务,并行执行当前任务

4.不处理,丢弃掉



10.4向线程池提交任务的两种方法


1.execute():


用于提交不需要返回值的任务,所以它无法判断任务是否已被线程池执行成功


2.submit():


用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象来判断任务是否成功,还可以用future的get()方法来获取返回值



10.5线程池的个数怎么设置


1.CPU密集型:


线程池大小设置为N+1——–N是CPU个数


2.IO密集型:


最佳线程数目:(线程等待时间/线程CPU时间+1)*N


3.混合型:


((线程IP时间+线程CPU时间)/线程CPU时间)*N



10.6线程和进程的区别

进程是一个独立的运行环境,它可以看作是一个程序或者应用,线程是在进程中执行的一个任务,可以说线程是进程的一个子集,不同的进程使用的是不同的内存空间,而所有的线程是共享相同的内存空间的



10.7 Thread类中start()和run()方法的区别

start()方法是启动一个线程,这时此线程是处于就绪状态,但是并没有运行。然后通过调用run()方法来完成接下来的运行操作。start()方法不可以多次启动一个线程,run()方法是可以重复调用的。必须等待这个线程的run()方法里的代码全都执行完了,另一个线程才可以执行run()方法里的代码

(我也考虑过就是说不调用start()方法直接调用run()应该是更方便,但是如果不调用这个start()方法直接调用run()方法,那它其实就是一个普通的函数调用了,并没有达到多线程的作用)



10.8 sleep()方法和wait()方法的区别


1.

sleep是Thread类的一个静态方法,作用于当前线程;wait是定义在Object中的实例方法,作用于对象本身


2.

sleep不会释放锁,也不需要占用锁;wait会释放锁,前提是当前线程占有锁,也就是说这块代码要在syn修饰范围内的


3.唤醒条件:


唤醒sleep的话可以让它超时或者调用interrupt()方法;唤醒wait的话可以让其他线程调用对象的notify或者notifyall方法(notify也是Object里面的方法,在执行notify之前,线程也必须获得锁)


注意:

如果没在try-catch中用sleep方法的话,当我们调用interrupt方法,JVM就会抛出InterruptedException异常,所以说必须在try-catch中调用这个sleep

在这里插入图片描述



10.9 ThreadLocal

ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量。


ThreadLocal与Synchronized的区别:


ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。


但是ThreadLocal与synchronized有本质的区别:


1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

2、ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。Synchronized正好与它相反,它用于在多个线程间通信时能够获得数据共享。



10.10 多线程中的忙循环

用循环让一个线程等待,不同于wait和sleep方法,wait和sleep是放弃了CPU控制,而忙循环不会放弃CPU,它其实就是在运行一个空循环,目的就是为了保留CPU缓存,避免重建缓存和减少等待重建的时间



10.11关闭线程池

shutdown shutdownnow interrupt

shutdown和shutdownnow区别:

shutdown:正在执行的任务会继续执行下去,没有被执行的则中断

shutdownnow:正在执行的任务则被停止,没被执行任务的则返回



10.12 join()方法

有时候我们在主线程中生成了子线程,如果子线程中有进行大量的耗时操作,主线程会比子线程早结束,这也是没有问题的,但是

如果主程序在结束之前需要子线程的处理结果,那么就必须要等子线程结束之后才能结束主线程。这个时候我们就可以用join()方法。 join()方法的作用就是让主线程等待子线程执行结束之后再运行主线程。


例:

线程A和B,A调用B,A依赖B的返回结果才可以执行,

在线程A 中调用线程B的join方法,当线程调用了这个方法时,线程B会强占CPU资源,直到线程执行结果为止(谁调用join方法,谁就强占cpu资源,直至执行结束)



10.13 sleep(),join(),wait(),yield()区别


1. sleep():


Thread.sleep(1000);

在指定时间内让当前执行的线程暂停执行一段时间,让其他线程有机会继续执行,但不会释放对象锁,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据,不推荐使用,sleep() 使当前线程进入阻塞状态,在指定时间不会执行。


2. wait():


对象的方法,会释放对象锁

wait()和notify()、notifyAll(),这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用也就是说,调用wait(),notify()和notifyAll()的任务在调用这些犯法前必须拥有对象锁

wait()和notify()、notifyAll()它们都是Object类的方法,而不是Thread类的方法。

(当调用某一对象的wait() 方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用 notify() 方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获得锁标志,他们随时准备争夺锁的拥有权,当调用了某个对象的notifyAll() 方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池)


wait():调用该方法使持有该对象的线程把该对象的控制权交出去,然后处于等待状态

notify():调用该方法就会通知某个正在等待这个对象的控制权的线程可以继续运行

notifyAll():调用该方法就会通知所有等待这个对象控制权的线程继续运行


3. yield():


Thread类的静态方法,不会释放对象锁,不抛异常

yield() 方法和sleep() 方法类似,也不会释放对象锁,它是Thread类的静态方法,区别在于,它没有参数,即yield() 方法只是使当前线程让步,重新回到就绪状态,所以执行yield的线程,有可能在进入到就绪状态后马上又被执行,另外yield方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep方法不同

在这里插入图片描述


4. join():


Thread 类的对象实例的方法

Thread t1 = new Thread();t1.join();

join() 方法会使当前线程等待调用join()方法的线程结束后才能继续执行



10.14 fork

调用fork()函数并返回成功之后,程序就将变成两个进程,调用fork()者为父进程,后来生成者为子进程。这两个进程将执行相同的程序文本,但却各自拥有不同的栈段、数据段以及堆栈拷贝。子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。从性能方面考虑,父进程到子进程的数据拷贝并不是创建时就拷贝了的,而是采用了写时拷贝(copy-on -write)技术来处理。调用fork()之后,父进程与子进程的执行顺序是我们无法确定的



线程的五种状态

在这里插入图片描述



线程阻塞的情况

在这里插入图片描述



线程死亡的三种情况

在这里插入图片描述



版权声明:本文为qq_43581919原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。