HashMap的使用

Node和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//hashCode通过key于value计算得到
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
}
  • 哈希桶的大小,默认为16,如果自定义,需要为2的n次幂,这样可以使用与运算高效的代替模运算。
  • 负载因子,负载因子*数组长度等于容量阈值。
  • 阈值,当前HashMap中元素元素超过阈值,需要扩容,防止各种操作hash冲突增多,效率变低。

    hash和寻址

    1
    2
    3
    4
    5
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    index = hash&(tab.length-1);
    hash值通过key的hashCode异或本身高位得到的,这样可以让高位参与运算,否则hash值异或(tab.length-1)将不能利用hash值的高位,导致hash冲突变多。

put过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //判断数组位置是否已经插入元素,如果没有就插入数组中
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //通过hash和key确定,是否key相同,是的话覆盖value即可
e = p;
else if (p instanceof TreeNode) //判断是不是转为了红黑树,是的话将此节点插入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //循环遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //到达尾部,没有找到key相同的节点,插入尾部
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//找到key相同的节点,直接返回,执行1

if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//1,在key相同的节点,修改value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//size大于阈值,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

get过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k)))) //首先通过hash值在哈希表数组中寻找,如果hash和key都符合,则表示找到,可以返回
return first;
if ((e = first.next) != null) { //出现hash冲突,
if (first instanceof TreeNode)//如果子节点是树节点,在红黑树中寻找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do { //循环遍历链表,在链表中查找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

扩容

在容量超过阈值以后,会进行扩容,数组容量是当前数组的两倍。扩容后会为当前HashMap中每一个桶中的元素重新寻址,新的下标是e.hash&(newCapcity-1)。如果新的hash桶中已有元素,则类似插入元素的方式将当前元素插入hash表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { //决定存在数组高位还是低位
if (loTail == null)
loHead = e;
else
loTail.next = e; //会形成一个链表,link_1;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e; //也会形成一个链表,link_2;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; //存link_1在新数组低位
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; //存link_2在数组高位
}
}

Executor框架的使用

在Executor框架中,工作单元包括Runnable和Callable,执行机制由Executor框架提供。

两层调度模型

executor.png

多线程程序将任务分解为多个任务,然后由用户级调度器Executor将这些任务交由Java线程执行。Java线程并不是直接被CPU调度执行,还会映射为操作系统内核线程,由内核调度器将内核线程调度到CPU执行。祥见Java线程和os线程

Exectuor框架的结构

worker.png

manager.png
Executor框架由三大部分组成:

  • 任务,包括被执行任务需要实现的接口,Runnable接口和Callable接口;
  • 任务的执行,任务机制的和讯即接口及其实现类,ThreadPoolExecutor和ScheduledThreadPool;
  • 异步计算的结果,Future及其实现类FutureTask;

Executor框架的使用

use.png
主线程创建一个Runnable或者Callable任务(Executor可以将Runnable类型转换为Callable类型),然后交给Executor执行,主线程通过返回的Future接口,阻塞等待任务执行以后返回结果,也可以在等待过程中取消任务执行。

Executor框架核心类

  • ThreadPoolExecutor,线程池的核心实现类,用来执行被提交的任务。通过工厂类Executors实现三种类型线程池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//核心线程数和最大工作线程数量相等,避免创建大量线程,适用于服务器负载较重的情况。
//使用无界队列LinkedBlockingQueue作为任务管理队列,意味着线程池中工作线程不会超过核心线程。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

//没有核心线程,最大工作线程取整数的最大值,适用于有比较多短期的小任务场景;
//使用SynchronousQueue作为工作队列,当主线程提交任务速度大于线程处理速度时,会不断创建线程,有可能会因为创建过多线程导致CPU和内存耗尽
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

//只会创建一个工作线程,适用于需要保证顺序的执行各个任务的场景。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

ScheduledThreadPoolExecutor用于在固定延迟后执行任务,通过Executors创建,包括两种类型:

1
2
3
4
5
6
7
8
9
10
//只会创建单个核心线程,工作线程为Integer.MAX_VALUE
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}

//可以设置多个核心线程,工作线程为Integer.MAX_VALUE
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

Runnable接口和Callable接口的实现类,都可以交个线程池执行,不同的是Callable接口可以返回结果.也可以将一个Runnable对象封装为Callable对象:

1
2
3
4
5
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}

ScheduledThreadPoolExecutor

1
2
3
4
5
6
7
//使用DelayWorkerQueue作为工作队列,这是一个无界阻塞队列,使用PriorityQueue实现。
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}

schedule.png
主线程向DelayWorkerQueue中添加任务时,任务会被包装为ScheduledFutureTask,线程池中的线程会从队列中取出任务执行。

ScheduledThreadPoolExecutor的实现

1
2
3
4
5
6
ScheduledFutureTask(Callable<V> callable, long ns) {
super(callable);
this.time = ns; //表示这个任务将要被执行的具体时间
this.period = 0; //表示任务执行的间隔周期,
this.sequenceNumber = sequencer.getAndIncrement(); //被添加入任务队列的顺序
}

DelayQueue中的任务是ScheduledFutureTask类型,包括三个成员变量。DelayQueue封装了一个PriorityQueue,会根据ScheduledFutureTask的time和sqquenceNumber进行排序。线程池中的线程会从任务队列中取出time大于当前时间的任务进行执行。在执行结束以后,会更新time,重新将任务放回队列之中。

1
2
3
4
5
6
7
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}

FutureTask

由于FutureTask继承了Future接口和Runnable接口,所以可以把一个FutureTask接口交由实现了Executor的线程池执行,也可以作为计算结果返回,然后执行FutureTask.get()阻塞当前线程等待返回计算结果。

1
<T> Future<T> submit(Runnable task, T result);

FutureTask是基于AbstractQueuedSynchronizer(AQS)实现的,很多可阻塞类都是基于AQS实现的,AQS是一个原子框架,提供了通用机制来原子性的管理状态,阻塞和唤醒线程,以及维护被阻塞线程的队列。对于很多阻塞类,其具体操作都会委托给实现了AQS的内部类Sync,由Sync进行具体的操作。

aqs.png


线程池的使用

使用线程池的益处

  • 避免重复创建线程执行任务,减少了创建线程和销毁线程需要的时间开销和性能开销;
  • 提高任务响应速度,线程池中通常缓存有线程,当提交任务以后,可迅速执行;
  • 避免了无规则的创建大量线程,导致大量线程排队等待CPU,响应速度变慢;

线程池处理流程

stage.png

我们可以调用线程池的execute和submit方法来提交任务,提交参数都是一个Runnable实例,不同的是,submit会返回一个Future类型的对象,可以通过future对象的get方法获得返回值,注意这个方法会阻塞当前线程。

  1. 当一个任务提交给线程池时,如果当前线程池中线程数量少于核心线程数,会重新创建新的线程执行这个任务,然后通过线程安全的方法更新当前线程数。注意,当提交一个任务给线程池时,线程池会创建一个核心线程来执行,即使其他核心线程空闲,直到核心线程达到预设值。
  2. 如果当前线程数已经大于或者等于核心线程数,那会尝试判断阻塞队列是否已满,未满的话将任务加入到阻塞队列中。
  3. 已满则判断当前线程池数量是否小于最大线程数量,如果是的话,创建工作线程执行,否则的话执行拒绝策略。

线程池参数说明

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize, //最大工作线程数,不能小于corePoolSize
long keepAliveTime, //非核心线程超时时间,闲置时间超过会被销毁
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //用于保存等待执行的任务的阻塞队列
ThreadFactory threadFactory,
RejectedExecutionHandler handler //拒绝策略)

阻塞队列有很多种,newCachedThreadPool使用SynchronousQueue,这种阻塞队列不存储任务,每个插入操作必须等待一个线程执行取出操作,否则插入线程阻塞。newFixedThreadPool则使用LinkedBlockingQueue。

handle.png

拒接策略分为四种,分别是直接只用调用者所在线程执行任务、直接抛出异常、丢弃队列里等待最长时间的任务,执行当前任务、或者直接丢弃掉当前任务。

配置线程池

根据不同的情况配置线程池:

  • 根据任务性质,比如是CPU密集型还是IO密集型,CPU密集型应该配置尽可能少的线程,防止线程持续等待CPU分配时间,IO密集型就可以多一些线程,因为大部分线程可能在等待IO;
  • 任务优先级,是否有些任务是高优执行的,可以使用ProrityQueue作为阻塞队列。

Java中的并发工具类

Java中有许多工具类可以在并发场景中使用,简化并发编程难度,提高程序准确性。

CountDaowLatch

可以实现类似于fork-join模型提供的功能,在多线程场景中,用于等待其他线程完成的线程可以调用countDownLatch的await方法进入等待状态,只有当其他线程将CountDownLatch中保存的值递减到0时,等待线程才会继续运行。

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CountDownLatchTest {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[]args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("continue");
countDownLatch.countDown();
System.out.println("continue");
countDownLatch.countDown();
}
});
threadOne.start();
countDownLatch.await();
System.out.println("over");
}
}

answer.png

运行结果总是相同,主线程在运行到await时,会进入等待状态,只有当子线程两次执行完countDown之后,主线程才会继续执行。

实现原理

查看CountDownLatch的实现,可以看到在new一个CountDownLatch时,需要一个int类型的参数:

1
2
3
4
5
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}

Sync是CountDownLatch的一个继承了AbstractQueuedSynchronizer的内部类,用于实现同步控制,具有一些列加锁和解锁的方法。构造CountDownLatch时传入的参数最终用来设置一个被volatile修饰的属性state。这个state值可以理解了当前所有线程可重入的获得了多少锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void countDown() {
sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}


protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

countDown会将当前的state值减1,这可以理解为释放一把锁的过程。

当主线程调用await方法之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

如果当前的state没有减到0,主线程就会去执行doAcquireSharedInterruptibly,这个方法会使得主线程不断死循环的去获取“锁”,或者直到中断,直到state减少到0,主线程才能得到“锁”,解除循环,继续执行。

Semaphore

使用方式

可以用于做流量控制,限制多线程对有限资源的访问,在多并发场景下,如果资源数量有限,只能够支持有限的线程的使用,可以使用信号量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SemaphoreTest {
private static final int THREAD_COUNT = 30; //线程规模
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);

public static void main(String[] args) {
for (int i = 0; i < 100 * THREAD_COUNT; i++) { //任务数量远大于线程数
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire(); //获取信号量
printThreadCount();
s.release(); //释放信号量
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
threadPool.shutdown();
}

private static synchronized void printThreadCount() {
System.out.println("当前可用许可证数:"+s.availablePermits() + " 等待线程数" + s.getQueueLength());
}
}

ww.png

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

private final Sync sync;

public Semaphore(int permits) {
sync = new NonfairSync(permits);
}

Sync(int permits) {
setState(permits);
}

protected final void setState(int newState) {
state = newState;
}

在初始化Semaphore时,默认构造非公平的同步器,传入参数为信号量的值。
在线程执行到acquire时,会尝试可中断的去获取信号量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg); //1
}

protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;//目前剩余信号量,小于0证明已经无可用
//尝试判断,小于0或者原子重置信号量值失败,都会返回负值,然后进入等待队列
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}

在释放信号量以后,会通过原子操作给state值加一,如果当前state的值大于0,会在等待队列中唤醒队列首部的线程去获得信号量。


Java中的原子类

在多线程读写共享变量的场景中,很容易出现数据竞争,导致数据不一致。Java提供了synchronized关键字和Lock接口来保证多线程对同步块的有序访问,但是这两种方式都需要隐式或者显式的获取锁,性能开销略大。Java的Atomic包提供了多个原子操作类,可以安全、高效、简单的实现在多线程场景下读写变量。Atomic包中的类基本上都是使用Unsafe实现的包装类。

AtomicInteger

包括诸多方法实现整型数据的原子操作。比如对于整数的递增操作i++,由于这一操作并不是原子的,所以即便使用volatile修饰也不能保证线程安全,这种场景就可以使用AtomicInteger的方法:

1
2
3
4
5
6
7
8
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下通过AtomitInteger的addAndGet方法来分析这一原子类是如何实现线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @param valueOffset the value memory address.
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

var5是通过native方法取得的当前变量的值,compareAndSwapInt通过原子操作,将预期的的结果var5+var4替换为当前变量的值var5,如果方法成功就,compareAndSwapInt返回ture,循环结束,如果返回false,说明有其他线程在这段时间修改了当前变量的值,会重新通过循环获取var5,继续重试。

AtomicBoolean

实现原理与AtomicInteger基本相同,核心思想是将true和false映射为1和0。int类型的value存储的就是当前AtomicBoolean的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private volatile int value;

/**
* Creates a new {@code AtomicBoolean} with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicBoolean(boolean initialValue) {
value = initialValue ? 1 : 0;
}

/**
* Atomically sets to the given value and returns the previous value.
*
* @param newValue the new value
* @return the previous value
*/
public final boolean getAndSet(boolean newValue) {
boolean prev;
do {
prev = get();
} while (!compareAndSet(prev, newValue));
return prev;
}

public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0; //将boolean类型映射为int类型
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);//通过原子操作更新当前值。
}

原子更新数组

更新数组的值是通过调用其他原子更新基本类型或者引用类型来实现的,重点在于通过数组下标获得当前值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final int updateAndGet(int i, IntUnaryOperator updateFunction) {
long offset = checkedByteOffset(i);//得到下标在内存中的地址
int prev, next;
do {
prev = getRaw(offset);
next = updateFunction.applyAsInt(prev);//转为int
} while (!compareAndSetRaw(offset, prev, next));
return next;
}

//在内存中取得当前值
private int getRaw(long offset) {
return unsafe.getIntVolatile(array, offset);
}

//以原子方式修改数组偏移量的值
private boolean compareAndSetRaw(long offset, int expect, int update) {
return unsafe.compareAndSwapInt(array, offset, expect, update);
}

原子更新引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package longkai;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
public static void main(String[] args) {
AtomicReference<User> atomicReference = new AtomicReference<User>();
User firstUser = new User("long", 23);
atomicReference.set(firstUser);
User secondUser = new User("kai", 24);
atomicReference.compareAndSet(firstUser, secondUser);
System.out.println(atomicReference.get().getName());
System.out.println(atomicReference.get().getAge());

}
}

class User {
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public int getAge() {
return age;
}

public String getName() {
return name;
}
}

public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

实现方式

通过查看源码,最后发现Unfase类只提供了三种CAS方法:

1
2
3
4
5
6
7
8
9
/**
* 如果当前值为var4,则将值更新为var5
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var5);

与AtomicBoolean类似,原子更新char、short、float、double可以实现。CAS算法是一种乐观锁的实现思想,在更新变量之前不对更新操作进行加锁,而是在更新之后再去看当前变量内存地址的值有没有发生改变,如果没有,就将希望更新的值写入到内存,如果发生了更改,则继续重试。


在冬季和北京再次相遇

昨天晚上八点到的北京,从广州穿着短袖飞越两千多公里,下飞机以后穿着和路人显得格格不入,大家都是穿着厚厚的棉袄了。

走了二十多分钟到之前订的民宿里暂住一晚,一路上是那种好久没有感觉过的冷。房东是个大学老师,很客气。十一点吃完外卖直接睡下了,感觉还是没有住酒店轻松,民宿总感觉不方便,比如担心声音大了一点惹主人家不高兴。

早上起来赶到回龙观和房东谈房子,感觉北京的空气比前些年要好很多了,很明显有那种高纬度地区天空纯洁的感觉。前几天从广州寄的被子今天刚好送到,不早不晚,可以直接用上。

秋招的offer拿到的全是北京的公司。在面试时很多面试官都会问我为什么要来北京?和同学说我打算去北京,他们也会问为什么要去那么远呢?北京空气质量不好,气候干燥,交通还拥堵。这些都是我真真实实要面对的问题,

但是我还是没有打算去其他城市,我觉得北京对于我有一种莫名的吸引力,或许是他深厚的文化底蕴,或许是他领先的经济发展水平,或者是他各类的活跃人才,这些也许其他城市也有,但总不能和北京比肩。在北京工作的经历会是一段富有意义的经历,这样的经历,越早越好。

近来也会思考人生的意义,以前觉得人生应该最求快乐,怎样快乐就怎样去度过一生,但是现在不这么觉得了,人生应该是追求充实,同样要去接纳苦难和波澜不惊,应该去领略各式各样的人生,如果总是快乐的,不免显得太单调无味。

距离今年春节已经不足三个月了,发现手里的事情还蛮多的,而且都有难度,毕业设计已经提上日程,目前还没有方向,想要在春招转后端开发,但是要学的东西还有很多,在快手的实习工作量应该会很足,也担心一些工作可能做不好,嗯,是的,不会说做完什么事就没事了,事情永远一件接一件,那,干就完了。


上海外滩之夜

来上海一周了,一切都好,因为自己前端基础比较薄弱,每天在公司会学很多东西,感觉很充实,很有成就感,但是每当晚上八九点独自一人回到住处,强烈的孤独感就会袭来,是一种让人心慌,不知所措的感觉。

昨天是星期六,来这里以后的第一个休息日,但是对这里都不熟,也不知道去哪里,只好一觉睡到十点。下午复习了一下课程,决定晚上出去走走,否则漫漫长夜实在难挨。东方明珠是小学课本里出现的,也一直作为上海的标志和上海联系在一起,出了地铁就看到了,但真的略感失落,因为她好像不是想象中的那么高大,尤其是在和周围那么多金融大厦相比。

走在黄埔江边,和王金梅聊了一路,感觉就没那么孤独了,孤独应该是要倾诉的吧。现在可以好好聊聊真心话的朋友真的越来越少了,不知为何,只能接受。天南地北聊了很久,我都忘记自己已经走了多远了。在意识到应该回去了才发现距离地铁站已经很远了,狠心想打车回去,奈何手机还没等到车来就没电关机了。立刻意识到情况不妙,地铁停运,手机没电,可能回不去了,只能趁现在江边还有人赶紧返回东方明珠附近,那里应该相对安全,一路快走,脚掌也不疼了,本来刚才散步就有点疼了。

已经十一点半了才走回到东方明珠塔下,幸好有一个保安亭可以充电,本来想央求他收容一晚上的,无奈他太不近人情了。一点钟充电差不多了,也不打算再去找住处了,现在到处跑也不安全,就在保安亭旁边角落做一晚上吧,和保安在一起应该更安全一点。从五个小时,一万八千秒倒数,晚上是真的有点冷,最冷的时候应该是凌晨四点钟的时候。先是坐地板上,发现地板太僵了,然后站着,站不动就蹲着,期待着黎明的到来。凌晨三点钟看到一个中年男人来翻垃圾桶,生活是有多难呢?凌晨四点钟就有人起床锻炼了,真的佩服;偶然听到那个保安和朋友聊天,他拒绝去工厂,太累了,还是当保安轻松,我就在想难道你可以做一辈子保安吗?凌晨四点钟就鸟儿就开始出来觅食了,叽叽喳喳的好多,我还以为城市没有鸟呢。

熬到五点,已经是黎明了,还好回去睡两个小时还赶得上上班。


在美团点评的第一天

今天是入职美团点评开始实习的第一天,是来到上海的第三天。在第一天晚上住的那个酒店里放了一个体重称,称了一下体重,133斤,果然,这三个月假期肥了7斤,比大学两年半肥的还要多。对于我来说,没有学习和工作的氛围,自律的学习和生活就不可能。

赶紧滚出舒适圈,今年行情不好,还是早些谋划出路。当混子的时间长了,连方向都分不清,先把三明北的高铁票买成了三明站,幸好没耽误下一程,然后把自己工作的地点弄错,之前联系要租的房子前功尽弃,在找房子的前一天晚上十二点还临时上网找房子,又把酒店定在了和公司截然相反的方向,行李箱都在漫长的换乘地铁的过程中拉坏掉,真的是路遥马亡。

今天入职,真正的开始把知识运用到工程。在办理入职手续的时候见到了很多也是今天入职的同龄人,感觉挺好的,年轻人比较多。HR联系徐斌学长接到了我,然后见到了部门leader,就是二轮面试的面试官,还记得当时聊了一个多小时,虽然很多问题我都答不出来,他也会给我解答一下,真人比视频上年轻是真的。部门工位满了,只能到隔壁部门借坐两天,这也挺好,和他们在一起自己那些low比操作怕是要被笑死。mentor是个应该毕业一两年的女生,虽然年纪轻轻,但是项目构思、技术表达都非常严谨和清晰,在今天晚上的小组周会上可见一斑。

八点下班走人,住得近真的好,可以步行上下班,早晚高峰挤地铁的事情我是不用体验了。回来接到了爸爸的电话,多是些叮嘱我这个涉世未深的小孩应该如何处理职场的人情世故。还记得在家面试的时候,每次面试妈妈都会问这家公司是哪里的,其实说了在哪个城市她也不知道,也还是要问问,应该是希望工作得城市不要太远吧。

要开始接收新任务了,还是赶紧学习,在被认出是菜鸡之前变得稍微强一点。


对每一个不曾到达的地方,都要心怀向往

放假已经快三个月了,因为疫情,才可以和爸妈在一起那么久,过完年就开始复习找实习,也准备了快三个月了,真的非常幸运,拿到了美团点评的Android工程师实习offer,否则我现在真的会焦虑死吧。

年后就在想,实习应该找什么岗位呢,自己大学学习的那些课程好像不足以应对工作,好像做什么都要从零开始,最终决定选择Android开发,是因为算法岗自己肯定不行,后台开发竞争太大了,前端庞杂难以短时间掌握,iOS开发目前没有硬件练习,测试门槛太低了,就只有Android是最优解了。整个准备的过程需要复习和学习的东西还是蛮多的,计算机网络、操作系统、数据库、Java、Android、JVM、设计模式等都要去学习,还要时常刷一点算法题。整个过程有一种山重水复疑无路,柳暗花明又一村的感觉。

3月10左右开始投简历,都忘记投了多少家公司了,每天都会去看一下简历状态,等待面试真的是很难受的一个过程。一开始面试的是蘑菇街,第一轮面试就挂掉了,然后小米、盛趣游戏、字节跳动、腾讯、CVTE等相继挂掉,虽然每一轮面试都入真准备,但是小厂需要可以直接干活的,大厂需要学习能力各方面都很优秀的,那我真的就没有去处了。

我一直感觉我是一个很幸运的人,特别是在一些人生的关键节点,都会遇到贵人相助,这次也不例外。美团点评也算大厂了,在美团点评面试之前就有一位美团点评iOS工程师打电话给我,希望我转iOS,让我好好准备笔试,这给我很好的心理暗示,然后面试的时候是一位女生面试官,人很友好,问的问题很简单,问题答不对也会引导,40分钟就结束面试了,当天就收到了第二次面试和第三次面试的邀请,这又给我了很好的心理暗示,难道默认第二轮面试通过吗?我想得太美了,第二轮是一个leader,一个多小时的面试都是基础知识轰炸,虽然很大都没有答出来,但是他还是给我过了。第三轮面试相对就比较轻松了,面试官人很好,天南地北的都聊。第二天就收到了offer call,下周一就收到了offer通知,曾为此不遗余力的复习,也曾辗转反侧的煎熬等待,终于所得届时所想,心中喜悦之情,难以言表。

在最近几次面试中,反复被问到了了对职业生涯有什么规划,没有想过的问题,自然回答得一团糟,但这个问题不可以回避,也无法回避,真的需要规划自己得职业生涯了,那种庸庸碌碌的校园生活所剩无几了。


想要问问你,生活可如意

在过3天就是2020年了,又一个十年的伊始。

回想过去十年应该是很惬意的十年,小学到初中在到高中,还有不紧不慢的大一大二两年,都过得潇洒恣意,也取得些许成就,至少无愧于自己也无愧于亲友。

现在的我说话已经时常用成年人打头了,成年人不能畏畏缩缩没有担当、成年人不能理所当然的继续成为家里的负担、成年人不能一事无成。

越是经常这样想就越是觉得失落。今年一心想着修学分,选课选得多了一些,再加上自己的资质是在过于平庸,一个学期下来整天围着作业打转,上半学期还好,还能勉强跟上,下半学期尤其是接近期末这段时间,只能是糊弄作业了,为此时常觉得难受。这种感觉在和同学们的对比总尤为强烈,自己进步得实在是太慢了,再仔细想想,自己有进步吗?

暂时先麻木吧,心为形役,挨过这个学期再说。

今年的下半年开始,考研和实习应该是思考得最为频繁得两件事。考研有风险,而且对经济独立有一种向往,所以时常在想着怎么开始第一份实习;感觉自己基础还不够扎实,尚不能胜任技术含量高得工作,而且担心本科学历会成为职业生涯的瓶颈。就是一个选择题,想了一个学期也没想明白,后来就决定先实习吧,一些主意光想就如同空中楼阁,不切实际,不如职场走一圈,去体会一下本科就业是否适合自己,就这样,路是走出来的,不是想出来的,适合什么亲自体验一番就知道了。

苒苒时光,当然不乏感动与快乐。

前两天哥哥在家人群里告知今年可能赶不上年夜饭了,让我们不要等他吃饭了,我先是厌恶它们公司放假晚而后只是无可奈何,爸爸看到消息以后说了一句“不怕伐个车去高铁站接你”,而后又说“我去高铁站联系车等你”,心里是感动的感觉,父母在的地方都是家,什么都不用怕。

每个人都开始忙碌各自的事,疏远的人越来越多,能够时常问候的人越来越少。和王金梅相识快十年了,感情依旧,幸甚。

今年课外文学阅读21本,还好阅读这个习惯没有被荒废掉。《霍乱时期的爱情》这本书使我最为震撼,贯穿一生的爱情,读来让人唏嘘。

一直想学口琴的,买了一把很贵的口琴却束之高阁,三分钟热度要不得。

今年有幸出境德国游学一次,有什么收获也说不上来,对自己的影响却是有的,时常想起自己说过好好学习,积极生活,山河俊秀,不可辜负。这个世界那么美,那么多原子聚合成一个现在的我在这宇宙千万年里就那么一次,真应该珍惜短短剑,去走遍山川湖海。

对下一个十年没什么规划,继续努力吧。

2020,对我好一点。