多线程和锁

一.进程和线程

1.1进程

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程.

1.2线程

进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。

1.3进程与线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位(也可以理解为进程当中的一条执行流程)

  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

  • 调度和切换:线程上下文切换比进程上下文切换要快得多。

二.线程的创建方式

2.1继承Thread类

自定义一个类继承Thread类,并重写run()方法,没有返回值

2.2实现Runable接口

自定义一个类实现Runable接口,并重写run()方法,没有返回值

2.3实现Callable接口

自定义一个类实现Callable接口,重写call()方法.

创建Future的实现类FutureTask对象,把自定义的Callable实现类对象作为构造方法的参数.

创建Thread类的对象,把FutureTask对象作为构造方法的参数.

再调用FutureTask对象的get方法,就可以获取线程结束之后的结果。

Runable接口与Callable接口主要区别:
1、Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
2、Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
​

2.4线程池创建

  • 构造方法参数

/*
    public ThreadPoolExecutor(
        int corePoolSize,                       最大核心线程数(正式员工最大数量)
        int maximumPoolSize,                    所有线程最大数量(正式员工 + 临时员工的最大数量)
        long keepAliveTime,                     临时线程最大的空闲时间
        TimeUnit unit,                          临时线程最大空闲时间的单位,TimeUnit枚举类型
        BlockingQueue<Runnable> workQueue,      任务队列。待处理任务数量>核心线程空闲数量,多余的任务存入该队列。
                                                如果队列满了,又有新的任务,就会创建临时线程。
        ThreadFactory threadFactory,            线程工厂。创建新线程的工厂。
        RejectedExecutionHandler handler        任务拒绝策略。拒绝条件:要执行的任务 > 所有线程最大数量(参数2) + 任务队列容量(参数5)
    ) {
 */
  • 工作流程

    当有任务提交时,判断

三.线程状态

状态描述:

NEW(新建) 线程刚被创建,但是并未启动。

RUNNABLE(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。

BLOCKED(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

WAITING(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

TIMED_WAITING(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。

TERMINATED(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

四.线程安全问题

线程安全问题:简单来说,就是在多线程的调度下,导致出现了一些随机性,随机性使代码出现了一些bug

4.1问题产生背景

线程安全问题取决于线程共享数据是否安全,这个问题往往产生于对共享数据的并发修改.

由此可以看出线程安全问题的产生需要同时满足两个条件:

  • 多线程

  • 对同一个共享资源进行修改操作

如果我们始终只用一个线程去增删改数据,那么就不会产生线程安全问题;此外如果我们只是使用多线程对同一个共享资源做数据查询,并不对数据产生修改,同样也不会出现线程安全问题.

4.2解决方案

给线程加锁,在执行线程任务之前,让线程们去竞争锁,只有竞争到锁的线程才可以对共享资源进行修改,其他没有竞争到该锁资源的线程进入锁阻塞的状态.只有当持有锁的线程释放了该锁,其他处于锁阻塞又会重新去竞争锁资源.

4.2.1使用Synchronized关键字

synchronized 是Java的关键字,它可以修饰方法和代码块。

出异常时会释放锁,不会出现死锁.

不会手动释放锁,只能等同步代码块或方法结束后释放锁.

使用方式:

  • 普通同步方法,锁是当前实例对象 this

  • 静态同步方法,锁是当前类的class对象

  • 同步代码块,锁是括号里面的对象【必须共享】

4.2.2使用Lock接口的实现类

Lockjava.util.concurrent.locks 包中的一个接口,它只能作用在代码块上。

出异常时不会释放锁,会出现死锁,需要在finally中手动释放锁

可以调用api手动释放锁

使用方式:

需要先创建一个Lock的实现类,然后在需要加锁的代码前面使用该实现类对象的lock()方法;当想要释放锁时,就需要调用该锁对象的unlock()方法.

4.2.3Synchronized 与 Lock 的区别

4.3锁分类

4.3.1悲观锁与乐观锁

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

乐观锁的实现方式:

    乐观锁的实现方式主要有两种,一种是CAS(Compare and Swap,比较并交换)机制,一种是版本号机制。

CAS机制:

    CAS操作包括了三个操作数,分别是需要读取的内存位置(V)、进行比较的预期值(A)和拟写入的新值(B),操作逻辑是,如果内存位置V的值等于预期值A,则将该位置更新为新值B,否则不进行操作。另外,许多CAS操作都是自旋的,意思就是,如果操作不成功,就会一直重试,直到操作成功为止。

版本号机制:

    版本号机制的基本思路,是在数据中增加一个version字段用来表示该数据的版本号,每当数据被修改版本号就会加1。当某个线程查询数据的时候,会将该数据的版本号一起读取出来,之后在该线程需要更新该数据的时候,就将之前读取的版本号与当前版本号进行比较,如果一致,则执行操作,如果不一致,则放弃操作。

悲观锁的实现方式:

    悲观锁的实现方式也就是加锁,加锁既可以在代码层面(比如Java中的synchronized关键字),也可以在数据库层面(比如MySQL中的排他锁)。
4.3.2公平锁与非公平锁

公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

  • 公平锁的优点是等待锁的线程不会饿死。

  • 缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。

非公平锁

  • 非公平锁是多个线程加锁时直接尝试获取锁,能抢到锁到直接占有锁,抢不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

  • 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。

  • 缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

4.4锁的升级过程

五.线程通信

多个线程在并发执行的时候,他们在CPU中是随机切换执行的,这个时候我们想多个线程一起来完成一件任务,这个时候我们就需要线程之间的通信了,多个线程一起来完成一个任务.

同步线程通信一般有4种方式:

  • 通过 volatile 关键字

  • 通过 Object类的 wait/notify 方法

  • 通过 condition 的 await/signal 方法

  • 通过 join 的方式

异步的线程通信可以使用MQ实现

进程间的线程通信可以使用Feign,HttpClient,Socket和MQ实现

六.多线程并发

6.1线程安全的特性

原子性 :即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值(volitale)

有序性:程序执行的顺序按照代码的先后顺序执行

6.2volitale 关键字的作用

volitale关键字保证了可见性和一定程度上的有序性,但是不能保证原子性。

6.2.1volitale 关键字保证有序性

内存屏障提供了避免重排序的功能

loadload():保证load1的读取操作在load2及后续读取操作之前执行。保证load2在读取的时候,自己缓存内到相应数据失效,load2会去主内存中获取最新的数据

storestore():在store2及其后的写操作执行前,保证store 1的写操作已刷新到主内存

loadstore():在store2及其后的写操作执行前,保证load1的读操作已读取结束

storeload( ): 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行。同时保证:强制把写缓冲区的数据刷回到主内存中,让工作内存/CPU高速缓存当中缓存的数据失效,重新到主内存中获取新的数据

6.2.2 volitale关键字保证可见性

内存屏障会把线程把工作内存中改变后的数据直接刷回主内存。其他线程就可以从主内存中获取最新数据

内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)

6.3单例模式

双重校验锁:

  • 声明的单例对象用volatile关键字修饰

  • 使用get方法获取单例对象的实例时加锁

七.Spring集成线程池

  • @Async标记在方法上, 表示当前方法是异步执行的

  • @EnableAsync标记在启动引导类上,开启多线程异步

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值