Gallon's Note


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

ThreadLocal

发表于 2019-10-31 | 分类于 ThreadLocal

ThreadLocal是一种空间换时间的方法。

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

ThreadLocal中填充的变量属于这个线程,对于其他线程是不可见的。

ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。

阅读全文 »

常用数据结构

发表于 2019-10-30 | 分类于 数据结构

跳表

红黑树

B+树

Queue容器

发表于 2019-10-29 | 分类于 容器

Queue主要可以分为两类,一类是不阻塞的,一类是阻塞的。非阻塞队列主要有PriorityQueue 和 ConcurrentLinkedQueue。

实现一个线程安全的队列主要有两种方式:阻塞队列(加锁,线程会阻塞;CAS,Locksupport.park)以及非阻塞队列(CAS,线程不会阻塞)。

使用阻塞算法的队列可以用一个锁 (入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。

非阻塞队列:PriorityQueue(线程不安全) ConcurrentLinkedQueue(线程安全)

双端队列

比如LinkedList以及ArrayDeque就是双端队列。其中,ArrayQueue是一个用数组实现的双端队列,可以在数组两端进行元素的插入以及删除,所以,这个数组必须是循环数组。LinkedList是基于双向链表的,容量没有限制,可在链表两端进行插入以及删除元素。

ArrayDeque底层是一个数组。

ArrayDeque是一个循环队列。它的实现比较高效,它的思路是这样:引入两个游标,head 和 tail,如果向队列里,插入一个元素,就把 tail 向后移动。如果从队列中删除一个元素,就把head向后移动。

非阻塞队列

非阻塞队列主要讲一下PriorityQueue以及ConcurrentLinkedQueue。

PriorityQueue

PriorityQueue又叫做优先级队列,保存队列元素的顺序不是按照及加入队列的顺序,而是按照队列元素的大小进行重新排序。

  • PriorityQueue内部实现是一个小顶堆,这样保证每次取出来的一定是最小值,他会要求你定义一个Comparable接口。PriorityQueue

  • PriorityQueue不是线程安全的,在多线程情况下最好使用PriorityBlockingQueue 。

  • 不允许插入 null 元素

ConcurrentLinkedQueue(ToDo)

无阻塞线程安全的队列,使用CAS+自旋的操作来执行,这样线程不会阻塞,所以叫做非阻塞队列。

如果我们要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环 CAS 的方式来实现。

这些方法实际上是通过调用UNSAFE实例的方法,通过CAS处理是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
//更改Node中的数据域item	
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
//更改Node中的指针域next
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
//更改Node中的指针域next
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

ConcurrentLinkedQueue

ConcurrentLinkedQueue

阻塞队列

阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。

生产者消费者问题。 put()以及take()方法

一般来说,使用锁和条件队列实现,线程会阻塞;CAS+LockSupport.park(),线程会阻塞。

ArrayBlockingQueue(一把锁)

ArrayBlockingQueue = ArrayQueue+ReentrantLock+Condition。

所以,一方面,ArrayBlockingQueue使用Array做一个循环队列,另一方面,通过ReentrantLock以及Condition来实现等待唤醒操作。

1
2
3
4
5
6
7
8
9
/** The queued items */
final Object[] items;
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是`ReentrantLock lock`,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。

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
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果当前队列已满,将线程移入到notFull等待队列中
while (count == items.length)
notFull.await();
//满足插入数据的要求,直接进行入队操作
enqueue(e);
} finally {
lock.unlock();
}
}

//添加数据,唤醒消费者
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//插入数据
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//通知消费者线程,当前队列中有数据可供消费
notEmpty.signal();
}

take方法

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
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列为空,没有数据,将消费者线程移入等待队列中
while (count == 0)
notEmpty.await();
//获取数据
return dequeue();
} finally {
lock.unlock();
}
}

//取出数据,唤醒生产者
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取数据
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//通知被阻塞的生产者线程
notFull.signal();
return x;
}

LinkedBlockingQueue(两把锁)

  • 底层采用链表来实现。

  • LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。

  • 通过takeLock和putLock两个锁来控制生产和消费,互不干扰,不会相互因为独占锁而阻塞。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    transient Node<E> head;

    /**
    * Tail of linked list.
    * Invariant: last.next == null
    */
    private transient Node<E> last;

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

PriorityBlockingQueue(无界队列)

PriorityBlockingQueue是一个底层由数组实现的无界队列,并带有排序功能,同样采用ReentrantLock来控制并发。由于是无界的,所以插入元素时不会阻塞,没有队列满的状态,只有队列为空的状态。通过这两点特征其实可以猜测它应该是有一个独占锁(底层数组)和一个Condition(只通知消费)来实现的。

DelayQueue

DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。

SynchronousQueue

CAS+park.

没有容量的队列。每进行以此put,必须要进行一次take。

LinkedTransferQueue

CAS+park

LinkedTransferQueue是一个无界的阻塞队列,底层由链表实现。

LinkedBlockingDeque

LinkedBlockingDeque是一个有界的双端队列,底层采用一个双向的链表来实现,在LinkedBlockingQeque的Node实现多了指向前一个节点的变量prev。并发控制上和ArrayBlockingQueue类似,采用单个ReentrantLock来控制并发,这里是因为双端队列头尾都可以消费和生产,所以使用了一个共享锁。

总结

无界队列:PriorityBlockingQueue(有序,数组)、DelayQueue(底层实现为PriorityBlockingQueue,适用于定时任务)和LinkedTransferQueue(链表)。

有界队列:ArrayBlockingQueue(数组)、LinkedBlockingQueue(链表)以及LinkedBlockingDeque(双向链表)。

没有容量:SynchronousQueue

阻塞队列

Map容器

发表于 2019-10-26 | 分类于 容器

HashMap

HashMap基于哈希表来实现,哈希表通过哈希计算可以快速地定位到元素的位置,这样插入,删除,查找元素的时间复杂度都是O(1),但是哈希计算有可能会产生哈希冲突,解决的办法包括拉链法,开放地址法等。

HashMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Node(同时包含了 Key 和 Value) 。

它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全。

在存储结构上,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。

HshMap

阅读全文 »

List容器

发表于 2019-10-19 | 分类于 容器

容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

Collection

Collection

Collection主要分为三类:Set,List,Queue。

Set是无序存储,而且不会存储重复的元素。

Queue是一个队列,只能从一端插入元素,从另一端取出元素。

阅读全文 »

线程池

发表于 2019-10-19 | 分类于 并发

线程池主要是为了避免频繁创建线程和销毁线程。当单个任务处理时间比较长或者是需要处理的任务量很大,为了避免系统效率降低,可以创建线程池,复用线程。

阅读全文 »

Semaphore and CountDownLatch

发表于 2019-10-16 | 分类于 并发

Semaphore

Semaphore叫做信号量,是一种共享锁,当其permit大于0时,线程可以获取锁,当permits小于0时,线程只能等待获取锁,等其他线程释放。

Semophore也有公平锁和非公平锁两种状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//公平共享锁尝试获取acquires个信号量
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors()) //前面是否有排队,有则返回获取失败
return -1;
int available = getState(); //剩余的信号量(旋转寿司店剩余的座位)
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining)) // 剩余信号量不够,够的情况下尝试获取(旋转寿司店座位不够,或者同时来两对情况抢座位)
return remaining;
}
}
//非公平共享锁尝试获取acquires个信号量
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState(); //剩余的信号量(旋转寿司店剩余的座位)
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining)) // 剩余信号量不够,够的情况下尝试获取(旋转寿司店座位不够,或者同时来两对情侣抢座位)
return remaining;
}
}

CountDownLatch

CountDownLatch用于协调多个线程的同步,能让一个线程在等待其他线程执行完任务后,再继续执行。内部是通过一个计数器去完成实现。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,可以调用countDown(),计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程(使用await()阻塞)就可以恢复执行任务。

使用场景

  • 一个线程执行需要等待其他线程执行完毕,才能继续执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public static void main(String[] args) {
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++) {
    new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + "---start");
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "---finish");
    //计数器减一
    latch.countDown();
    }).start();
    }
    try {
    //将主线程阻塞在latch,直到latch的计数器等于0
    latch.await();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("执行完毕-----");
    }

常用方法

CountDownLatch通过state来表示计数器。

  • await() 将当前线程阻塞在CountDownLatch上,直到计数器的数量减少至0.
  • await(long timeout, TimeUnit unit),与await()不同的是,设置了超时等待,等到了时间,不管计数器是不是0,都会继续执行。
  • countDown(),计数器数量减一。当计数器数量减为0时,将await()的线程唤醒。

CyclicBarrier

用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。

和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。

CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。

CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。

img

读写锁

发表于 2019-10-14 | 分类于 并发

ReentrantReadWriteLock:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。

读锁是共享锁,可以有多个线程读;而写锁是独占锁,同时只可能有一个线程写。

共享变量

读锁可以多线程访问,写锁只可以有一个线程访问,我们很容易想到可以使用两个变量来表示读写状态。但是,AQS却只是使用一个state来实现。

1
2
3
4
5
6
7
8
9
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

举个例子来看:

这里有两个关键方法sharedCount和exclusiveCount,通过名字可以看出sharedCount是共享锁的数量,exclusiveCount是独占锁的数量。

共享锁通过对c像右位移16位获得,独占锁通过和16位的1与运算获得。

state前十六位代表读锁,后十六位代表写锁。

举个例子,当获取读锁的线程有3个,写锁的线程有1个(当然这是不可能同时有的),state就表示为0000 0000 0000 0011 0000 0000 0000 0001,高16位代表读锁,通过向右位移16位(c >>> SHARED_SHIFT)得倒10进制的3,通过和0000 0000 0000 0000 1111 1111 1111 1111与运算(c & EXCLUSIVE_MASK),获得10进制的1。

由于16位最大全1表示为65535,所以读锁和写锁最多可以获取65535个。

WriteLock

写锁是一把独占锁,同时只可能有一个线程访问,而且不可能与读锁同时存在,所以与ReentrantLock不同的是,WriteLock不仅要判断是否还有其它写线程占用,还要考虑是否还有读线程占用。

读锁是否存在。因为要确保写锁的操作对读锁是可见的。如果在存在读锁的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected final boolean tryAcquire(int acquires) {

Thread current = Thread.currentThread();
int c = getState(); //获取共享变量state
int w = exclusiveCount(c); //获取写锁数量
if (c != 0) { //有读锁或者写锁
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) //写锁为0(证明有读锁),或者持有写锁的线程不为当前线程
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires); //当前线程持有写锁,为重入锁,+acquires即可
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) //CAS操作失败,多线程情况下被抢占,获取锁失败。CAS成功则获取锁成功
return false;
setExclusiveOwnerThread(current);
return true;
}

锁降级

在获取写锁的时候,如果资源存在读锁,因为可能存在多个不同的线程读,要是修改了线程除了本线程别的线程也感知不到,那么肯定是无法获取写锁的。

但是,在获取读锁的时候, 如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁(锁降级),主要原因是因为在同一个线程内,写锁所做的修改读锁时立即可见的,但是在别的线程内就没有可见性了。

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
class CachedData {
Object data;
//保证状态可见性
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 在获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
//再次检查其他线程是否已经抢到
if (!cacheValid) {
//获取数据
data = ...
cacheValid = true;
}
// 在释放写锁之前通过获取读锁来降级
rwl.readLock().lock();
//释放写锁,保持读锁
rwl.writeLock().unlock();
}

use(data);
rwl.readLock().unlock();
}
}

ReadLock

  • 申请读锁,资源上没有写锁,且读锁数量小于最大值,申请读锁成功。
  • 申请读锁,资源上有写锁,且写锁就在本线程上,那么申请成功。

读写锁

锁降级

读写锁

ReentranrLock

发表于 2019-10-13 | 分类于 并发

ReentrantLock是可重入锁,它实现了Lock接口。

可重入锁就是说同一个线程可以多次申请到该锁。

ReentrantLock有公平锁和非公平锁两种方式。

1
2
3
4
5
6
7
8
public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }

使用lock和unlock进行加锁和解锁。

阅读全文 »

JVM优化

发表于 2019-10-12 | 分类于 JVM

Java有三种编译器i,一种是前端编译器,就是将java文件转变为class文件,这是在编译阶段。一种运行期编译器(JIT编译器),将字节码文件转变为机器码,这是在运行阶段。

编译期优化

编译过程主要分为:

  • 词法语法分析。
  • 填充符号表。
  • 注解处理器。
  • 语义分析。
  • 生成字节码。

泛型

泛型是java语法糖的一种,他的本质是参数化类型。

泛型主要有泛型类,泛型接口,泛型方法。

Java中的泛型只存在于编译阶段,只是用来在编译阶段进行数据校验的作用,在运行时期,泛型就被擦除了,替换为他的原生类型。

1
2
3
4
5
6
7
8
9
10
11
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
System.out.println("----equals----");
}

//输出:----equals----

在运行阶段,泛型已被擦除,所以,都被替换为ArrayList,是相同的。

个人觉得泛型的作用就是在编译阶段进行语义的审查的作用。

泛型擦除只是从字节码中擦除了,但是元数据中还是保留了泛型信息,所以,我们还是可以通过反射手段取得参数化类型。

  • 获取方法返回泛型
1
2
Method method = MyClass.class.getMethod("getStringList", null);
Type returnType = method.getGenericReturnType();
  • 获取成员变量泛型参数
1
2
Field field = MyClass.class.getField("stringList");
Type genericFieldType = field.getGenericType();

运行期优化

javac 将程序源代码编译,转换成 java 字节码,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多。为了提高执行速度,引入了 JIT 技术。

当JVM发现某一段代码执行特别频繁的时候,就会认为他是热点代码,为了提高执行效率,虚拟机就会用过JIT编译器将这些代码编译成机器码,缓存下来。

title

那么,为什么不直接编译呢?

首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。当然,如果一段代码频繁的调用方法,或是一个循环,也就是这段代码被多次执行,那么编译就非常值得了。因此,编译器具有的这种权衡能力会首先执行解释后的代码,然后再去分辨哪些方法会被频繁调用来保证其本身的编译。

JVM是采用解释器与编译器并行的架构。

程序启动时,解释器首先发挥作用,省掉编译的时间,迅速执行。

程序运行后,随着时间的推移,编译器发挥作用,将代码编译成机器码,获取更高执行效率。

HotSpot有两个即时编译器,Client Compiler和Server Compiler,一个注重优化速度,一个注重优化质量。

Client Compiler:编译速度快,优化简单可靠。

Server Compiler:会有一些编译时间比较长的优化。

热点代码

热点代码有两类。

  • 被多次调用的方法。
  • 被多次执行的循环体。

那么,怎么计数呢?

  • 基于采样的热点探测。虚拟机周期性的检查各个线程栈顶,如果某个方法频繁出现,那么这就是一个热点方法。
  • 基于计数器的热点探测。为每个方法(代码块)建立计数器,统计执行次数。

在HotSpot采用的是第二种,有方法计数器来统计方法调用次数,回边计数器来统计循环代码调用次数。当计数器超过阈值的时候,就会对其进行编译。

优化技术

公共子表达式消除

如果一个表达式E计算过了,且他的值没有任何变化,那么E再次出现就没必要再次计算,直接用结果。

方法内联

内联举例:

1
2
3
4
5
6
7
public int  add(int a, int b , int c, int d){
return add(a, b) + add(c, d);
}

public int add(int a, int b){
return a + b;
}

内联之后:

1
2
3
public int  add(int a, int b , int c, int d){
return a + b + c + d;
}

调用一个方法需要建立栈帧等,成本比较大,方法内联可以很好的消除方法调用的成本。

内联条件:

  • 热点代码。
  • 方法体不是太大。
  • 如果希望方法被内联,尽量用private、static、final修饰,这样jvm可以直接内联。如果是public、protected修饰方法jvm则需要进行类型判断,因为这些方法可以被子类继承和覆盖,jvm需要判断内联究竟内联是父类还是其中某个子类的方法。

但是,内联并不是这么简单的,我们的程序中大多都是虚方法(不用private,final,static修饰的),那么就会有多态的可能,不知道会不会有子类重写了方法。

JVM团队采用CHA来解决这个问题。

  • 方法是非虚方法,直接内联即可。
  • 是虚方法,看程序内该方法是否有多个实现,若只有一个,直接内联。
  • 若有多个,采取内联缓存。内联缓存中保存的是第一次调用使用的版本,并且以后每次调用都会比较版本信息,一致,继续使用。
  • 不一致,取消内联。

逃逸分析

  • 方法逃逸:一个对象在方法中被定义后,可能被其他方法使用。
  • 线程逃逸:一个对象在方法中定义后,可以被别的线程访问到。

如果一个对象没有逃逸,可以对其做以下优化。

  • 栈上分配。如果一个对象没有逃逸出方法之外,那么只会在一个方法内部访问,这样的话,将其分配在栈上,方法结束,对象自动被销毁,GC压力会小很多。
  • 同步消除。因为一个对象没有逃逸,所以不会被外部线程访问到,所以同步措施也可以消除。
  • 标量替换。标量是指一个数据无法再分解成更小的数据,例如基本数据类型,引用类型等。如果一个对象不会被外部访问,可以选择不创建这个对象,转而直接创建这个对象会被方法调用的成员变量。
12345
SKJin

SKJin

49 日志
12 分类
16 标签
RSS
GitHub E-Mail
© 2020 SKJin
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4