茂展的分享博客

多线程总结(三)以及HashMap底层源码跟读

多线程的总结(三)

Synchronized有哪几种用法

方法声明时使用

放在声明符之后,返回值之前,即一次只有一个线程进入该方法。,其他线程排队等候,等当前线程执行结束之后才可以进入执行。

对于某一代码块使用

synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块

1
2
3
4
5
6
7
8
9
public int synMethod(int a1){

synchronized(a1) {

//一次只能有一个线程进入

}

}

synchronized后面括号里是一对象,此时,线程获得的是对象锁

如果线程进入,获得是对象锁,那么别的线程在该类所有对象上都不能进行任何操作,使用对象级锁范围太过于大,所以性能不高,完全可以让其他线程访问该类上的其他同步方法来共享资源。由于每个对象都有锁,所以可以使用虚拟对象锁来上锁。

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
class FineGrainLock {

MyMemberClass x, y;

Object xlock = new Object(), ylock = new Object();

public void foo() {

synchronized(xlock) {

//access x here

}

//do something here - but don't use shared resources

synchronized(ylock) {

//access y here

}

}

public void bar() {

synchronized(this) {

//access both x and y here

}

//do something here - but don't use shared resources

}

}

不推荐这样使用this,范围太大了,导致进入其他对象不能访问其他同步方法,最好使用虚拟变量来上锁

synchronized后面括号里是类

如果线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步,我们通常用 类.class 来加锁.

锁的四种声明方式总结

锁是和对象相关联的,每个对象有一把锁,为了执行synchronized语句,线程必须能够获得synchronized语句中表达式指定的对象的锁,一个对象只有一把锁,被一个线程获得之后它就不再拥有这把锁,线程在执行完synchronized语句后,将获得锁交还给对象。

在方法前面加上synchronized修饰符即可以将一个方法声明为同步化方法。同步化方法在执行之前获得一个锁。如果这是一个类方法,那么获得的锁是和声明方法的类相关的Class类对象的锁。如果这是一个实例方法,那么此锁是this对象的锁
总之

  1. 同步synchronized(.class)代码块的作用其实和synchronized static方法作用一样。Class锁对类的所有对象实例起作用。synchronized应用在static方法上,那是对当前对应的.Class进行持锁。
  2. synchronized同步方法
    ①对其它的synchronized同步方法或synchronized(this)同步代码块调用是堵塞状态;
    ②同一时间只有一个线程执行synchronized同步方法中的代码。
  3. synchronized(this)同步代码块
    ①对其它的synchronized同步方法或synchronized(this)同步代码块调用是堵塞状态;
    ②同一时间只有一个线程执行synchronized同步方法中的代码

重入锁

可重入锁最大的作用就是避免死锁
中断响应: 对于synchronized块来说,要么获取到锁执行,要么持续等待。而重入锁的中断响应功能就合理地避免了这样的情况。比如,一个正在等待获取锁的线程被“告知”无须继续等待下去,就可以停止工作了

公平锁:所有的等待锁线程都排在队列中,使用FIFO的方式来获取锁
非公平锁:等待线程在无论在不在队列中或者队列末尾都会直接抢夺锁,抢不到进入队列中,下次继续抢夺

1
2
3
4
5
6
7
8
9
10
ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly(); // 以可以响应中断的方式加锁

Thread t1 = new Thread(deadLock1);
Thread t2 = new Thread(deadLock2);
t1.start();
t2.start();
Thread.sleep(1000);

t2.interrupt();//线程t2响应中断,不会继续等待t1所持有的锁

tryLock 锁申请等待限时

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
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockTest implements Runnable{
public static ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
Thread.sleep(2000); //休眠2秒
} else {
System.err.println(Thread.currentThread().getName() + "获取锁失败!");
}
} catch (Exception e) {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
TryLockTest test = new TryLockTest();
Thread t1 = new Thread(test); t1.setName("线程1");
Thread t2 = new Thread(test); t1.setName("线程2");
t1.start();t2.start();
}
}

首先普及一下API
isFair():作用是判断ReentrantLock是否是公平锁。返回true为公平锁,false为非公平锁。
isHeldByCurrentThread()查询当前线程是否保持锁
isLocked():查询锁是否有线程保持。

Fork/Join框架是干什么的(执行任务的框架)

是Java7提供的用于执行任务的框架,是把一个大任务分割成若干个小任务,然后把各个小任务执行的结果汇总就是大任务的结果。
完成两件事情
1. 任务分割:首先Fork/Join框架需要把大的任务分割成多个子任务,如果子任务比较大的话还要继续分割
2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据

HashMap和HashTable

集合线程不安全: 多个线程操作同一个集合时,需要自己设置同步机制,否则会出现异常

  1. HashMap是线程不安全的,多线程环境下可能会发生死锁。HashTable是线程安全的,因为它的每个方法都加入了synchronized方法。HashMap比HashTable执行效率高。当需要多线程的操作的时候,我们可以使用ConcurrentHashMap,多线程环境下ConcurrentHashMap比HashTable的执行效率高好几倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
  2. HashTable键值对都不可以为空,否则会抛出空指针异常。,HashMap的键是可以为空的,但是只可以一个键为空。值也可以为null,所以我们不可以使用get()方法来判断是否map中有没有key,因为返回null并不意味着键不存在,有可能有键但是值为null。所以我们可以使用containsKey()方法来判断
  3. 继承的父类不同。 HashMap继承AbstractMap,而HashTable继承Dictionary类,由于Dictionary类已经被废弃,所以使用的不多了
  4. 初始容量不同和每次扩充容量不同
    HashTable的初始容量是11,而HashMap的初始容量是16。在创建时,如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap则会将其扩充为2的幂次方大小。Hashtable会尽量使用素数、奇数。原因在于两种侧重面不同。HashTable更侧重于将结果分布均匀,是哈希冲突减少。HashMap使用位运算(>> <<)能够快速定位位置。但是Hash冲突也增加了。因为得出的hash值的低位相同的概率比较高,为了解决这个冲突,让取到的位置更分散,然后将得到的hashcode再进行了一次位处理。
    源码:
    HashMap再次hash

什么是装载因子?他有什么作用?

1
2
3
4
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

作用:就是控制什么时候map需要通过resize方法扩容,然后通过rehash方法把原有的元素复制到新的容器里面
是否resize,由装载因子和初始容量决定的。

HashMap源码解读

前面我们知道了装载因子(用来衡量HashMap满的程度),那么HashMap如何计算呢
size: 表示HashMap中所有的KV数量,包括链表和树的总和
然后capacity表示顺序表的长度
然后 size/capacity(初始大小为16,第二次要增加到64,以后每次翻2倍,所以都是2的幂次方)和装载因子比较,大于装载因子就进行resize操作,然后通过rehash方式把原有的元素复制到新的容器里面。
threshold = 装载因子(LoadFactor)*capacity,就是说size的数量什么时候超过threshold就执行resize方法。
那么什么时候会将链表树化呢?跟源码
HashMap何时树化

------本文结束感谢阅读------
🐶 您的支持将鼓励我继续创作 🐶