当前位置:首页 > 技术学院 > 技术前线
[导读]线程切换能够在一个 CPU 周期内完成(实际上可以没有开销,上个周期在运行线程A,下个周期就已在运行线程B)。这样子看起来像是每个线程是独自运行的,没有其他线程与目前共享硬件资源。

线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。

粗粒度交替多线程

一个线程持续运行,直到该线程被一个事件挡住而制造出长时间的延迟(可能是内存load/store操作,或者程序分支操作)[4]。

举例来说

周期 i :接收线程 A 的指令 j周期 i+1:接收线程 A 的指令 j+1周期 i+2:接收线程 A 的指令 j+2,而这指令缓存失败周期 i+3:线程调度器介入,切换到线程 B周期 i+4:接收线程 B 的指令 k周期 i+5:接收线程 B 的指令 k+1[5]硬件成本

此种多线程硬件支持的目标,是允许在挡住的线程与已就绪的线程中快速切换。

这些新增功能的硬件有这些优势:

线程切换能够在一个 CPU 周期内完成(实际上可以没有开销,上个周期在运行线程A,下个周期就已在运行线程B)。这样子看起来像是每个线程是独自运行的,没有其他线程与目前共享硬件资源。对操作系统来说,通常每个虚拟线程都被视做一个处理器。这样就不需要很大的软件变更(像是特别写支持多线程的操作系统)。为了要在各个现行中的线程有效率的切换,每个现行中的线程需要有自己的暂存设置(register set)。像是为了能在两个线程中快速切换,硬件的寄存器需要两次例示(instantiated)。

细粒度交替式多线程

执行过程很像桶形处理器(Barrel Processor)就像这样:

周期 i :接收线程 A 的一个指令周期 i+1:接收线程 B 的一个指令周期 i+2:接收线程 C 的一个指令这种线程的效果是会将所有从运行流水线中的资料从属(data dependency)关系移除掉。因为每个线程是相对独立,流水线中的一个指令层次结构需要从已跑完流水线中的较旧指令代入输出的机会就相对的变小了。而在概念上,这种多线程与操作系统的核心先占多任务(pre-exemptive multitasking)相似。

1.基本概念

进程:在操作系统中运行的程序就是进程,比如:QQ、播放器等。

线程:一个进程可以有多个线程,如:视频可以同时听声音、看图像。

并行:多个cpu实例或者多台机器同时执行各自的处理逻辑,是真正的同时。

并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。

2.线程的生命周期

1)有新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)共5种状态。

新建状态:new关键字新建一个线程后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化成员变量的值。

就绪状态:线程对象调用start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

运行状态:处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,此时该线程处于运行状态。

阻塞状态:处于运行状态的线程失去所占用资源后,便进入阻塞状态。

死亡状态:线程run()方法执行结束后进入死亡状态。此外,如果线程执行了interrupt()或stop()方法,也会以异常退出的方式进入死亡状态。

2)线程状态的控制:

start():启动当前线程,自动调用当前线程的run()方法。

run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作方法声明在此方法中。

yield():释放当前CPU的执行权。

join():若线程a中调用线程b的join(),则线程a进入阻塞状态,直到线程b执行完成后线程a才结束阻塞状态。

sleep(long militime):让线程睡眠指定的毫秒数,指定时间内,线程是阻塞状态,不会释放锁。该方法可以再任何场景下调用。

wait():执行此方法,当前线程会进入阻塞状态,并释放同步监视器(锁)。该方法必须在同步代码块和同步方法中才能调用。

notify():唤醒被wait的一个线程,多个线程wait时,唤醒优先度最高的。

notifyAll():唤醒所有被wait的线程。

LockSupport():LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒。

3.多线程的5种创建方式

① 继承Thread类,重写run()方法。

② 实现Runnable接口,重写run()方法。

③ 匿名内部类的方式,重写run()方法。相当于继承了Thread类。new Thread(){ public void run(){逻辑功能} }.start();

④ Lambda表达式创建,相当于实现Runnable接口的方法。new Thread(()->{逻辑功能}).start();

⑤ 线程池创建。Executor pool = Executors.newFixedThreadPool(); pool.excute(new Runnable(){public void run(){逻辑功能}});

线程池创建的一些参数:

corePoolSize:队列没满时,线程最大并发数。

maximumPoolSizes:队列满后线程能够达到的最大并发数。

keepAliveTime:空闲线程过多久被回收的时间限制。

unit:keepAliveTime的时间单位。

workQueue:阻塞的队列类型。

RejectedExecutionHandler:超出maximumPoolSizes + workQueue时,任务会交给RejectedExecutionHandler来处理。

4.线程的同步

为了防止多个线程访问一个数据对象时,对数据造成破坏,采用线程同步来保证多线程安全访问竞争资源。

1)普通同步方法:synchronized关键字加在普通方法上,此时锁就是当前实例对象,进入同步方法前要获取当前实例的锁。

2)静态同步方法:synchronized关键字加在静态方法上,此时锁就是当前类的class对象,进入同步方法前要获取当前类对象的锁。

3)同步方法块:synchronized关键字加在代码块前,小括号中指定锁是什么,进入同步代码块前就需要获取指定的锁。

synchronized底层实现:

数据在JVM内存的存储:Java对象头、moniter对象监视器。

① 在JVM虚拟机中,对象在内存中的存储布局分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。Java对象头包括:类型指针(Klass Pointer)和标记字段(Mark Word)。类型指针是对象只想它的类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。标记字段用于存储对象自身的运行时数据,比如哈希码、锁状态标志、线程持有的锁等。所以,synchronized使用的锁对象是存储在Java对象头里的标记字段里。

② moniter:对象监视器可以类比为一个特殊的房间,房间中有一些被保护的数据,monitor保证每次只有一个线程能进入房间,进入即为持有monitor,退出即为释放monitor。使用synchronized加锁的同步代码块在字节码引擎中执行时,主要就是通过锁对象monitor的取用(monitorenter)与释放(monitorexit)来实现的。

5.多线程引入问题

1)线程安全问题

① 原子性:常通过synchronized或者ReentrantLock来保证原子性。

② 可见性:指一个线程修改了某个变量值,其他线程能够立即得到这个修改的值。每个线程都有自己的工作内存,工作内存和主存间要通过store和load进行交互。可见性问题常使用volatile关键字解决。当一个共享变量被volatile修饰时,它会保证修改的值立即更新到主存,当其他线程需要读取时,会去主存中读取新值,而普通共享变量不能保证可见性,因为其被修改后刷新回主存的时间是不确定的。

2)线程死锁

由于两个或多个线程互相持有对方所需的资源,导致线程都处于等待状态,无法继续执行。

3)上下文切换

多线程有线程创建和线程上下文切换的开销。CPU通常会给不同的线程分配时间片,当CPU从一个线程切换到另外一个线程的时候,CPU需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据、程序指针等,这个切换就称为上下文切换。通常使用无锁并发编程、CAS算法、协程等方式解决。

6.使用ReentrantLock

Java语言直接提供了synchronized关键字用于加锁,但这种锁存在两个问题:①很重,②获取时必须一直等待,没有额外的尝试机制。java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁。调用ReentrantLock()对象的lock()方法获取锁,最后调用unlock()方法手动释放锁。ReentrantLock是可重入锁,和synchronized一样,一个线程可以多次获取同一个锁。和synchronized不同的是,ReentrantLock可以尝试获取锁,即:调用ReentrantLock()对象的tryLock()方法,其中传入等待时间,时间单位,如果在这个时间后仍然没有获取到锁,tryLock()方法会返回false,程序可以去做其他事情。

synchronized可以配合wait()和notify()实现线程在条件不满足时等待,条件满足时唤醒,而ReentrantLock则需要借助Condition对象来实现,注意Condition对象必须来自于ReentrantLock()对象调用newCondition()方法,这样才能获得一个绑定了ReentrantLock实例的Condition实例。Condition提供了await()、signal()、signalAll()方法,与wait()、notify()、notifyAll()是一致的。

① await():会释放当前锁,进入等待状态;

② signal():会唤醒某个等待线程;

③ signalAll():会唤醒所有等待线程;

此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来:

if ( condition.await(1, TimeUnit.SECOND) ) {

// 被其他线程唤醒

} else {

// 指定时间内没有被其他线程唤醒

}

7.使用ReadWriteLock

ReentrantLock保证了只有一个线程可以执行临界区代码,但是有些时候,这种保护有点过头。有些方法只是读取数据,并不修改数据,此时应该允许多个线程同时调用才对。使用ReadWriteLock可以解决这个问题,它可以保证:

① 只允许一个线程写入(此时其他线程既不能写入也不能读取);

② 没有写入时,多个线程允许同时读(提高性能);

使用方法:new出来ReadWriteLock()对象实例后,调用该对象的readLock()和writeLock()分别获取读锁和写锁实例,接着在读方法中使用读锁,写方法中使用写锁。

private final ReadWriteLock rwlock = new ReentrantReadWriteLock();

private final Lock rlock = rwlock.readLock();

private final Lock wlock = rwlock.writeLock();

8.使用StampedLock

ReadWriteLock读的过程中不允许写,是一种悲观的读锁。其实读的过程中大概率不会有写操作的发生,所以并发效率有待提高。StampedLock和ReadWriteLock相比,读的过程中也允许获取写锁后写入。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。其提供了将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。

使用方法:new出来StampedLock()对象实例后,调用该对象的readLock()和writeLock()可以分别获取读锁和写锁实例并上锁,同时还会返回版本号,释放的时候调用unclockWrite()或者unlockRead()方法需要传入版本号。调用tryOptimisticRead()获得乐观读锁,同时返回版本号,它在操作数据前并没有通过 CAS设置锁的状态,仅仅通过位运算测试,所以不需要显式地释放锁。通常获取乐观锁,读入数据后,会调用validate()方法传入版本号stamp进行验证,如果中途有写入,则版本号会发生变化,方法会返回false,此时需要通过获得悲观锁再重新读入数据。

9.使用Semaphore

上面的锁保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock)。还有一种受限资源,需要保证同一时刻最多有N个线程能访问,比如同一时刻最多创建100个数据库连接,最多允许10个用户下载等。这种限制数量的锁,用Lock数组来实现很麻烦,此时就可以使用可以使用Semaphore。其本质上就是一个信号计数器,用于限制同一时间的最大访问数量。

使用方法:new出来Semaphore()对象实例,其中传入允许访问的线程数量,在需要控制的方法中,调用acquire()方法,接着完成功能逻辑,最后调用release()方法释放。调用acquire()可能会进入等待,直到满足条件为止。也可以使用tryAcquire()指定等待时间。

10.使用Future

Runnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不方便。Callable接口和Runnable接口比,多了一个返回值功能,并且Callable接口是一个泛型接口,可以返回指定结果的类型。线程池对象的submit()方法提交任务执行后会返回一个Future对象,也支持泛型,其表示一个未来能获得结果的对象。当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。

Future接口定义的方法有:

get():获取结果(可能会等待);

get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;

cancel(boolean mayInterruptIfRunning):取消当前任务;

isDone():判断任务是否已完成。

11.使用CompletableFuture

使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。CompletableFuture针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

使用方式:调用CompletableFuture的supplyAsync()创建CompletableFuture对象,其中传入实现了Supplier接口的对象(无传入值,有返回值),他会被提交给默认的线程执行。调用thenAccept()方法,其接收实现了Consumer接口的对象(有传入值,无返回值),设置执行完成时的回调方法。调用exceptionally()方法,接收实现了Function接口的对象,设置报异常时的回调方法。

12.使用ForkJoin

Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。

比如:计算一个超大数组的和,可以把数组拆成两部分,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行,如果拆成两部分还是很大,我们还可以继续拆,用4个线程并行执行。这就是Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。

使用方法:新建类继承RecursiveTask,其中重写compute方法,设定阈值,如果任务小于设定的阈值,就直接计算,最后返回结果。如果任务大于设定的阈值,就分裂成两个小任务,调用invokeAll()传入两个小任务,再分别调用两个小任务的join()方法来得到返回结果,最后将两个结果加起来返回。

13. 使用ThreadLocal

上下文(Context):在一个线程中,横跨若干方法调用,需要传递的对象,通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

Web应用程序是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,去完成类似以下的工作:检查权限、做工作、保存状态、发送响应。如果这些工作中也需要用到上下文(context),此处就是user实例,可以简单地直接通过参数传入,但是往往一个方法又会调用其他很多方法,这样会导致User传递到所有地方,但是给每个方法都增加一个Context参数非常麻烦,而且如果调用链中有无法修改源码的第三方库,context就传不进去了。ThreadLocal就很适合解决这个问题,它可以在一个线程中传递同一个对象。

public void process(User user) {

checkPermission();

doWork();

saveStatus();

sendResponse();

}

使用方法:ThreadLocal实例通常总是以静态字段初始化,通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例。普通的方法调用一定是同一个线程执行的,所以,该线程中所有方法调用threadLocalUser.get()获取的User对象是同一个实例。ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰,特别注意ThreadLocal一定要在finally中清除。因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。其实,可以ThreadLocal看成一个全局Map:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭