addr2line
1 | addr2line -f -e path/to/binary <address> |
nm
列出对象文件中的符号
1 | nm libssl.a |
1 | 安装 |
ldd
1 | ldd main |
strings
strings 程序的主要功能是找出文件(包括文本文件、二进制文件等)内容中的可打印字符串。
1 | strings /usr/lib/libstdc++.so.6 | grep GLIBCXX |
1 | addr2line -f -e path/to/binary <address> |
列出对象文件中的符号
1 | nm libssl.a |
1 | 安装 |
1 | ldd main |
strings 程序的主要功能是找出文件(包括文本文件、二进制文件等)内容中的可打印字符串。
1 | strings /usr/lib/libstdc++.so.6 | grep GLIBCXX |
对比
Function<A, B>
: 一个参数 + 返回值Supplier<A>
: 无参数 + 返回值1 | public class FunctionTest { |
以多线程申请及获取锁的顺序区分,先申请先得则为公平锁。非公平锁容易造成饥饿现象,但吞吐量优于公平锁(公平锁实现会先判断是否有前驱线程在等待)。
ReentrantLock
可通过构造参数指定为公平锁,默认非公平锁,基于AQS实现线程调度。
synchronized
没有线程调度机制,所以为非公平锁。
可重入锁可重复可递归调用的锁,行为为一个线程可以对同一对象多次加锁。
独享锁一次只能被一个线程加锁,共享锁可被多个线程加锁。
互斥锁
访问临界资源前加锁,访问后解锁。加锁后,再次加锁会被阻塞,直到当前进程解锁。同一时间,只有一个线程,能够访问被互斥锁保护的资源。
读写锁
读写锁由read模式的共享锁和write模式的互斥锁组成,读写锁有三种状态,读加锁状态、写加锁状态、不加锁状态。
悲观锁
总是假设最坏情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java
中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition
机制,其实都是提供的乐观锁。在Java
中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
分段锁是一种锁的设计,通过减小锁的粒度,提升多并发程序性能。
JVM为了提高锁的获取与释放效率,在对象头中添加字段,使得对象监视器可获取其锁的状态,锁的状态如下。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
实现示例
1 | public class SpinLock { |
lock() 方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
问题
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
优点
总结
AbstractQueuedSynchronizer
的简称jdk 1.5 之前,synchronized
是一种独占式的重量级锁,底层使用系统的 mutex lock 实现。jdk 1.6 之后,做了大量优化,加入了CAS,轻量级锁和偏向锁的功能,性能上已经跟ReentrantLock相差无几。
同步代码块
monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。
同步方法
synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,有一个ACC_SYNCHRONIZED标志,JVM就是通过该标志来判断是否需要实现同步的,具体过程为:当线程执行该方法时,会先检查该方法是否标志了ACC_SYNCHRONIZED,如果标志了,线程需要先获取monitor,获取成功后才能调用方法,方法执行完后再释放monitor,在该线程调用方法期间,其他线程无法获取同一个monitor对象。其实本质上和synchronized块相同,只是同步方法是用一种隐式的方式来实现,而不是显式地通过字节码指令。
锁对象 | 粒度 |
---|---|
普通同步方法 | 当前实例对象 |
静态同步方法 | 当前类的class对象 |
同步方法块 | 关键词括号内的对象 |
1 | public class TestLock { |
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
核心思想,如果被请求的共享资源空闲,那么将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。
CAS
是英文单词Compare and Swap
(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization
)。CAS算法涉及到三个操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。
为了提高程序执行的性能,编译器和执行器(处理器)通常会对指令做一些优化(重排序)。
并发编程的三大特性,原子性、可见性、有序性。synchronized可保证三大特性(保护的代码块通知只能有一个线程执行,单线程没有指令重排的问题),volatile 可以保证可见性(把工作内存中的最新变量强制刷新至主存)和有序性(编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序)。
- | ReentrantLock | Synchronized |
---|---|---|
锁实现机制 | AQS | 监视器模式 |
灵活性 | 支持响应中断、超时、尝试获取锁 | 不灵活 |
释放形式 | 必须显式调用 unlock 释放锁 | 自动释放监视器 |
锁类型 | 公平锁 & 非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
可重入性 | 可重入 | 可重入 |
JDK 1.7
Jdk 1.7 中,HashMap 底层为数组+链表实现,ConcurrentHashMap与其不同的是,添加了 Segment 作为数组+链表的上层结构,Concurrent 继承 ReentrantLock,对 Segment 加锁,实现不同 Segment 可以同时读写。在写入获取key所在Segment是需要保证可见性,ConcurrentHashMap使用如下方法保证可见性,取得最新的Segment。
1 | Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u) |
写入 Segment 时,需要获取锁,先使用自旋锁,超过重试次数后,通过 lock 获取锁(会被阻塞,切换至内核态等待)。
JDK 1.8
Jdk 1.8 中的 ConcurrentHashMap 刨除了 Segment 的设计,直接 <大数组+[链表|红黑树]> 的方式,如果数组对应的链表超过一定阈值后,转换为红黑树。大数组使用 volatile 关键字修饰。
对于写操作,key对应的数组元素,使用 synchronized 关键字申请锁(当然,如果为null,则不需要加锁),然后进行操作。
对于读操作,数组使用 volatile 保证可见性,数组的每个元素为Node实例(jdk 1.7 为 HashEntry),他的 key 和 hash 值都使用 final 修饰,无需担心可见性。value 和下一个元素的引用由 volatile 修饰,保证可见性。
1 | static class Node<K,V> implements Map.Entry<K,V> { |
对于 key 对应的数组元素的可见性,使用 Unsafe 的 getObjectVolatile 方法保证。
1 | static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { |
所谓锁,在计算机中,本质是内存中的一块空间,其值被赋予了加锁/未加锁的含义,其底层实现,是基于计算机系统中的原子操作。
mutex lock 核心包含两点。
1 | // 创建 executor |
Tips
Future.get()
在主线程捕获异常,execute 则不行1 | TSerializer serializer = new TSerializer(new TBinaryProtocol.Factory()); |
1 | TDeserializer deserializer = new TDeserializer(new TBinaryProtocol.Factory()); |
正则
1 | // 将 :***.*** 替换为 ${***.***} |
[toc]
1 | package main |
1 | 格式 |
类型 | 说明 |
---|---|
uint8 | - |
uint16 | - |
uint32 | - |
uint64 | - |
int8 | - |
int16 | - |
int32 | - |
int64 | - |
float32 | - |
float64 | - |
complex64 | 复数 |
complex128 | 复数 |
byte | 类似 uint8 |
rune | 类似 int32 |
uint | 硬件架构相关,32位或64位无符号整型 |
int | 硬件架构相关,32位或64位有符号整型 |
uintptr | 无符号整型,存放指针 |
示例
1 | var i int = 1 |
定义
1 | var name string = "wii" |
操作
1 | // 拼接 |
1 | val b bool = true |
字面常量
1 | var i, s, b = 1, "wii", false |
itoa
特殊常量,可以被编译器修改的常量。
1 | const ( |
声明
1 | var identifier [type] = v |
示例
1 | var a string = "wii" |
声明
1 | var variable_name [SIZE] variable_type |
初始化
1 | var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} |
多维数组示例
1 | // 先声明后赋值 |
底层实现是双向链表。
1 | 创建 |
1 | var m map[string]string = map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo"} |
1 | // 单行注释 |
1 | // 1. 类似于 for(...; ...; ...) |
range
1 | // map |
格式
1 | func function_name( [parameter list] ) [return_types] { |
示例
1 | func swap(x, y string) (string, string) { |
参数
1 | // 值传递 |
函数参数
1 | package main |
匿名函数
1 | func getSequence() func() int { |
方法
Go 同时有函数和方法,一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集,格式如下。
1 | func (variable_name variable_data_type) function_name() [return_type]{ |
下面定义一个结构体,及该类型的一个方法。
1 | package main |
1 | // 定义 |
示例
1 | type Data struct { |
1 | fmt.Printf 占位符 |
1 | // 定义接口 |
注意
NextLine 方法定义由于需要修改 impl 对象内容,必须使用指针。如果使用指针,创建接口时,必须加 & ,比如
sr := &FileReaderImpl{dataPath: dp, file: file, scanner: scanner, done: false}
。
1 | func PointArg(l interface{}) { |
1 | func PointArg(l interface{}) { |
使用 interface{}
作为参数类型,来匹配任意类型,注意,不可用 *interface{}
1 | // i 为 interface{} 变量, 实际类型为 map[string]interface{} |
1 | // 保存函数 |
1 | t := map[string]interface{}{} |
创建名为 {code}_test.go
的文件,即为 {code}.go
的单测文件。
echo.go
1 | package tm |
echo_tests.go
1 | package tm |
命名中的首字母大小写,大写指明可被外部访问,小写指明私有。对于 bool 类型,首字母应该为 Has
、Is
、Can
、Allow
。
c
好于 lineCount
,i
好于 slliceIndex
chubby.File
代替 chubby.ChubbyFile
_
分隔1 | // Package math provides basic constants and mathematical functions. |