zhulinyin

  • 首页

  • 归档

算法设计与分析-Week16

发表于 2019-02-03

Russian Doll Envelopes

题目描述

You have a number of envelopes with widths and heights given as a pair of integers (w, h). One envelope can fit into another if and only if both the width and height of one envelope is greater than the width and height of the other envelope.
What is the maximum number of envelopes can you Russian doll? (put one inside other)
Note:
Rotation is not allowed.

Example 1:

Input: [[5,4],[6,4],[6,7],[2,3]]
Output: 3
Explanation: The maximum number of envelopes you can Russian doll is 3 ([2,3] => [5,4] => [6,7]).

解题思路

本题实为俄罗斯套娃问题,给定一组拥有高和宽的信封,当一个信封的高和宽都大于另一个信封时,可将另一个信封装入该信封,层层嵌套,求最大的嵌套数。
由于必须高宽都满足大于的条件,因此可先对数组的其中一维进行排序,如对宽进行排序,然后可仅考虑高,用最长递增子序列可得到问题的解。在对宽进行排序时,若宽相等则高大的要放在前面,如[3, 4]要放在[3, 3]前面,因为此时[3, 3]是不能套进[3, 4]的,若[3, 3]放在前面会影响后面最长递增子序列的计算。
最长递增子序列:
用一个数组arr,arr[i]表示以i结尾的最长递增子序列的长度,初始化arr[0] = 1,要求arr[i],需要比较i和前面所有的元素j,若信封i的高大于信封j的高且arr[j] + 1 > arr[i],则更新arr[i]为arr[j] + 1。

源代码

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
class Solution {
public:
int maxEnvelopes(vector<pair<int, int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](pair<int, int> a, pair<int, int> b){
if(a.first < b.first) return true;
if(a.first == b.first && a.second >= b.second) return true;
return false;
});
int maxEnvelopes = 1;
int n = envelopes.size();
if(n == 0) return 0;
vector<int> arr(n, 0);
arr[0] = 1;
for(int i = 1; i < n; i++) {
arr[i] = 1;
for(int j = 0; j < i; j++) {
if(envelopes[j].second < envelopes[i].second && arr[j] + 1 > arr[i]) {
arr[i] = arr[j] + 1;
}
}
if(arr[i] > maxEnvelopes) maxEnvelopes = arr[i];
}
return maxEnvelopes;
}
};

AsyncTask原理总结

发表于 2019-02-02 | 更新于 2019-02-28

1个任务队列(SerialExecutor)

一个继承于Executor的静态内部类,因此是所有AsyncTask对象所共有的,作为任务调度,内部维护1个双向队列,通过同步锁修饰execute(Runnable),使得任务被一个一个添加进队列,所有AsyncTask任务都是串行执行的。

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
private static class SerialExecutor implements Executor {
// SerialExecutor = 静态内部类
// 即 是所有实例化的AsyncTask对象公有的

// SerialExecutor 内部维持了1个双向队列;
// 容量根据元素数量调节
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;

// execute()被同步锁synchronized修饰
// 即 多个任务需1个个加到该队列中;然后 执行完队列头部的再执行下一个,以此类推
public synchronized void execute(final Runnable r) {
// 将实例化后的FutureTask类 的实例对象传入
// 即相当于:向队列中加入一个新的任务
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
// 若当前无任务执行,则去队列中取出1个执行
if (mActive == null) {
scheduleNext();
}
}

protected synchronized void scheduleNext() {
// 1. 取出队列头部任务
if ((mActive = mTasks.poll()) != null) {

// 2. 执行取出的队列头部任务
// 即 调用执行任务线程池类(THREAD_POOL_EXECUTOR)
THREAD_POOL_EXECUTOR.execute(mActive);

}
}
}

1个线程池(THREAD_POOL_EXECUTOR)

真正执行具体的线程任务,实际上是一个已经配置好的可执行并行任务的线程池。

内部Handler(InternalHandler)

用于在主线程更新线程执行的进度和结果。

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
private static class InternalHandler extends Handler {

// 构造函数
public InternalHandler() {
super(Looper.getMainLooper());
// 获取的是主线程的Looper()
// 故 AsyncTask的实例创建 & execute()必须在主线程使用
}

@Override
public void handleMessage(Message msg) {

AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;

switch (msg.what) {
// 若收到的消息 = MESSAGE_POST_RESULT
// 则通过finish()将结果通过Handler传递到主线程
case MESSAGE_POST_RESULT:
result.mTask.finish(result.mData[0]); ->>分析3
break;

// 若收到的消息 = MESSAGE_POST_PROGRESS
// 则回调onProgressUpdate()通知主线程更新进度的操作
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}

实现步骤

  1. 创建 AsyncTask 子类 & 根据需求实现核心方法:onPreExecute(),doInBackground(arg),onPostExecute(result),onProgressUpdate(progress),onCancelled()
  2. 创建 AsyncTask 子类的实例对象(即 任务实例)
  3. 手动调用execute()或executeOnExecutor(Executor)从而执行异步线程任务

注意

调用cancel(boolean)时并不会中止doInBackground(arg),需要在doInBackground(arg)函数中做判断处理,如isCancelled(),或捕获InterruptedException异常(当 cancel 传入参数为true时,会调用子线程的interrupt方法,可能会抛出InterruptedException异常)

JVM、Dalvik、ART总结

发表于 2019-02-01
  • Dalvik是Google公司自己设计用于Android平台的虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
  • 相比于jar文件包含多个.class文件,每个.class文件里面包含了该类的头信息(如编译版本)、常量池、类信息、域、方法、属性等等,当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,这样会很慢,而移动设备的内存本来就很小,不可能像JVM这样加载,所以它使用的不是.jar文件,而是.apk文件,该文件里面只包含了一个.dex文件,这个.dex文件里面将所有的.class里面所包含的信息全部整合在一起了,这样再加载就很快了。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度。
  • Dalvik指令集是基于寄存器的架构,执行特有的文件格式——dex字节码(适合内存和处理器速度有限的系统)。而JVM是基于栈的。相对于基于栈的JVM而言,基于寄存器的Dalvik VM实现虽然牺牲了一些平台无关性,但是它在代码的执行效率上要更胜一筹。
  • Dalvik与ART的区别: 在Dalvik下,应用每次运行都需要通过即时编译器(JIT)将字节码转换为机器码,即每次都要编译加运行,这虽然会使安装过程比较快,但是会拖慢应用的运行效率。而在ART环境中,应用在第一次安装的时候,字节码就会预编译(AOT)成机器码,这样的话,虽然应用得安装会变慢,但是以后每次启动执行的时候,都可以直接运行,因此运行效率会提高。预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗,但是字节码解释成机器码后,占用的存储空间会变大。

handler机制总结

发表于 2019-01-31

用一个最常见的例子来总结handler的机制:子线程处理耗时操作,然后通知主线程更新UI。

1. 在主线程创建handler对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Handler() {
this(null, false);
}
public Handler(Callback callback, boolean async) {

...// 仅贴出关键代码

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}

mQueue = mLooper.mQueue;
}

创建handler对象的过程会绑定当前线程的looper和该looper的消息队列,主线程的looper在主线程创建时就通过Looper.prepareMainLooper()创建,而子线程要创建looper应该使用Looper.prepare(),因此在子线程创建handler时应先调用Looper.prepare()创建looper。looper在创建的时候就会创建其消息队列。

2. 在子线程处理完耗时操作后通过主线程的handler发送消息

发送消息有两种方式:

  1. sendMessage(Message msg)
  2. post(Runnable r)

两者最后均为调用sendMessageAtTime(Message msg, long uptimeMillis),区别在于后者的msg.callback = r。消息队列会根据消息处理的时间将消息放在合适的位置,而不是直接放在队尾。

3. 主线程的looper会不断从消息队列中取出消息进行分发

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 static void loop() {
...// 仅贴出关键代码
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// myLooper()作用:返回sThreadLocal存储的Looper实例;若me为null 则抛出异常
// 即loop()执行前必须执行prepare(),从而创建1个Looper实例

final MessageQueue queue = me.mQueue;
// 获取Looper实例中的消息队列对象(MessageQueue)

for (;;) {

Message msg = queue.next();
if (msg == null) {
return;
}
// next():取出消息队列里的消息

msg.target.dispatchMessage(msg);
// 把消息Message派发给消息对象msg的target属性
// target属性实际是1个handler对象

msg.recycle();
}
}

loop()首先获取当前线程的looper,因此调用该函数之前必须确定当前线程已经创建了looper。通过一个死循环,从消息队列里取出消息进行分发。

注意: 当消息队列没有消息取出来时,主线程会阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,重新唤醒主线程工作,所以大多数时候主线程处于休眠状态,不会消耗大量CPU资源。

4. 消息会被分发到指定的handler进行处理,更新UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void dispatchMessage(Message msg) {

// 1. 若msg.callback属性不为空,则代表使用了post(Runnable r)发送消息
// 则执行handleCallback(msg),即回调Runnable对象里复写的run()
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}

// 2. 若msg.callback属性为空,则代表使用了sendMessage(Message msg)发送消息
// 则执行handleMessage(msg),即回调重写的handleMessage(msg)
handleMessage(msg);

}
}
private static void handleCallback(Message message) {
message.callback.run();
// Message对象的callback属性 = 传入的Runnable对象
// 即回调Runnable对象里复写的run()
}

关于内存泄漏

由于Java的非静态内部类和匿名内部类会隐式地持有外部类的引用,所以下面的代码会存在内存泄漏的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SampleActivity extends Activity {

private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

当activity被finish的时候,延迟发送的消息仍然会存活在UI线程的消息队列中,直到10分钟后它被处理掉。这个消息持有activity的Handler的引用,Handler又隐式的持有它的外部类(这里就是SampleActivity)的引用。这个引用会一直存在直到这个消息被处理,所以垃圾回收机制就没法回收这个activity,内存泄露就发生了。需要注意的是:匿名Runnable子类也会导致内存泄露。非静态的匿名类会隐式的持有外部类的引用,所以context会被泄露掉。

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
public class SampleActivity extends Activity {

/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}

private final MyHandler mHandler = new MyHandler(this);

/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

实现Handler的子类或者使用static修饰内部类。静态的内部类不会持有外部类的引用,所以activity不会被泄露。如果要在Handler内调用外部activity类的方法的话,可以让Handler持有外部activity类的弱引用,这样也不会有泄露activity的风险。关于匿名类造成的泄露问题,我们可以用static修饰这个匿名类对象解决这个问题,因为静态的匿名类也不会持有它外部类的引用。

Java多线程

发表于 2019-01-30 | 更新于 2019-03-08

线程状态

  1. 新建状态(New): 新创建一个线程对象。
  2. 就绪状态(Runnable): 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running): 就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    (2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态结束、join()状态中断、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep不会释放持有的锁)
  5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程调度

Java的线程调度是抢占式调度,线程具有线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),共十个级别(1-10),当两个线程均处于就绪状态时,优先级越高的线程越容易被系统选择执行。但是Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

常用函数

  1. Thread.sleep(long millis):使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,转为就绪(Runnable)状态。sleep()平台移植性好。sleep()不释放锁。
  2. Object.wait():使当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
  3. Thread.yield():暂停当前正在执行的线程对象,使当前线程转为就绪状态。
  4. Thread.join():等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  5. Object.notify()/Object.notifyAll():唤醒在此对象监视器上等待的单个/全部线程。wait()/notify()/notifyAll()都应该在synchronized修饰的块中执行。

多线程实现方式

  1. 继承Thread,实现run函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Thread1 extends Thread{  
    public void run() {
    //执行操作
    }
    }
    public class Main {
    public static void main(String[] args) {
    Thread1 mTh1=new Thread1();
    mTh1.start();
    }
    }
  2. 实现Runnable接口,实现run函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Thread1 implements Runnable{  
    public void run() {
    //执行操作
    }
    }
    public class Main {
    public static void main(String[] args) {
    new Thread(new Thread1()).start();
    }
    }
  3. Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

线程同步

  • synchronized

当修饰普通方法时,同一个对象调用该方法需要获得同步锁,否则阻塞;当修饰静态方法时,会锁住整个类;可修饰一个语句块。

  • volatile

volatile修饰的变量对所有线程可见,当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去,这个写操作会导致其他线程中的缓存无效。但是volatile不会提供原子操作,对于复合操作如num++不能实现线程同步。

  • ReentrantLock
    1
    2
    3
    Lock lock = new ReentrantLock();
    lock.lock();
    lock.unlock();

使用lock()加锁,使用unlock()释放锁。

  • ThreadLocal

    • ThreadLocal 并不解决线程间共享数据的问题
    • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
    • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
    • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • concurrent

使用java.util.concurrent包提供的工具,如ConcurrentHashMap。

  • concurrent.atomic

使用java.util.concurrent.atomic包提供的原子变量实现线程同步,如AtomicInteger。

线程池(ThreadPoolExecutor)

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,而使用线程池就可以对线程进行复用。线程池详解

四种线程池

  1. 定长线程池(FixedThreadPool)
    • 特点:只有核心线程 & 不会被回收、线程数量固定、任务队列无大小限制(超出的线程任务会在队列中等待)
    • 应用场景:控制线程最大并发数
  2. 定时线程池(ScheduledThreadPool)
    • 特点:核心线程数量固定、非核心线程数量无限制(闲置时马上回收)
    • 应用场景:执行定时 / 周期性 任务
  3. 可缓存线程池(CachedThreadPool)
    • 特点:只有非核心线程、线程数量不固定(可无限大)、灵活回收空闲线程(具备超时机制,全部回收时几乎不占系统资源)、新建线程(无线程可用时)、任何线程任务到来都会立刻执行,不需要等待
    • 应用场景:执行大量、耗时少的线程任务
  4. 单线程化线程池(SingleThreadExecutor)
    • 特点:只有一个核心线程(保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题)
    • 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作,文件操作等

Touch事件分发机制

发表于 2019-01-29 | 更新于 2019-03-01

  1. Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。
  2. ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViewGroup。
  3. 触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。
  4. 当Acitivty接收到Touch事件时,将调用其ViewGroup的dispatchTouchEvent函数,该函数会递归地调用其子View的dispatchTouchEvent函数,遍历子View进行Down事件的分发。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchEvent函数中返回true。
  5. 当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。
  6. 当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouchEvent事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。
  7. onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。当子控件已经接收到前驱事件如Down事件,而后续事件被父控件拦截时,子控件会收到Cancel事件。

Java的四种引用方式

发表于 2019-01-28 | 更新于 2019-03-01

java对象的引用包括强引用,软引用,弱引用,虚引用。Java中提供这四种引用类型主要有两个目的:一是可以让程序员通过代码的方式决定某些对象的生命周期;二是有利于JVM进行垃圾回收。

强引用

强引用是指创建一个对象并把这个对象赋给一个引用变量。例如:

1
2
Object object =new Object();
String str ="hello";

强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。可以显示地将引用赋值为null,JVM就会在合适的时间回收该对象。

软引用

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留(软引用有两个特殊的变量:clock——记录上一次GC的时间;timestamp——记录该变量上一次get的时间。这两个变量之差表示这个软引用对象距离上次GC时一直没被使用的时间,当这个差值大于阈值_max_interval(该阈值由上一次GC后剩余可用空间计算得出,空间越大,该阈值越大)时,则该对象标记为可废弃的,将在下一次GC时回收)。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。

1
2
3
MyObject aRef = new  MyObject();  
SoftReference aSoftRef=new SoftReference(aRef);
aRef = null;

将MyObject对象变成软引用对象,并结束对这个对象的强引用。

1
MyObject anotherRef=(MyObject)aSoftRef.get();

SoftReference保存一个Java对象的软引用后,在垃圾线程对Java对象回收前,可以通过SoftReference类提供的get方法获得该Java对象的强引用。但是一旦垃圾线程回收了该Java对象,get方法返回null。

作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以当软可及对象被回收之后,这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如:

1
2
ReferenceQueue queue = new  ReferenceQueue();  
SoftReference ref=new SoftReference(aMyObject, queue);

当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。

在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。

弱引用

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

1
2
3
4
5
6
public static void main(String[] args) {  
WeakReference<People>reference=new WeakReference<People>(new People("zhouqian",20));
System.out.println(reference.get());
System.gc();
System.out.println(reference.get());
}

第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。

1
2
3
4
5
6
7
public static void main(String[] args) {  
People people=new People("zhouqian",20);
WeakReference<People>reference=new WeakReference<People>(people);
System.out.println(reference.get());
System.gc();
System.out.println(reference.get());
}

两个输出结果相同。

虚引用

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。

1
2
3
4
5
public static void main(String[] args) {  
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}

Java内存回收机制

发表于 2019-01-28 | 更新于 2019-03-16

内存回收的意义

当我们使用C++这门开发语言时,每次new出来的变量都需要手动delete掉,否则会出现内存泄漏的问题。而Java语言中一个显著的特点就是引入了内存回收机制,它使得Java程序员在编写程序的时候不再需要考虑内存管理,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

内存回收机制的算法

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

1.引用计数法(Reference Counting Collector)

算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,则该对象的引用计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

优点

引用计数收集器可以很快的执行,交织在程序运行中。对需要不被长时间打断的程序比较有利。

缺点

无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样他们的引用计数永远不可能为0。

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2;
object2.object = object1;

object1 = null;
object2 = null;
}
}

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep)

算法分析

tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。

优点

解决了循环引用的问题。

缺点

该算法不需要进行对象的移动,并且仅对不可达的对象进行处理,在可达对象比较多的情况下极为高效,但由于标记-清除算法直接回收不可达的对象,因此会造成内存碎片。

3.标记-整理算法(Mark-Compact)

算法分析

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

优点

解决了内存碎片的问题。

缺点

由于需要对对象进行移动,因此成本较高。

4.copying算法

算法分析

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象面和一个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

优点

实现简单,运行高效且不容易产生内存碎片

缺点

对内存空间的使用付出了巨大的代价,因为可用的内存空间减少了一半。Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

5.Generational Collection(分代收集)算法

算法分析

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

  • 年轻代(Young Generation)
    年轻代就是为了快速清理掉那些生存周期短的对象而设立的,年轻代分为三个模块,一个eden区,两个survivor区(survivor0和survivor1),它们内存按8:1:1分配,一个新的对象建立首先在eden区,年轻代的回收叫做minor GC,在回收时将eden区中存活的对象复制到survivor0区中,然后清空eden区。当survivor0区的内存被存满时,eden区和survivor0区将全部存活的对象存入survivor1区。然后将survivor0区和eden区清空,将survivor1区与survivor0区交换,以此一直循环,直到回收时survivor1区存不下survivor0+eden区的存活对象时就将存活对象放入老年代。
  • 老年代(Old Generation)
    当对象在年轻代经历过次次历练后,他终于存活到了老年区,所以老年代中的对象大多都是一些生命周期比较长的对象,老年代也比年轻代分到到的内存要大,默认是1:2。当老年代内存也存满时就会触发一次full GC或者叫major GC,也就是对年轻代老年代都进行回收。
  • 持久代(Permanent Generation)
    用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

GC root

GC root是一组根引用,由于Java垃圾回收主要是针对堆内存,因此这些引用则来自于JVM运行时数据区的其它几部分:虚拟机栈,本地方法区,方法区。主要包括:

  1. 虚拟机栈中的局部变量表。
  2. 类的静态属性引用。
  3. 常量对象引用。
  4. 本地方法区中的对象引用。

Java关键字及其作用

发表于 2019-01-28 | 更新于 2019-02-01

native

native 关键字可以应用于方法,以指示该方法是用 Java 以外的语言实现的。

strictfp

strictfp的意思是FP-strict,也就是说精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点运算的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令人满意。而一旦使用了strictfp来声明一个类、接口或者方法时,那么所声明的范围内Java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。因此如果想让浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,那就请用关键字strictfp。
可以将一个类、接口以及方法声明为strictfp,但是不允许对接口中的方法以及构造函数声明strictfp关键字

synchronized

synchronized 关键字可以应用于方法或语句块,使得该代码块一次只能被一个线程执行。
如果应用于静态方法,那么,当该方法一次由一个线程执行时,整个类将被锁定。
如果应用于实例方法,那么,当该方法一次由一个线程访问时,该实例将被锁定。
如果应用于对象或数组,当关联的代码块一次由一个线程执行时,对象或数组将被锁定。

transient

transient 关键字可以应用于类的成员变量,使得当该类的实例被序列化时,该变量不被序列化。

volatile

volatile 关键字用于表示可以被多个线程异步修改的成员变量。
volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。

throw 和 throws

throw语句用在方法体内,表示抛出异常,由方法体内的语句处理。
throws主要是声明这个方法可能会抛出这种类型的异常,使它的调用者知道要捕获这个异常。

HashMap、Hashtable、ConcurrentHashMap的原理(jdk11)

发表于 2019-01-27 | 更新于 2019-01-28

HashMap

重要方法

  • static final int hash(Object key):获取key的hash值,当key为空时,hash值为0;否则hash值由两部分组成:前16位为key的hashcode的前16位,后16位为key的hashcode的前16位和后16位的异或。

    1
    2
    3
    4
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • static final int tableSizeFor(int cap):获取最小大于指定容量cap的2次幂,如cap为6时返回8。

    1
    2
    3
    4
    static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • public V get(Object key):获取指定key值的value。

  • final Node<K,V> getNode(int hash, Object key):获取指定hash值和key值的节点。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    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))))
    return first;
    if ((e = first.next) != null) {
    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;
    }

通过hash值的低位(与容量进行与操作)得到节点在数组中对应的索引,得到first节点,如果是普通的链表节点,则first为链表头,通过链表头向后遍历搜索对应的key,得到相应的节点。

  • public V put(K key, V value):将键值对放入hash表中。
  • final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict):将键值对放入hash表中,当onlyIfAbsent为true且key已存在时,不会改变已存在的value的值,且返回之前的value,否则返回null。
    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
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
    }
    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))))
    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) {
    p.next = newNode(hash, key, value, null);
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
    break;
    }
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    break;
    p = e;
    }
    }
    if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    e.value = value;
    afterNodeAccess(e);
    return oldValue;
    }
    }
    ++modCount;
    if (++size > threshold)
    resize();
    afterNodeInsertion(evict);
    return null;
    }

首先通过key的hash值得到对应的索引,若得到的节点为空,则直接插入键值对;若得到的节点不为空且不是树节点,则该节点为链表头,依次遍历该链表,若找到相同的key,则替换对应的value值,因为onlyIfAbsent默认为false;若到达链表的末尾,则将新的键值对插入到链表末尾,若此时链表的长度大于等于8(TREEIFY_THRESHOLD),则将链表转化为红黑树,因为链表太长会限制查找速度,这样做是为了提高查询性能。若hash表中元素的个数超过阈值,则进行扩容。

  • final Node<K,V>[] resize():对hash表进行扩容。将数组长度扩大到原来的两倍,对原来数组中的元素进行重分配,若(e.hash & oldCap) == 0则分配在低链上,否则分配在高链上。例如原来的数组长度为8,则下标为0-7,如果有两个元素的key的hash值的低四位分别为0000和1000,那么在旧表中会被分配到下标为0的链表中,而在新表中会被分别分配到下标为0和下标为8的链表中。
    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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
    else { // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
    Node<K,V> e;
    if ((e = oldTab[j]) != null) {
    oldTab[j] = null;
    if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;
    else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    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;
    loTail = e;
    }
    else {
    if (hiTail == null)
    hiHead = e;
    else
    hiTail.next = e;
    hiTail = e;
    }
    } while ((e = next) != null);
    if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
    }
    if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
    }
    }
    }
    }
    }
    return newTab;
    }

总结

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • 默认初始size为16,扩容时扩充为原来的两倍,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 当插入元素后Map中元素总数超过Entry数组的0.75(默认加载因子),触发扩容操作,为了减少链表长度,元素分配更均匀,增加查询效率
  • 当插入元素后,当前链表长度大于8时会将当前链表转换成红黑树,提高查询效率

Hashtable

重要方法

  • public synchronized V get(Object key):获取指定key值的value。通过key的hash值获得其在数组中的索引,遍历链表,若找到相同的key,则返回对应的value;否则返回null。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @SuppressWarnings("unchecked")
    public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {
    return (V)e.value;
    }
    }
    return null;
    }
  • protected void rehash():重哈希,相当于HashMap的resize扩容操作。将数组长度扩大到原来的两倍加一,并把原来表中的元素重新计算索引值放到新表中。

    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
    @SuppressWarnings("unchecked")
    protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
    if (oldCapacity == MAX_ARRAY_SIZE)
    // Keep running with MAX_ARRAY_SIZE buckets
    return;
    newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;

    for (int i = oldCapacity ; i-- > 0 ;) {
    for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
    Entry<K,V> e = old;
    old = old.next;

    int index = (e.hash & 0x7FFFFFFF) % newCapacity;
    e.next = (Entry<K,V>)newMap[index];
    newMap[index] = e;
    }
    }
    }
  • public synchronized V put(K key, V value):将键值对放入表中。首先判断值是否为空,为空则抛出异常。计算key的hash值(若key为空也会抛出异常,因为没有对key为空做处理)得到其在数组中的索引,遍历该索引指向的链表,若找到相同的key值,则更新value值并返回原来的value值;若没有找到则将键值对插入到链表头。

    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
    private void addEntry(int hash, K key, V value, int index) {
    Entry<?,?> tab[] = table;
    if (count >= threshold) {
    // Rehash the table if the threshold is exceeded
    rehash();

    tab = table;
    hash = key.hashCode();
    index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    modCount++;
    }
    public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
    throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
    if ((entry.hash == hash) && entry.key.equals(key)) {
    V old = entry.value;
    entry.value = value;
    return old;
    }
    }

    addEntry(hash, key, value, index);
    return null;
    }

总结

  • 与HashMap一样,底层由数组+链表实现,但是key和value都不能为null,由于公共方法都用了synchronized关键字进行修饰,因此是线程安全的
  • 初始默认容量为11,加载因子为0.75,每次扩容容量变为原来的两倍加一,容量不一定是2的指数次方,每次插入元素前判断元素个数是否大于阈值,如果是则需要先进行扩容,再插入元素
  • 在链表头插入元素
  • 计算数组下标时是使用取模的运算,相比于HashMap的位运算效率会低一些
  • HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低

ConcurrentHashMap

总结


HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

  • ConcurrentHashMap中的key和value不能为空
  • ConcurrentHashMap由多个Segment组成,每个Segment相当于一个Hashtable
  • ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高
12345

Li Jiangtao

48 日志
14 标签
© 2019 Li Jiangtao
由 Hexo 强力驱动 v3.7.0
|
主题 – NexT.Mist v7.1.1
0%