11.定时器和时间管理

2021-10-24 | Linux

内核中的时间概念

系统定时器被用来计算流逝的时间,系统定时器以某种频率自行触发(击中/射中)时钟中断,该频率可以编程修改,叫做节拍率
节拍是两次中断间隔的时间,为1/节拍率秒,墙上时间(实际时间)
时间中断周期中执行的操作

  • 更新系统运行时间
  • 在SMP系统上,均衡调度处理程序各处理器上的运行队列负载
  • 检查当前进程是否用尽了自己的时间片
  • 更新资源消耗和处理器时间的统计值

理想的HZ值

不同体系结构拥有默认节拍率,提高节拍率时:

  • 时钟中断更加频繁
  • 提高时钟中断解析度
  • 时间驱动事件的精度提高
  • 提高系统负担,频繁陷入时钟中断
    无节拍的OS:如果一段时间内无事可做,就延长时钟中断频率,这样可以减少系统损失的能耗

jiffies

这个全局变量用来记录系统启动以来产生的节拍的总数,unsigned long类型,64位的jiffies变量永远不可能溢出,因此一般只取变量的后32位,jiffies回绕是指变量溢出后会自动回到0值

实时时钟

实时时钟(RTC)在系统关闭后可以通过主板上的微型电池提供的电力确保系统的计时,内核读取RTC来初始化墙上时间,存放在xtime变量中。

系统定时器

可编程中断时钟(PIT)在内核定时机制中很重要,采用周期性触发中断机制,有些体系结构利用衰减测量器或电子晶振分频实现

11.5 时钟中断处理程序

Read More

10.内核同步方法

2021-10-24 | Linux

原子整数操作

SPARC体系结构是啥??
原子操作就是不可被分割的操作,原子函数只接受一个特殊的数据类型,叫做atomic_c,这种类型可以屏蔽不同体系结构上数据的差异,也可以确保编译器不对相应的值进行访问优化。
内联函数是啥?

10.2 自旋锁

锁的出现是因为原子操作并不能满足在多个复杂函数共享数据的情况下安全。
linux内核主要是自旋锁,它最多只能被一个可执行线程持有,并且征用时,线程不能睡眠,必须进行忙等待。由于从睡眠中唤醒线程会导致开销,所以往往是短期占有锁/禁止睡眠时才需要自旋锁
要在获取锁之前禁止本地中断,因为中断处理程序会试图打断内核进程而陷入忙等待,同时锁持有者又挂起,就陷入了死锁
锁的是数据而不是代码

自旋锁和下半部

同类的tasklet不可能同时运行,同一个处理器上不可能相互抢占;而软中断会在不同处理器上运行,因此需要自旋锁,同一个处理器上软中断不可能被另外的软中断抢占,不需要自旋锁。
大量读者会使写者处于饥饿状态

10.3 读—写自旋锁

又称(共享/排斥锁;并发/排斥锁),锁可被多个读任务持有,一个写任务持有,读任务可以并发

10.4 信号量

信号量:征用信号量的进程会被加入一个睡眠队列。
它适用于锁被长时间使用的情况,短时间持有的锁,中断上下文中都不可用。

计数信号量和二值信号量

计数信号量:可以记录持有者数量,允许任意数量的锁持有者
二值信号量:锁持有者只能是0,1
读—写信号量downgrade_write()方法动态把写锁转换为读锁

10.6 互斥体

相比信号量,优先用mutex
mutex指可睡眠的强制互斥锁,类似于信号量,只是接口更简单。
只能在同一上下文中对它上锁和解锁,不可递归的上锁和解锁,不可被拷贝,手动或重复初始化。

10.9 顺序锁

seq锁 :主要依靠一个序列计数器,写入操作会导致值被增加,读前和读后查看此锁的数据有没有发生变化就能知道读之前和之后有没有被写入过。
当数据存在很多读者,很少写者,希望写者优先于读者,数据很简单时使用。

Read More

9.内核同步介绍

2021-10-24 | Linux

临界区和竞争条件

临界区就是共享数据的代码段,访问临界区时代码不可被打断。

造成并发执行的原因

由于内核的调度程序,用户进程会在运行时被其他进程抢占,造成伪并发;
内核中造成并发执行的原因:

  • 中断:中断可在任何时刻打断当前代码
  • 软中断和tackest
  • 内核抢占
  • 睡眠和与用户空间的同步:内核进程可能会睡眠导致调度一个新的用户程序运行
  • 对称多处理:进程分别运行在有多个处理器的系统会造成真并发。
    SMP安全代码:对称多处理机器中能避免并发
    抢占安全代码:内核抢占时能避免并发

9.4 征用和扩展性

锁的征用:锁正在被占用时,其他程序试图获得它
扩展性:系统可扩展性的量度,只要是可被计数的设备都有扩展性,但性能和个数不一定成正比。
提高扩展性可以在大型SMP系统,处理能力强大的机器上获得良好效果,比如用一个锁控制一个链表,在很多进程访问它的时候就会遇到扩展性瓶颈,如果用很多锁控制此链表的每个节点就会好很多;但是在访问进程少的情况下,锁的粒度太细就会造成性能损耗。

Read More

8.下半部和推后执行的工作

2021-10-24 | Linux

下半部机制:内核中所有将工作推迟的机制都叫下半部机制
1.中断处理程序以异步方式执行,并可能打断其他代码
2.不能消耗太长时间
3.中断处理不在进程上下文中运行,所以不能阻塞
所以一般将时间敏感的操作硬件的不可被中断的程序放在(中断处理程序)上半部中

下半部的重要性

为了缩短中断处理程序的响应时间及其他程序被屏蔽的时间,如果系统不太繁忙,一般上半部返回时就立即执行下半部

8.1.2 下半部的环境

BH:它提供了一个静态创建,由32个bottem harves组成的链表,上半部提供一个数来确定执行哪个,此机制在所有cpu上只能运行一个中断
任务队列:内核定义了一组队列来挨个执行函数组成的链表,此机制用来代替BH
软中断和tacklst:软中断是静态定义的32个下半部接口,可以在多个cpu上同时运行相同/不同的中断;不同类型的tacklst可在不同处理器上同时运行,相同类型就不可以

8.2 软中断

软中断在编译期间是静态分配https://www.cnblogs.com/zhxshseu/p/5293979.html
软中断不会抢占软中断,它只能被中断处理程序抢占,最多有32个软中断
触发软中断指软中断被标记后才能执行
在每个处理器上单独的地址空间运行中断,避免资源加锁
https://www.sohu.com/a/216557079_236714

8.3 tacklst实现

tacklst是用tasklst结构体实现,结构体被放在一个链表中,每个结构体都是一个tasklst,tacklst是动态分配,tacklst是用软中断实现的
4.ksoftirqd:当内核出现大量软中断时,cpu将被中断处理程序占满,ksoftirqd是用来处理这些大量软中断的。
如果内核忙于处理中断程序,用户就会陷入饥饿,反之内核陷入饥饿,ksoftirqd做一个折中:每个cpu上会有一个内核线程,在最低的优先级上运行,通过死循环保证只要有空闲的处理器就会处理软中断
内核饥饿:把自行触发的软中断放到下一次中断返回之后去处理,确保及时响应用户
用户饥饿:软中断可能会在执行时重新触发自己使自己再次得到执行,立即处理会造成cpu负载过重

8.4 工作队列

可以把工作推后,交给一个内核线程执行,这个下半部总会在进程上下文中执行,所以可以睡眠,所以需要睡眠的任务,就给工作队列,不需要睡眠的就给软中断/tacklst
工作队列子系统可以创建一个内核线程接口,用来执行排队的线程,这些线程叫做工作者线程,一般由缺省的工作者线程来处理,处理器密集型任务会有自己的工作者线程,确保性能

工作队列的结构

所有工作者线程在一个workqueue_struct结构体中,结构体内有一个数组对应着很多处理器和很多工作者线程,每个工作者线程用cpu_workqueue_struct()表示,同时对应一个链表,每个链表节点有一个work_struct死循环实现,链表都处理完时就休眠

8.5 下半部机制的选择

1.软中断:需要采取一些步骤保证共享数据安全,如果完全使用单处理器变量,那么软中断就很好
2.tasklet:本质上就是用软中断实现的,实现起来更简单,不能并发运行,保证了资源安全,性能比软中断差一点
3.工作队列:如果需要睡眠,就用工作队列,此机制实现简单,开销也最大,因为涉及到上下文切换和内核线程

8.7 禁止下半部

为了保护内存资源可能要先得到一个锁然后禁止下半部执行

Read More

7.中断和中断处理

2021-10-24 | Linux

7.1 中断

中断是一个由硬件产生的电信号,先通过中断线传给中断处理器,中断控制器收到此硬件的中断信号之后会通过地址总线存入一个该设备的编号,表示这次中断需要关注的设备,在由中断处理器传信号给cpu,然后cpu从地址总线取出设备编号,通过编号找到中断向量所包含的中断服务的入口地址,压入 PC 寄存器,然后内核陷入一个中断处理程序ISR)来处理这个中断
不同的设备对应着不同的中断,不同的中断由叫中断请求线(IRQ)的值来唯一标识
异常和中断的区别:当执行代码时出现特殊情况:如错误指令,时,cpu会产生一个异常,通知内核处理,而中断来自处理器外部,而异常是执行某指令的结果

7.2 中断处理程序

程序通过特定代码去响应一个中断;
设备驱动程序:一个设备的中断处理程序,是对设备管理的内核代码;
linux中断处理程序就是C代码,运行于中断上下文中(原子上下文),中断处理程序有时间限制,还要通知硬件是否收到信号

7.3上半部和下半部

上半部指很迫切需要执行的代码,需要立即执行且有时间限制
下半部指允许稍后完成执行的代码
网络硬件的例子:网卡传中断信号给cpu,中断上半部需要立即复制网络数据包到系统内存,然后把控制权交给中断之前的代码,然后下半部再挑时机对数据包处理后再交给协议栈或应用程序

7.4 注册中断处理程序

request_irq()函数注册一个中断处理程序:
1.irq参数表示要分配的中断号
2.hander是一个指向处理函数的指针
3.flags参数可以为0或以下标志的位掩码:
·
IRQF_DISABLED
此参数表示不能同时运行两个同cpu的中断处理程序
·IRQF_SAMPLE_RANDOM:来自设备中断的间隔时间会作为熵值填充到内核熵池
·IRQF_SHARED:在多个处理程序之间共享中断线
4.name与中断设备关联的文本表示
5.dev参数用于区分共享中短线上的诸多处理程序
这个函数可能会睡眠,不能在不允许阻塞的函数中调用它
**free_irq()**删除指定的中断处理程序,如它所在的中断线上只有一个程序,则禁用此中断线
内核接受到中断时,检查此中断线上的每个程序

7.6 中断上下文

进程上下文:进程已执行过的字段/数据(存放在堆栈中),进程执行活动全过程的静态描述
中断上下文尽量节约时间和内存栈

7.9 中断控制

可禁止/激活整个处理器上所有的中断函数,或只禁止某一条中断线

Read More

5.系统调用

2021-10-24 | Linux

graph LR
A[应用程序]--调用-->B[库函数]
B--调用-->C[系统调用]
C--指挥-->D[操作系统]

POSIX是一套系统调用API规范,应用于LINUX,UNIX,macOS

5.3系统调用

系统调用会通过一个long类型返回值来表示成功或者错误,0为成功,负值是错误,错误码被保存在errno全局变量中。系统调用限定词为asmlinkage,为了兼容32位和64位,用户空间返回值为int,内核空间为long,另外系统调用和内核命名规则也不一样

5.3.1 系统调用号

每个系统调用都有一个系统调用号,此号不能轻易改变,删除一个号之后,用‘未实现系统调用函数’:sys_ni_syscall()填补空缺,该函数返回-ENOSYS,这些号被存在系统调用表里

系统调用处理程序

用户空间函数不可直接访问内核空间,但可以间接通知内核,使内核陷入内核态,使用软中断:system_call()实现内核陷入。
系统调用把
系统调用号
参数分别通过eax和其他五个寄存器传给内核

graph TB
A[用户空间函数]-->B[系统调用]
B--触发软中断-->C[system_call函数检查系统调用号是否小于NR_syscalls值]
C-->D[函数陷入内核空间检查参数权限合法性]
D-->E[系统调用号和参数通过寄存器传入内核]

参数验证

系统调用必须检查参数的合法性,不能传入访问不到的地址,比如:其他进程空间的,内核空间的,内存的只读只写。。。访问限制
权限的合法性通过suser()检查是否为超级用户,capable()检查对资源操作的权能

系统调用上下文

绑定系统调用

在entry.s文件里的调用表里添加一个表项,在**<asm/unisted>设置系统调用号,系统调用要被编译进内核映像kernel/**里

用户访问系统调用

用户可通过一组linux宏访问系统调用,它会设置好寄存器并调用陷入指令,
宏的形式为:

NR_open 5
_syscall3(long, open, const char*,filename,int,flags,int,mode) 第一二个参数对应着返回类型和系统调用名字

Read More

4.进程调度

2021-10-24 | Linux

4.1多任务

多任务操作系统:能并发执行多个进程
抢占式:可以强制性挂起一个进程
非抢占式:不可挂起

I/O消耗性和处理器消耗性的进程

I/O消耗进程多数时间用来等待文件输入,运行时间很少,处理器消耗反之

4.2 linux 调度算法

linux调度器Schedule()选择最优的拥有进程的调度类,调度类schedule()再选择最优的进程运行,进程运行的时间取决于它被分配到的时间片的多少
进程优先级:nice值来设置优先级(-20~19)默认是0,值越大优先级越低
时间片:进程被强占前可运行时间
CFS(公平调度):大多数普通进程都用CFS策略调度,CFS没有时间片的概念,而是根据目标延迟值/进程的数量计算出处理器分配的份额,nice会成为处理器分配的权重,每个进程分配到的份额都相等,但处理器使用比也是可变的,当进程使用cpu的时间远小于被分配的份额时,另一些进程会抢占此进程,重新计算这个使用比;
这里有一个问题就是当进程无限多时,处理器使用比会趋于0,从而内核忙于调度,因此设置一个时间片底线(最小粒度)为1ms

linux调度的实现

4.5.1时间记账

每一个时钟节拍会使得时间片减少一个节拍周期,当节拍周期减到0时,它会被其他进程抢占
CFS里使用调度器实体结构来记账,它的实体结构被放在进程描述符里
viruntine变量:虚拟运行时间,记录一个程序运行了多长时间,相关函数计算

4.5.2进程选择

挑选viruntine最小的进程作为最大优先级,CFS使用红黑树管理进程,最左叶子节点是最大优先级的进程,进程堵塞或终止时从红黑树删除,陷入等待时被标记成休眠状态,加入等待队列并设置成不可执行状态,再从红黑树中删除,事件发生时再改为可运行状态,再把它加入运行队列,再从等待队列删掉
need_reached描述符表示是否需要重新调度

4.6.1 用户抢占

系统调用/中断处理程序要返回用户空间的时,会检查need_reached,如果设置了,就会重新调度

4.6.2 内核抢占

1.中断处理程序正在执行,且返回内核空间之前
2.内核代码再一次拥有可抢占性
3.内核任务显示(阻塞也会导致)调用schedule()

实时调度策略

就是比某进程优先级高的进程才可以抢占它
SCHED_FIFO没有时间片,而SCHED_RR带有时间片
软实时:进程时间片用完前可能被抢占
硬实时:不会被抢占

4.8与调度相关的系统调用

4.8.2处理器绑定

可以通过设置位掩码(保存在进程task_struct标志中)指定某进程只能在哪些cpu上运行,每一位标志着一个处理器,子进程继承了父进程的位掩码

4.8.3 放弃处理器时间

sched_yield()系统调用可把某进程调为最小优先级,并放入过期队列,确保其一段时间不会被执行

Read More

3.进程管理

2021-10-24 | Linux

3.1 进程:处于执行期程序和相关资源的总称

线程:私有:进程栈,程序计数器,进程寄存器
进程资源:打开的文件,挂起的信号,内核内部数据,处理器状态,内存地址空间,数据段
调用fork()来创建子进程
exee()创建地址空间
exit()退出进程
父进程调用wait4()查看子进程是否终结
陷入等待时,进程退出执行后被设置为僵死,直到父进程调用wait(),waitpid()

进程描述符及任务结构

进程描述符task_struct结构体里的成员变量被用来描述进程信息,slab分配器用来分配task_struct,thread_info结构体内部有一个指向进程描述符的指针,它被存放在进程内核栈的底部,PID号唯一的标识了某个进程,PID最大值实际上是内核中允许同时存在进程的总数,最大值越小,转一圈就越快,且数值大的进程迟运行。
current宏负责查找当前进程的描述符

进程状态

1.TASK_RUNNING 运行:进程是可执行的
2.TASK_INTERRUOTIBLE可中断的:等待状态
3.TASK_UNINTERRUOTIBLE不可中断:等待状态,并不会被信号唤醒
4.TASK_TRACED被其他进程跟踪的进程
5.TASK_STOPPED停止执行

3.2.6 进程家族树

所有进程都是一个PID为1的init进程的后代,这个进程在内核启动最后被调用,用于初始化并启动相关进程,此进程的描述符init_task是静态分配的
遍历可执行双向链表可以遍历系统中所有进程

写时拷贝

在创建子进程时,如果子进程不需要写入,就不必拷贝一份父进程资源
fork():1.创建内核栈,描述符和描述符指针
2.检查子进程数量是否超过限制
3.把拷贝来的描述符里的某些值初始化
4.设置为不可中断态
5.设置进程PID
6.根据传递给clone()的参数(资源权限描述),进行各种资源分配
vfork():不拷贝父进程页表项,只能读

3.4 进程在linux中的实现

linux内核把线程当作进程对待,线程没有什么私有资源的特别描述,只有共有资源的描述
clone()可加参数来设定创建子进程的需共享资源

内核线程

在内核后台运行,跟用户空间没有交集,也没有被分配进程空间

3.5 进程终结

终结时释放所有资源,并把其告诉父进程:记账信息,文件描述符,文件系统的引用计数,切换到新进程。。。
如果子进程正在执行时父进程退出了,它就会僵死在那里永远不会被释放,变成孤儿;为了防止,要给它和它的兄弟们找养父进程,如果它所在的线程组没有其他进程,就把它交给init进程;找到它的兄弟,只需遍历子进程链表;
init会调用wait()清除所有的僵死进程;

Read More

4.对象的组合

2021-10-24 | java并发编程

设计线程安全的类:

  • 找出构成对象状态的所有变量(变量组成对象状态的n元组)
  • 找出约束状态的不变性条件
  • 建立对象状态的并发管理策略

收集同步需求

确保线程的不变性不会在并发访问时被破坏,需要对状态进行判断。后验条件判断状态迁移是否有效,不变性条件判断状态是否有效,执行前也会有先验条件。

实例封闭

将数据封装在对象内部,可以将数据的访问限制在那个对象的方法上,从而确保数据访问时总能有正确的锁。
在Java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。一些基本的容器类例如ArrayList不是线程安全的,但类库提供了包装器工厂方法,例如Collections.synchronizedList及其类似方法,使得这些非线程安全的类可以在多线程环境中安全地使用。这些工厂方法通过”装饰器Decorator”模式将容器封装在一个同步的容器对象上,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),那么它就是线程安全的。在这引起方法的Javadoc中指出,对底层容器对象的所有访问必须通过包装器来进行。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。

监视器模式

Read More

3.对象的共享

2021-10-24 | java并发编程

同步可以确保以原子的方式实现操作,还可以确保内存可见性,即确保一个线程修改了对象状态后,另一个线程可以看见.

策略:
1.线程封闭
2.只读共享
3.线程安全共享
4.保护对象

可见性

即”内存可见性”,当共享变量在某个线程中变化时,其他线程在使用该共享变量前能得到变化后的值,仿佛这个变量的一动一静所有线程都能察觉一样.
synchronized 块是能保证块中的共享变量的可见性的。首先它要求写该变量的时候进行同步。它是如何做到可见性的呢?当变量在同步块中改变后,在退出同步块时会迫使该变量马上把修改后的值告诉主内存,并且更新所有线程中的拷贝。这样就保证了变量的可见性。

除了同步块,还有一种方式可使变量保持可见性:volatile 关键字。它的实现方式是迫使该关键字修饰的变量写之后马上更新到主内存,使用之前总会再从主内存中取最新值,通过这种貌似放弃线程私有拷贝的方式来保证可见性。

非原子的64位操作

非volatile的long,double变量,JVM会在读写中把它分解为两个32位操作,如果读写操作在不同线程中执行,那么就可能读到某个值的高32位和低32位.

赋值操作的步骤

1.分配变量空间
2.引用指向变量
3.初始化
(在非volatile的情况,指令重排导致2,3可倒序)

volatile

在一个多线程的应用中,线程在操作非volatile变量时,出于性能考虑,每个线程可能会将变量从主存拷贝到CPU缓存中。如果你的计算机有多个CPU,每个线程可能会在不同的CPU中运行。这意味着,每个线程都有可能会把变量拷贝到各自CPU的缓存中,意味着缓存和主存保存的变量可能不一样,,如下图所示:

完整的volatile可见性保证

public class MyClass {
02
    private int years;
03
    private int months
04
    private volatile int days;
05
 
06
 
07
    public void update(int years, int months, int days){
08
        this.years  = years;
09
        this.months = months;
10
        this.days   = days;
11
    }
12
}

days变量写入主存时,前面两个也会被写入.
读取volatile变量时,也同样会触发线程中所有变量从主存中重新读取。因此,应当尽量将volatile的写入操作放在最后,而将volatile的读取放在最前,这样就能连带将其他变量也进行刷新。(但是指令重排造成了例外)
局限性:只确保了可见性,并未确保原子性,应使用synchronized保证读写变量是原子的.
性能:读写volatile变量会导致变量从主存读写。从主存读写比从CPU缓存读写更加“昂贵”。访问一个volatile变量同样会禁止指令重排,而指令重排是一种提升性能的技术。因此,应当只在需要保证变量可见性的情况下,才使用volatile变量。

发布与逸出

发布:对象能够在当前作用域之外的代码中被使用.
如果在对象构造完成之前就发布它,就会破坏线程安全性,如果不应该发布的对象被发布了,就被称为逸出.

线程封闭

一种避免使用同步的方式就是不共享数据,仅在单线程中访问数据,就不存在同步.
如JDBC中线程从连接池中获取一个connection对象,用完再还给连接池,这就隐含地把对象封闭再线程中.

栈封闭

**”栈”**是指java虚拟机栈,或者说是虚拟机栈中局部变量表部分。首先Java虚拟机栈是私有的,它的生命周期和线程相同。Java虚拟机栈描述的Java方法执行的内存模型:每个方法在执行时都会创建一个“栈帧”,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应这个一个栈帧在虚拟机中入栈到出栈的过程。

为什么局部变量是线程安全的呢?因为局部变量存放在虚拟机栈中,而虚拟机栈是线程私有的,既然线程不共享,所以它是线程安全的。封闭栈的线程安全性体现在Java虚拟机的内存特性。

ThreadLocal类

当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的,通过将对象保存到ThreadLocal中,每个线程都会拥有属于自己的对象。

ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。**ThreadLOcal 包含了Map<Thread,T>对象.

/** 线程封闭示例 */
public class Demo7 {
    /** threadLocal变量,每个线程都有一个副本,互不干扰 */
    public static ThreadLocal<String> value = new ThreadLocal<>();
 
    /**
     * threadlocal测试
     *
     * @throws Exception
     */
    public void threadLocalTest() throws Exception {
 
        // threadlocal线程封闭示例
        value.set("这是主线程设置的123"); // 主线程设置值
        String v = value.get();
        System.out.println("线程1执行之前,主线程取到的值:" + v);
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                String v = value.get();
                System.out.println("线程1取到的值:" + v);
                // 设置 threadLocal
                value.set("这是线程1设置的456");
 
                v = value.get();
                System.out.println("重新设置之后,线程1取到的值:" + v);
                System.out.println("线程1执行结束");
            }
        }).start();
        Thread.sleep(5000L); // 等待所有线程执行结束
        v = value.get();
        System.out.println("线程1执行之后,主线程取到的值:" + v);
 
    }
 
    public static void main(String[] args) throws Exception {
        new Demo7().threadLocalTest();
    }
}
# 结果
线程1执行之前,主线程取到的值:这是主线程设置的123
线程1取到的值:null
重新设置之后,线程1取到的值:这是线程1设置的456
线程1执行结束
线程1执行之后,主线程取到的值:这是主线程设置的123

说明主线程和子线程中的执行结果互不干扰.

不变性

用不可变对象来满足同步需求,它永远是安全的
不可变三个条件:

  • 创建以后状态不变
  • 所有域都是final类型
  • 正确创建的(没有this引用逸出)

安全发布

错误的发布对象:

私有成员变量在对象的公有方法中被修改。当其他线程访问该私有变量时可能得到不正确的值。

https://blog.csdn.net/MyySophia/article/details/104784202

安全发布的常用模式

  • 静态初始化函数中初始化一个对象的引用
  • 对象的引用保存到一个voliate类型的域或者AtomicReferance对象中
  • 保存到某个正确构造对象的final域
  • 保存到某个由锁保护的域

事实不可变对象:从技术上来看可变,但发布后不会改变,视为不可变对象即可,要通过安全方式发布
可变对象:必须通过安全方式来发布,并且一定要是线程安全且由锁保护

Read More