23 Oct 2021
期权的定价模型很复杂, 不过价格的上下界是比较好确定的. 先简单介绍下期权, 期权是一种未来的权利, call 的期权是未来以一定价格买入资产的权利, put 的期权是未来以一定价格卖出资产的权利. 既然是一种权利, 那么就可以选择行使或者不行使这个权利. 需要注意的是, 卖出 call 或者 put 的期权一方, 是必须确保买入一方权利可以正常行使的, 保证的方式是通过保证金制度来进行的.
金融衍生品的价格需要满足的一个条件就是市场无法在不承担风险的情况下获得超过无风险利率的收益. 下面的讨论我们假设期权是欧式期权 (只能在期权到期日行权), 其底层的金融资产是股票, 现价是 S
, 市场无风险利率为 r
, 期权的行权价为 K
, 期权到期时间为 t
.
价格上界
先来看下 call 的期权, 我们假设现在 call 期权的价格为 C
, 超过了其上界, 因为其价格超过了上界, 那么我们这时候就应该卖出期权, 这样我们就获得了现金 C
, 卖出 call 期权意味着在最差情况下到了期权行权日需要卖出股票给买入期权的一方, 为了抵消风险我们需要在卖出期权的时候以现价 S
买入对应股票, 这样我们就付出了现金 S
. 这样的资产组合我们在期权到期时是不用承担任何风险的. 只要 C > S
就存在套利空间, 所以 call 的期权的上界为:
\[ C \leq S \]
下面我们看 put 的期权, 假设现在 put 期权的价格为 P
, 超过了其上界, 因为其价格超过了上界, 那么我们这时候就应该卖出, 这样我们就获得了现金 P
, 卖出 put 期权意味这在最差情况下到了期权行权日需要以价格 K
购买买入期权一方的股票. 现在的现金 P
到了行权日会以无风险利率 r
增值到 \(Pe^{rt}\). 只要 \(Pe^{rt} > K\) 就存在套利空间, 所以 put 的期权的上界为:
\[ P \leq Ke^{-rt} \]
价格下界
下界的确定和上界的确定是类似的, 只是多了一个额外的情况, 就是期权的价格至少是大于等于零的, 因为如果期权的价格为负, 那么期权的买入方的收益肯定是正的. 我们先来考虑 call 的期权. 同样的, 假设现在 call 期权的价格为 C
, 低于其下界, 因为价格低于下界, 我们这时应该买入期权, 这样我们就付出了现金 C
, 买入期权意味着到了期权行权日我们至少可以以价格 K
买入股票, 所以我们可以在现在做空股票, 这样就获得了现金 S
, 此时我们手上有现金 S - C
. 到了期权行权日, 我们的现金将以无风险利率 r
增值到 \((S-C)e^{rt}\), 同时为了平仓之前的做空操作, 我们最多要付出 K
的现金来买入股票, 那么只要 \((S-C)e^{rt} > K\) 或者 C < 0
就存在套利空间, 所以 call 的期权的下界为:
\[ C \geq MAX(S - Ke^{-rt}, 0) \]
下面看 put 的期权, 假设现在 put 的期权的价格为 P
, 低于其下界, 同样的, 我们买入期权, 这样就付出了现金 P
, 买入期权意味着我们在期权行权日至少可以以价格 K
卖出股票, 所以我们可以做多股票, 为此我们付出了现金 S
, 现在我们总共借入了现金 S + P
, 因为无风险利率为 r
, 所以到了期权行权日我们需要偿还 \((S+P)e^{rt}\) 的现金, 同时我们之前做多的股票至少可以卖出 K
的价格, 那么只要 \((S+P)e^{rt} < K\) 或者 P < 0
就存在套利空间, 所以 put 的期权的下界为:
\[ P \geq MAX(Ke^{-rt} - S, 0) \]
26 Dec 2020
backtrader 支持在线实时的数据源输入, 目前支持海外的 IbPy (盈透), Oanda 等方式的接入, 不支持我准备用的富途, 于是准备看源码参照 IbPy, Oanda 接入的方式, 自己对接下富途. 看了下源码, 发现用到了 Python 的几个特性: 装饰器, 描述符, 元类. 以前简单接触过, 理解不是太深, 所以这次就稍微整理下.
装饰器 decorator
Decorator 还是比较好理解的, 它本质上来说就是个函数 (严格来说只要是 callable 的就行), 如果你把它放在方法上, 那么它的入参就是这个方法, 返回的新方法就会替代原本的方法定义. 如果放在类上, 那么它的入参就是这个类, 返回的新类会替代原本的类定义.
Decorator 有两种形式, 一种就是直接的 @decorator
形式, 这个就是我上面说的. 另一种是 @decorator(arg1, arg2, ...)
这种用法其实是前一种的变体, decorator(arg1, arg2, ...)
就是个简单的方法调用, Python 会把它的返回值当成真正的装饰器来解析.
描述符 descriptor
Descriptor 的定义很简单, 只要是一个类定义了下面几个方法, 那么它就是一个描述符:
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
如果一个描述符只定义了上述方法中的 __get__
方法, 那么它就是一个 non-data 的描述符, 如果除了 __get__
以外还定义了其它的方法 ( __set__
或者 __delete__
), 那么就是一个 data 的描述符. data 与 non-data 的描述符有什么区别? 要回答这个问题, 我们要先明白, 描述符的作用是什么. 首先描述符只有被定义为类变量 (class variable [1]) 时才有意义, 当你使用描述符时, Python 会调用相应的描述符的方法而不是直接操作对应的描述符. 可以理解为描述符提供了一种对类变量的 get ( Clz.descr
), set ( Clz.descr = x
), delete ( del Clz.descr
) 进行重载的机制. 不过这边有点不符合直觉的是, 一般我们重载方法的时候是重载的主语, 所以按道理来说我们要重载 Clz.descr
这个表达式的话, 应该是在 Clz
中定义 __get__
来重载, 但是描述符的重载是在宾语上发生的, 我们是在 descr
上定义的 __get__
方法. 不过这个也好理解, 在 descr
定义就不用在每个用到描述符的地方都重复定义一遍了.
下面来回答 data 与 non-data 的描述符的区别. 当我们使用 obj.descr
表达式时, 如果对象中找不到 descr
Python 就会找到 Clz.descr
然后走描述符那套处理逻辑, 那么对象中有同名的 descr
时会怎么样呢? 这时候就要看 descr
是 data 还是 non-data 了. 如果是 data 的, 那么 Clz.descr
的优先级就高于 obj.descr
. 如果是 non-data 的, 那么 obj.descr
的优先级就高于 Clz.descr
不会走描述符那套逻辑. 具体解析顺序的实现在 object.__getattribute__()
中, 下面是用 Python 描述的实现逻辑:
def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
常见的 @staticmethod
与 @classmethod
就是将装饰器与描述符结合起来的一种应用. 下面是对应的 Python 的实现版本:
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
class MethodType:
"Emulate Py_MethodType in Objects/classobject.c"
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# 这段逻辑是 Python 3.9 新增的, 用于实现描述符的链式调用 [1]
# 原文档里上面的 if 判断条件是 hasattr(obj, '__get__'), 这个是错误的
return self.f.__get__(cls)
return MethodType(self.f, cls)
元类算是这几个概念里比较难理解的了, 不过只要我们记住在 Python 里任何东西都是对象这个知识点, 那么理解元类就比较简单了. 任何东西都是对象, 那么类自然也不例外. 类的类被称为元类, 一般情况下, 我们通过 class
定义的类的元类是 type
, type
的元类是 type
自身. 我们通过 type(Clz)
表达式来查看一个类的元类是什么.
在 Python2 中要指定一个类的元类通过 __metaclass__
这个类变量来指定, 要注意的是, 只有当我们使用 class
关键字来定义一个类的时候, __metaclass__
才生效 [2]. Python3 中使用下面的语句来指定一个类的元类:
class Clz(metaclass=type):
pass
哪些对象能作为元类的? 答案是任何 callable 的对象. 当你指定了元类, Python 在创建类的时候会调用你指定的元类, 并传入以下参数:
- name: 当前类的名字
- bases: 当前类的基类
- attrs: 当前类的属性
一个类的默认的元类是 type
, 所以当你使用 class
关键字来定义类时, 等价于下面两条语句:
class Clz(object):
hello = 'world'
# 等价
Clz = type('Clz', (object,), {'hello': 'world'})
# 等价
Clz = type.__new__(type, 'Clz', (object,), {'hello': 'world'})
上面两种写法有什么区别呢? 只要了解了 __init__
和 __new__
的区别就明白了. __init__
我们都知道, 当我们执行 Clz(arg1, arg2)
创建对象时, Python 会执行 Clz.__init__(self, arg1, args)
其中 self
表示当前正在创建的对象. 很自然的一个问题就是, self
哪里来的呢? self
就是通过 __new__
创建出来的. 总结下就是, __new__
创建对象, __init__
初始化对象, 所以当我们执行 Clz(arg1, arg2)
时等价于执行下面的语句:
# Clz(arg1, arg2) 等价于
obj = Clz.__new__(Clz, arg1, arg2)
obj.__init__(arg1, arg2)
顺便再多说几句, 官方文档里提到 [3]:
If new() does not return an instance of cls, then the new instance’s init() method will not be invoked.
也就是说, 如果我们重写 __new__
方法, 但是 __new__
返回的对象的类不是第一个参数指定的类,
那么新创建的对象的 __init__
方法不会被调用:
class Foo:
def __new__(cls, *args, **kwargs):
print('Foo new')
return Bar.__new__(Bar, *args, **kwargs)
class Bar:
def __init__(self):
print('Bar init')
# 不会调用到 Bar 的 __init__ 方法
Foo()
说了这么多的废话, 好像和元类一点关系都没有. 其实不是的, 我们看下 type
的 __new__
方法的定义, 第一个参数表示要创建的对象的类, 而我们现在创建的对象就是一个类, 类的类可不就是我们所说的元类吗? 需要说明的是, 如果使用 type
的 __new__
方法来创建类并指定元类, 那么就仅仅是指定了新创建类的元类, 新类并不是通过 call
元类来创建的.
最后我们说下元类的继承问题, 一句话, 一个类的元类如果没有指定的话会从父类继承. 虽然说任何 callable 的对象都可以当做元类, 但是如果我们想要写个可以被继承的元类就需要用 type.__new__
来创建类:
class MetaClz(type):
def __new__(mcs, *args, **kwargs):
return type.__new__(mcs, *args, **kwargs)
class Foo(metaclass=MetaClz):
pass
class Bar(Foo):
pass
MetaClz
的 __new__
方法中创建了一个类, 并且指定了这个类的元类, 所以最终创建出来的 Foo
的元类是 MetaClz
, Bar
会继承 Foo
的元类 MetaClz
, 所以 Bar
的元类也是 MetaClz
. 我们再看另外一种错误的写法:
class MetaClz(type):
def __new__(mcs, *args, **kwargs):
return type(*args, **kwargs)
class Foo(metaclass=MetaClz):
pass
class Bar(Foo):
pass
和之前的写法唯一的区别就是我们直接用了 type()
来创建类, 这样写的话, 最终创建出来的 Foo
的元类是 type
, Bar
会继承 Foo
的元类 type
, 所以 Bar
的元类也是 type
. 那么谁的元类是 MetaClz
呢? 答案是下面这个语句的元类是 MetaClz
:
# 这条语句的元类是 MetaClz
class Foo(metaclass=MetaClz):
pass
[1] Descriptor HowTo Guide
[2] Inheritance of metaclass
[3] Data model
21 Nov 2020
更新
打电话给电信把光猫改成桥接模式,在路由器输入宽带账号和密码始终连接不上,摸索了一阵发现,原来使用 PPPoE 模式时,路由器 CPU 的口子不再是 8 口,而是 5 口。同时,从光猫来的数据送到 5 口时需要是 Untagged 的。另外就是在给 CPU 的口子配 Untagged 时必须强制指定 U,否则就是 T 的。
原文
家里网络很早之前就改造好了, 今天忽然想要改下交换机的配置, 但是因为时间太长, 一下次想不起来怎么连上交换机了. 折腾了好一会才连上, 所以决定把家里网络的相关改造记录下, 做个备份.
为什么需要单线复用?
现在我们上网通常只需要一个电信猫, 这个猫一般是具备直接拨号的和 WIFI 连入的, 只是性能和可玩性比较差, 所以我们还是会再接一个路由器, 我用的是淘宝淘的二手 NETGEAR R6300v2. 这样一来, 我们就需要把电信猫和路由器同时放到网络设备箱里, 如果你像我一样用的是 R6300v2 这种比较大的路由器可能就放不下了, 另外无线路由器放在网络设备箱里也会影响无线信号. 最好的办法就是, 无线路由器可以放到任意有有线的位置, 比如客厅. 但是像客厅这些地方, 一般只有一根网线用于连接路由器的 WAN 口, 这样其他地方的网线就无法直接与路由器相连了. 怎么办呢? 这时候就需要我们进行单线复用了, 也就是通过 WAN 口既可以与电信猫传输数据, 又可以与本地 LAN 的设备通信.
前置知识点
实现单线复用用到的关键手段是 VLAN.
VLAN
当我们本地网络有 N 个设备, 这些设备需要相互通信的话, 就需要借助于交换机. 有时候出于安全考虑, 我们希望本地网络中可以手动划分设备分组, 只有被划到一个分组中的设备可以相互通信, 不同组之间的设备相互隔离. 要实现这个功能, 就需要用到 VLAN. 现在通常用的 VLAN 协议是 802.1Q, 协议会在数据链路层的数据包中插入 VID (VLAN ID), 交换机根据 VID 进行相应的数据处理转发. 在 802.1Q 协议之前, 还有 Port-based VLAN, 使用这种 VLAN 协议, 数据包只在交换机内部有 VLAN 的概念, 出了交换机, 数据包就和普通的数据包没有区别了, 所以 Port-based VLAN 无法进行交换机的级联. 我们主要介绍 802.1Q 的 VLAN 协议. 对于交换机上的端口来说有三种类型:
这几种类型有什么区别呢? 我们从端口收到数据的处理方式来看. 对于一个端口来说, 它可以从交换机外部接收到数据, 也可以从交换机内部接收到数据. 对于前面一种我们暂时把它叫做 input, 后一种叫做 receive. 对于 input 来说, 有可能接收到不带 VID 数据, 也可能接收到带 VID 的数据. 对于 receive 来说接收到的都是带 VID 数据. 顺便再说下, 每个端口还有一个 PVID (Port VLAN ID) 的属性, 一个端口可以属于多个 VLAN, 但是只能有一个 PVID. 下面我们就从前面说的几个方面来看几种端口类型的区别:
- Access
- input
- 有 VID: 直接丢弃
- 无 VID: VID 打上 PVID 的标, 再发送给属于同一个 VLAN 的端口
- receive
- VID 与 PVID 相同: 将 VID 剥离后转发出去
- VID 与 PVID 不同: 直接丢弃
- Trunk
- input
- 有 VID: 端口属于 VID 的 VLAN 就转发, 不属于就丢弃
- 无 VID: 同 Access 处理方式
- receive
- VID 与 PVID 相同: 同 Access 处理方式
- VID 与 PVID 不同: 端口属于 VID 的 VLAN 就直接转发 (不剥离 VID 信息), 否则丢弃
- Hybird
- input
- 有 VID: 同 Trunk 处理方式
- 无 VID: 同 Access 处理方式
- receive
- VID 与 PVID 相同: 同 Access 处理方式
- VID 与 PVID 不同: 如果端口不属于 VID 的 VLAN 就直接丢弃, 如果属于 VID 所在 VLAN 还要看端口在该 VID 下是配置的 U (Untag) 还是 T (Tag), 如果是 U 就将 VID 剥离后转发, 否则就直接转发
WAN
之所以单线复用可以成功的另一个因素是, 路由器区分 WAN 口与 LAN 的底层是基于 VLAN 实现的. WAN 口与 LAN 口属于不同的 VLAN , CPU 根据 VID 来决定是按照 WAN 还是 LAN 来处理. 所以只要我们交换机将从电信猫过来的数据打上和 WAN 相同的 VID, 从本地网络的过来数据打上与 LAN 相同的 VID, 路由器就可以正常工作了.
实操
知道了原理, 剩下的就好办了.
交换机配置
先连上交换机, GS108PEv3 是可网管的交换机 (v2 是不行的, 只能用专用的工具连接). 如果不知道交换机的 IP 可以用 Netgear 提供的工具来设备发现 (需要与交换机直连, 中间不能有路由器) [1, 2, 3].
网件的端口都是 Hybird 类型的. 我这边是将 5 号口与路由器 WAN 口相连, 8 号口与电信光猫相连, 其余的口用于 LAN 设备连接.
VLAN ID |
Port Members |
1 |
1U 2U 3U 4U 5T 6U 7U |
2 |
5T 8U |
VLAN 1 中的 5 号口设置成 T, 其余口设置成 U. VLAN 2 中 5 号口也设置成 T, 8 号口设置成 U. 最后把 8 号口的 PVID 设置成 2, 其余口设置成 1.
顺便说下, 网件交换机里, T 表示 Tagged, U 表示 Untagged, 没有标记表示这个端口不属于这个 VLAN [4].
路由器配置
我们先 SSH 到路由器, 对于 R6300v2 使用 robocfg show
查看 VLAN 设置:
# robocfg show
Switch: enabled
Port 0: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 1: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 2: 1000FD enabled stp: none vlan: 1 jumbo: off mac: 00:11:32:bb:c3:45
Port 3: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 4: 1000FD enabled stp: none vlan: 2 jumbo: off mac: 38:f9:d3:19:0a:6b
Port 5: 1000FD enabled stp: none vlan: 1 jumbo: off mac: dc:ef:09:94:2d:f7
Port 7: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 8: 1000FD enabled stp: none vlan: 1 jumbo: off mac: dc:ef:09:94:2d:f7
VLANs: BCM5301x enabled mac_check mac_hash
1: vlan1: 0 1 2 3 4t 8t
2: vlan2: 4t 8t
56: vlan56: 8u
57: vlan57: 2t 4t 5t 8t
58: vlan58: 4 7 8t
59: vlan59: 0 1t 2 3t 5
60: vlan60: 0 4t
61: vlan61: 0 2t 3 5 8u
62: vlan62: 0t 1t 2t 3t
上面是我现在的配置, 很多是路由器默认的, 和我们这次相关的就是 vlan1 与 vlan2 的配置. (这里带 t 的表示 Tagged, 没有 t 的表示 Untagged, 与交换机有区别). 对于我的路由器来说, 4 口是 WAN 口, 8 口是什么呢? 8 口代表的是 CPU, 路由器把 CPU 也抽象成了一个端口.
Port 4: 1000FD enabled stp: none vlan: 2 jumbo: off mac: 38:f9:d3:19:0a:6b
中的 vlan: 2
代表的是端口的 PVID, 我们可以用 robocfg port 4 tag 2
来配置 [5].
为了路由器每次重启都能自动配置 VLAN, 我们需要在 /jffs/scripts/services-start
中配置启动命令 [6]:
#!/bin/sh
robocfg vlan 2 ports "4t 8t"
robocfg vlan 1 ports "0 1 2 3 4t 8t"
这样, 单线复用就配置好了.
备注
家里各设备的网管地址 (年纪大了, 记性不好):
- 路由器: 192.168.50.1
- 交换机: 192.168.2.4 (密码要求有大小写与数字)
- AP: 到路由器里找 AP-105 的 IP
- 电信猫: 192.168.2.1, hcsk5
[1] GS108PEv3 — 8 Port Gigabit Ethernet PoE Smart Managed Plus Switch with 4-Ports PoE
[2] What is the default IP address of my NETGEAR Smart Managed Plus, Smart Managed Pro, or Insight Managed Smart Cloud switch?
[3] How do I access the admin page of my ProSAFE Web Managed Plus or Click Switch?
[4] 简单网管交换机的 VLAN 功能设置及应用
[5] 单线复用实践篇 #74
[6] 网件GS108PE交换机,搭配R8500梅林固件,实现单线复用
21 Oct 2020
几年前花费了很大精力在 Mac 下把 JDK7 编译了出来, 中间踩了无数的坑, 前几天想编译调试 JDK9 看看各种锁优化的实现. 本以为参考下之前的文章很快就能编译出来, 没想到很多工具都已经失效, Mac 系统的更新更是让编译雪上加霜.
放心, 这篇文章不会又是一篇填坑指南. 我自己填坑的时候忽然想到, 为什么不在 Docker 里编译呢?
使用 Docker 的好处就是环境统一, 另外采用 Linux 系统, 构建起来也更方便, 缺什么就安装什么. 下面就是我用的可以成功编译 OpenJDK9 的 Dockerfile:
FROM ubuntu:18.04
RUN apt update && \
apt install -y vim gdb file build-essential unzip zip \
openjdk-8-jdk libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev \
libcups2-dev libfreetype6-dev libasound2-dev libelf-dev
CMD bash
编译环境有了, 还缺的就是源码了, 有很多下载方式, 我是从这里下载的 http://jdk.java.net/java-se-ri/9.
假设你下载解压后的代码放在 ~/Downloads/openjdk
. 现在我们启动 Docker:
docker run -v ~/Downloads/openjdk:/openjdk -it -p 1234:1234 --rm --security-opt seccomp=unconfined jdkbuilder
其中 --security-opt seccomp=unconfined
是为了让后面用 gdb
顺利调试用的. 映射 1234
端口是为了后面远程 Debug.
启动镜像后, 切换到 /openjdk
目录, 执行下面命令:
bash configure --with-jvm-variants=server \
--disable-warnings-as-errors \
--with-debug-level=slowdebug \
--with-native-debug-symbols=internal
--with-debug-level=slowdebug
和 --with-native-debug-symbols=internal
是为了之后调试用的, --disable-warnings-as-errors
是让编译器忽略 warning, --with-jvm-variants=server
是编译服务器版本的 jvm.
configure
如果顺利执行, 就可以用 make images
来开始编译了, 我 docker 用 6 核 8 G 内存大概要 1 个多小时.
如果一切顺利, 编译好的 jdk 就在 /openjdk/build/.../jdk
下面, 可以用 gdb --args ./java -version
来试着调试. 调试的时候会提示有 received signal SIGSEGV, Segmentation fault.
的错误, 不用管, 直接 continue
就行.
如果觉得用 gdb
不方便, 也可以在 Docker 里启动 gdbserver
后用 CLion 远程 Debug:
# 容器里启动 gdbserver
gdbserver :1234 ./java -version
CLion 里创建一个 GDB Remote Debug 的配置, target 里填入: 127.0.0.1:1234
. Path mappings 里, Remote 填入 /openjdk
, Local 里填入你对应的本地地址 /Users/.../Downloads/openjdk
.
CLion 支持一整套的远程开发流程了, 包括代码同步, 编译, 部署, 调试. 不过只支持 CMake 的项目, 我就懒得弄了, 反正就是准备简单看看代码调试的, 而且 CLion 我也只是试用版, 就用 GDB 从命令行调试得了.
20 Oct 2020
JDK6 之前大家对 synchronized 的印象就是慢,所以 JDK6 对 synchronized 做了大量的优化。主要有:
- 自旋锁 Spinlock
- 适应性自旋 Adaptive Spinning
- 锁消除 Lock Elimination
- 锁粗化 Lock Coarsening
- 偏向锁 Biased Locking
- 轻量级锁 Lightweight Locking
- …
自旋锁以及它的进阶版适应性自旋优化针对的场景是,大部分时候线程持锁的时间是比较短暂的,我们没必要让线程使用重量级的系统调用来等待唤醒,可以增加一些空循环,说不定就能获取到锁了。普通自旋锁这个空循环的次数是固定的,适应性自旋会根据运行时的信息,来动态调整空循环的次数。
锁消除与锁粗化比较简单,就是有时候一个对象根本不会被并发使用,所以完全没必要去做加锁的操作,或者对于一个锁连续的加锁解锁可以合并成一次加锁解锁。
偏向锁针对的场景是,大部分第三方库是针对并发场景开发的,但是很可能在实际使用的时候并不会有并发发生,所以对于这种场景,一旦一个线程获取了这个锁,下次使用的时候就不用去执行昂贵的加锁操作了。
轻量级锁与偏向锁类似,它针对的场景是,虽然程序会有并发,但是大部分时候并不会触发竞争,所以可以用一些轻量级的方式来代替重量级的系统实现。
对于偏向锁与轻量级锁他们只是假设了某种场景,但是实际中可能这个假设是错误的,也就是说发生了竞争,那么这些锁就需要升级,偏向锁升级为轻量级锁,轻量级锁升级为重量级锁,升级的具体过程就要去看源码了,就不展开了。
锁这个词即是名词又是动词,我的理解是当强调锁的作用时应该用名词的锁 (Lock),强调锁的方式时用动词的锁 (Locking)。不过很多时候在英文里,Lock 与 Locking 各种混用。
https://github.com/farmerjohngit/myblog/issues/12
https://m.douban.com/book/subject/34907497/