多线程及锁总结
注:本博客参考了网上的文章结合自己工作总结后所写,主要用于记录自己工作所得,如有错误请批评指正。
参考:https://blog.csdn.net/tyyj90/article/details/78236053
参考:https://www.cnblogs.com/wxd0108/p/5479442.html
想了解多线程底层及原理的
参考:http://hbprotoss.github.io/post/java多线程总结/
1.多线程简介
1.1简介:
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。
1.2使用条件:
多个任务执行顺序不影响结果。
1.3使用场景:
- 提高用户体验,用户交互问题、避免用户等待时间过长。
- 减少阻塞,为了等待网络、文件系统、用户或其他I/O响应而耗费大量的执行时间,此时使用多线程可以提高效率。
- 可拆分的大任务,大任务处理起来比较耗时,使用多线程并行加快处理(例如:分片上传,分布式计算)。
- 监听轮询重发任务。
- 其他。
2. 多线程同步实现
目前常用的能实现多线程同步的有:
- synchroized配合wait()、notify()、notifyAll()
- Lock类ReentrantLock重入锁、ReentrantReadWriteLock读写锁及结合condition
- 使用特殊域变量(volatile)实现线程同步
- 使用局部变量(ThreadLocal)实现线程同步等
2.1 synchronized
synchronized是java关键字,它可以作为函数的修饰符,也可作为函数内的语句,可以配合await(),notify()/notifyAll()使用。
Synchronized的作用主要有三个:
- 确保线程互斥的访问同步代码。
- 保证共享变量的修改能够及时可见。
- 有效解决重排序问题。
2.1.1 synchronized与Lock区别
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可中断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
2.2 Lock类
Lock接口的主要api:
- lock():获取锁,如果锁被暂用则一直等待
- unlock():释放锁
- tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
- tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
- lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
Lock锁分下面两种:
- 重入锁ReentrantLock
- 读写锁ReentrantReadWriteLock
2.2.1 重入锁ReentrantLock
可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。是可中断锁,通过lockInterruptibly()+interrupt()实现。
ReentrantLock也可以设置为公平锁,ReentrantLock lock = new ReentrantLock(true);true为公平锁,false非公平(默认)。
ReentrantLock的一些方法:
- isFair():判断锁是否是公平锁
- isLocked():判断锁是否被任何线程获取了
- isHeldByCurrentThread():判断锁是否被当前线程获取了
- hasQueuedThreads():判断是否有线程在等待该锁
代码示例:
private Lock lock = new ReentrantLock();
public void testMethod() {
try {
lock.lock();
doSomeThing();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
使用流程:
- 先new一个实例
private Lock lock = new ReentrantLock(); - 加锁
r.lock()或r.lockInterruptibly();
此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。(如果使用后者,必须throw interruptable exception 或catch) - 释放锁
r.unlock()
必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally是可以信任的:经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。否则可能造成死锁。
2.2.2 读写锁ReentrantReadWriteLock
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
用法类似重入锁ReentrantLock不多介绍。
两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码
2.2.3 Condition
Lock对象里面可以创建多个Condition(对象监视器)实例,线程对象可以注册在指定Condition中,从而有选择性的进行线程通知,在调度线程上更加灵活。
Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。
Condition相比Object的wait()notify()具有更细粒度,对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
代码示例:
await方法
public class MyService {
private Lock lock = new ReentrantLock();
private Condition condition=lock.newCondition();
public void testMethod() {
try {
lock.lock();
System.out.println("开始wait");
condition.await();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
}
通过创建Condition对象来使线程wait,必须先执行lock.lock方法获得锁
signal方法
public void signal()
{
try
{
lock.lock();
condition.signal();
}
finally
{
lock.unlock();
}
}
condition对象的signal方法可以唤醒wait线程
2.3 volatile实现线程同步
声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
当一个变量定义为 volatile 之后,将具备两种特性:
-
保证此变量对所有的线程的可见性,这里的“可见性”,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
-
禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
2.4 ThreadLocal实现线程同步
一般同步机制采用了“以时间换空间”的方式,提供一份变量,维护一个队列让不同的线程排队访问。线程间数据共享。
ThreadLocal采用了“以空间换时间”的方式。为每一个线程都提供了一份副本变量,因此可以同时访问而互不影响。线程间数据隔离。
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
优点:效率高。缺点:耗内存。
原理:维护一个以线程为key,变量为value的map。
api:
- T get():返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
- void remove():移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
- void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值(需要重写initialValue())。
代码示例:
ThreadLocal在hibernateUtil应用
private static ThreadLocal<Session> threadLocal = new ThreadLocal<Session>()
public static Session getCurrentSession(){
Session s=(Session) threadLocal.get();
//打开一个新的session,如果这个线程还不存在的话
if(s==null) {
s=sessionFactory.openSession();
threadLocal(s);
}
return s;
}
3.多线程锁
常见的锁:
- 公平锁/非公平锁
- 可重入锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
- 其他锁
这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计。
3.1 悲观锁/乐观锁:
注:悲观锁/乐观锁并不是锁的类型,而是指看待并发同步的角度。
悲观锁:阻塞同步,会上锁,synchorized关键词是一种悲观锁。Lock锁也是一种悲观锁。
乐观锁:非阻塞同步,不会上锁,利用CAS原理,在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。 Ps(cas设计缺陷可能会造成ABA问题)
3.2 公平锁/非公平锁:
公平锁:尽量以请求锁的顺序来获取锁。按顺序排队排最前的获取锁,即等的最久的获取锁。线程获取锁执行完后需要重新排队。
非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。synchorized为非公平锁,Lock默认为非公平锁,可设置为公平锁。非公平锁的优点在于吞吐量比公平锁大。
可中断锁/不可中断锁:
顾名思义不解释。
synchorized是一种不可中断锁,Lock可以通过lockInterruptibly()+interrupt()实现可中断
3.3 独享锁/共享锁:
独享锁指该锁一次只能被一个线程所持有,共享锁可以被多个线程持有。
ReentrantLock,synchorized是独享锁,ReadWriteLock中读锁为共享锁。共享锁可保证并发,效率高。
3.4 互斥锁/读写锁:
互斥锁/读写锁是对独享锁/共享锁的实现。
3.5 分段锁:
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
Ps(HashTable容器使用synchronized来保证线程安全,所有访问HashTable的线程都竞争同一把锁,高并发访问时效率远低于ConcurrentHashMap)
3.6 自旋锁:
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。