多线程的总结(三)
Synchronized有哪几种用法
方法声明时使用
放在声明符之后,返回值之前,即一次只有一个线程进入该方法。,其他线程排队等候,等当前线程执行结束之后才可以进入执行。
对于某一代码块使用
synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块
1 | public int synMethod(int a1){ |
synchronized后面括号里是一对象,此时,线程获得的是对象锁
如果线程进入,获得是对象锁,那么别的线程在该类所有对象上都不能进行任何操作,使用对象级锁范围太过于大,所以性能不高,完全可以让其他线程访问该类上的其他同步方法来共享资源。由于每个对象都有锁,所以可以使用虚拟对象锁来上锁。
1 | class FineGrainLock { |
不推荐这样使用this,范围太大了,导致进入其他对象不能访问其他同步方法,最好使用虚拟变量来上锁
synchronized后面括号里是类
如果线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步,我们通常用 类.class 来加锁.
锁的四种声明方式总结
锁是和对象相关联的,每个对象有一把锁,为了执行synchronized语句,线程必须能够获得synchronized语句中表达式指定的对象的锁,一个对象只有一把锁,被一个线程获得之后它就不再拥有这把锁,线程在执行完synchronized语句后,将获得锁交还给对象。
在方法前面加上synchronized修饰符即可以将一个方法声明为同步化方法。同步化方法在执行之前获得一个锁。如果这是一个类方法,那么获得的锁是和声明方法的类相关的Class类对象的锁。如果这是一个实例方法,那么此锁是this对象的锁
总之:
- 同步synchronized(.class)代码块的作用其实和synchronized static方法作用一样。Class锁对类的所有对象实例起作用。synchronized应用在static方法上,那是对当前对应的.Class进行持锁。
- synchronized同步方法
①对其它的synchronized同步方法或synchronized(this)同步代码块调用是堵塞状态;
②同一时间只有一个线程执行synchronized同步方法中的代码。 - synchronized(this)同步代码块
①对其它的synchronized同步方法或synchronized(this)同步代码块调用是堵塞状态;
②同一时间只有一个线程执行synchronized同步方法中的代码
重入锁
可重入锁最大的作用就是避免死锁
中断响应: 对于synchronized块来说,要么获取到锁执行,要么持续等待。而重入锁的中断响应功能就合理地避免了这样的情况。比如,一个正在等待获取锁的线程被“告知”无须继续等待下去,就可以停止工作了
公平锁:所有的等待锁线程都排在队列中,使用FIFO的方式来获取锁
非公平锁:等待线程在无论在不在队列中或者队列末尾都会直接抢夺锁,抢不到进入队列中,下次继续抢夺
1 | ReentrantLock lock = new ReentrantLock(); |
tryLock 锁申请等待限时
1 | import java.util.concurrent.TimeUnit; |
首先普及一下API
isFair():作用是判断ReentrantLock是否是公平锁。返回true为公平锁,false为非公平锁。
isHeldByCurrentThread()查询当前线程是否保持锁
isLocked():查询锁是否有线程保持。
Fork/Join框架是干什么的(执行任务的框架)
是Java7提供的用于执行任务的框架,是把一个大任务分割成若干个小任务,然后把各个小任务执行的结果汇总就是大任务的结果。
完成两件事情
1. 任务分割:首先Fork/Join框架需要把大的任务分割成多个子任务,如果子任务比较大的话还要继续分割
2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据
HashMap和HashTable
集合线程不安全: 多个线程操作同一个集合时,需要自己设置同步机制,否则会出现异常
- HashMap是线程不安全的,多线程环境下可能会发生死锁。HashTable是线程安全的,因为它的每个方法都加入了synchronized方法。HashMap比HashTable执行效率高。当需要多线程的操作的时候,我们可以使用ConcurrentHashMap,多线程环境下ConcurrentHashMap比HashTable的执行效率高好几倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
- HashTable键值对都不可以为空,否则会抛出空指针异常。,HashMap的键是可以为空的,但是只可以一个键为空。值也可以为null,所以我们不可以使用get()方法来判断是否map中有没有key,因为返回null并不意味着键不存在,有可能有键但是值为null。所以我们可以使用containsKey()方法来判断
- 继承的父类不同。 HashMap继承AbstractMap,而HashTable继承Dictionary类,由于Dictionary类已经被废弃,所以使用的不多了
- 初始容量不同和每次扩充容量不同
HashTable的初始容量是11,而HashMap的初始容量是16。在创建时,如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap则会将其扩充为2的幂次方大小。Hashtable会尽量使用素数、奇数。原因在于两种侧重面不同。HashTable更侧重于将结果分布均匀,是哈希冲突减少。HashMap使用位运算(>> <<)能够快速定位位置。但是Hash冲突也增加了。因为得出的hash值的低位相同的概率比较高,为了解决这个冲突,让取到的位置更分散,然后将得到的hashcode再进行了一次位处理。
源码:HashMap再次hash
什么是装载因子?他有什么作用?
1 | /** |
作用:就是控制什么时候map需要通过resize方法扩容,然后通过rehash方法把原有的元素复制到新的容器里面
是否resize,由装载因子和初始容量决定的。
HashMap源码解读
前面我们知道了装载因子(用来衡量HashMap满的程度),那么HashMap如何计算呢 HashMap何时树化
size: 表示HashMap中所有的KV数量,包括链表和树的总和
然后capacity表示顺序表的长度
然后 size/capacity(初始大小为16,第二次要增加到64,以后每次翻2倍,所以都是2的幂次方)和装载因子比较,大于装载因子就进行resize操作,然后通过rehash方式把原有的元素复制到新的容器里面。
threshold = 装载因子(LoadFactor)*capacity,就是说size的数量什么时候超过threshold就执行resize方法。
那么什么时候会将链表树化呢?跟源码