1. 线程安全和共享资源的概念
共享资源就是说该资源被多个线程所持有或者多个线程都可以访问该资源。线程安全问题是指当多个线程同时读写共享资源并且没有任何同步措施,导致出现脏数据或者其他不可预料结果的问题。
2. synchronized关键字
synchronized同步关键字,可以修饰方法和同步代码块(修饰代码块可以控制锁的粒度)。synchronized块是Java提供的一种原子性内置锁,Java的任意非NULL对象都可以把它当做一个同步锁来使用,这些java内置的使用者看不到的锁被称为内部锁,也叫监视器锁。
线程的代码块在进入synchronized块前自动获取内部锁,这个时候其他线程访问该同步代码块,该线程会被阻塞挂起,拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步代码块内部调用了内置锁资源的wait系列方法时会释放该内置锁。内置锁是排它锁,也就是当一个线程获得了这个锁后,其余线程只有等该锁释放才能获取该锁。synchronized的使用会导致上下文的切换,非常耗时。
synchronize保证了内存可见性,和操作的原子性。
synchronize的核心组件:
-
Wait Set:调用wait方法的被阻塞的线程放置在这里。
-
Contention List:竞争队列,所有竞争(请求)锁的线程首先放入该位置。
-
Entry List:Contention List中有资格成为候选资源的线程移动到该位置。
-
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程称为OnDeck。
-
Owner:当前已经获取锁资源的线程称之为Owner。
-
!Owner:当前释放锁的线程。
synchronize的实现原理:
-
JVM每次从任务队列的尾部取出一个线程用于锁竞争候选者(OnDeck),但是在并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争, JVM将部分线程移动到EntryList中作为候选竞争线程。
-
Owner线程会在unlock时,将ContentionList中部分线程移动到EntryList,并指定EntryList一个线程为OnDeck线程(一般为最先进入那个线程)。
-
Owner线程并不直接把锁传递给OnDeck线程,而是把锁的竞争权利交给OnDeck,OnDeck需要重新竞争锁(跟谁竞争?未进入ContentionList的自旋线程 6)。这样虽然牺牲了公平性,但是提高了系统吞吐量。JVM中把这种选择行为成为“竞争切换”。
-
OnDeck线程获取锁资源后变为Owner线程,其他线程仍然停留在EntryList中,如果Owner线程被wait阻塞,则转移到WaitSet,并释放锁,直到某个时刻通过notify方法唤醒,会重新回到EntryList中。
-
处于ContentionList、EntryList、WaitSet的线程都是阻塞状态,该阻塞是由操作系统完成的。
-
synchronize是非公平锁,当线程进入ContentionList之前,该线程会先尝试自旋获取锁,如果获取不到进入ContentionList,这对于已经入队列中的线程来说是不公平的,同时自旋获取锁的那个线程还可能直接抢占OnDeck的锁资源。
-
每个对象都有监视器monitor对象,加锁就是在竞争monitor对象。编译后,代码块加锁是在代码块前后加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标志位判断的。
-
synchronize是重量级锁,需要调用操作系统相关接口(线程状态的切换),性能较低,有可能加锁的时间超过有用的执行时间。
-
Java1.6对synchronize进行了许多优化,引入了适应性自旋锁、锁消除、锁粗化、轻量级锁和偏向锁等,效率有了本质的提高。之后的1.7,1.8均对synchronize关键字的实现机制进行了优化,轻量级锁和偏向锁都是在对象头中有标记位,不需要经过操作系统加锁。
-
JDK1.6默认开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
-
3. ReentrantLock 类
ReentrantLock 继承接口Lock并实现了其中方法,它是一种可重入锁,除了能完成synchronize的所有功能,还提供了诸多如可响应中断锁,可轮询锁请求,定时锁等避免死锁的方法。
- ReentrantLock 的主要方法:
- void lock():执行此方法时,如果锁处于空闲状态,则当前线程获取该锁,如果锁已经被持有,将禁用当前线程,直到当前线程获取到该锁。
- boolean tryLock():如果锁可用,获取锁,返回true,否则返回false。tryLock()是尝试获取锁,如果锁不可用,不会禁用当前线程,当前线程继续往下执行,而lock()则是一定要获取锁,如果锁不可用,则当前线程一直等待直到获取锁。
- void unlock():当前线程释放持有锁,锁只有持有者才能释放,如果线程不持有锁,执行该方法,可能抛出异常。
- Condition newContion():条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,调用后,当前线程释放锁。
- int getHoldCount():获取当前线程持有锁的次数,也就是执行lock()方法的次数。
- int getQueueLength():返回正在等待获取锁的线程个数,如果启动10个线程,1线程一个获取锁就返回9。
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁。
- hasQueuedThreads():是否有线程等待此锁。
- isFair():该锁是否公平锁。 ......
- ReentrantLock的实现:
public class MyReentrantLock {
private Lock lock = new ReentrantLock();
//Lock lock=new ReentrantLock(true);//公平锁
//Lock lock=new ReentrantLock(false);//非公平锁
private Condition condition = lock.newCondition();//创建 Condition
public void testMethod() {
try {
lock.lock();//lock 加锁
// 1:wait 方法等待:
// System.out.println("开始 wait");
condition.await();
//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
// 2:signal 方法唤醒
condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
- Condition类和Object类锁方法的联系:
- Condition类的await方法和Object中的wait方法等效。
- signal方法和notify方法等效。同理:signalAll和notifyAll
- ReentrantLock 类可以唤醒指定线程,Object类的唤醒是随机的。
- tryLock和Lock和lockInterruptibly的区别:
- tryLock获得锁就返回true,不能立即返回false,tryLock(long timeout,TimeUnitunit)可以增加时间限制,如果超过该时间还没有获得锁,则返回false。
- lock获得锁就返回true,没有就一直等待获得锁。
- Lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,lockInterruptibly会抛出异常。
4.Semaphore信号量
Semaphore是一种基于计数的信号量。可以设定阈值,多个线程竞争获取许可信号,做完自己的任务后归还信号,如果超过阈值,线程申请许可信号会被阻塞。Semaphore可以用来构建对象池,资源池之类的,如数据库连接池等。
-
实现资源互斥(互斥锁)
可以创建技术为1的Semaphore,是一种类似于互斥锁的机制,也叫做二元信号量,表示两种互斥状态。
public void testSemaphore() { // 创建一个计数阈值为 5 的信号量对象 // 只能 5 个线程同时访问 Semaphore semp = new Semaphore(5); try { // 申请许可 semp.acquire(); try { // 业务逻辑 } catch (Exception e) { } finally { // 释放许可 semp.release(); } } catch (InterruptedException e) { } } -
Semaphore和ReentrantLock 的区别:
- Semaphore基本能完成ReentrantLock所有工作,使用方法类似,通过acquire()和release()方法来获得和释放资源。Semaphore.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一样,在等待资源的过程中可以被Thread.interrupt()方法中断。
- Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名不同,使用跟ReentrantLock 一致。
- Semaphore也提供了公平锁和非公平锁的机制,在构造方法中设置。
- Semaphore的锁释放也是需要手动释放,为避免线程抛出异常而无法释放锁,释放锁在finally中完成。
5.原子类
在多线程中,i++等运算都不是原子性的操作,是线程不安全的。通常会使用synchronize将其设置为原子操作,同时JVM为我们提供了原子操作类,使用方便并且效率更高。例如:AtomicInteger(Integer类),数据类型对应的都有一个原子操作类,同时还有AtomicReference类,将一个对象的所有操作转换为原子操作。
6. 锁的概述
-
悲观锁:
悲观锁指对数据被外界修改保持保守状态,认为数据很容易被其他线程修改(读少写多),所以处理数据前先对数据加锁,整个处理过程中,数据处于锁定状态。Java中的悲观锁就是synchronize在AQS框架下的锁,先尝试CAS乐观锁,获取不到转为悲观锁。例:ReentrantLock。
-
乐观锁:
乐观锁是相对悲观锁来说的, 它认为一般情况下数据是不会被修改的(读多写少),所以在访问前不会加锁,而进行数据更新时才会对数据是否冲突进行检测(比较冲突数据的版本号)。
-
公平锁:
公平锁表示线程获取锁的顺序是按照线程请求顺序确定的。
ReentrantLock pairLock = new ReentrantLock(true);
-
非公平锁:
线程获取锁的顺序通过抢占确定,按照随机、就近原则分配锁。性能远远超过公平锁。
ReentrantLock pairLock = new ReentrantLock(false);
-
独占锁:
独占锁保证任何时候都只有一个线程得到锁,是一种悲观锁。
-
共享锁:
共享锁是乐观锁,放宽了加锁的条件,允许多个线程同时进行读操作。
-
可重入锁:
当一个线程获取别其他线程持有的独占锁时,该线程会被阻塞,那么当线程再次获取它之前获取的锁时会不会被阻塞?如果不被阻塞,说明锁是可重入的。synchronized 内部锁是可重入锁 ,可重入锁原理是在锁的内部维护了一个线程标示,用来标示该锁目前是被那个线程占用,然后关联一个计数器。
-
自旋锁:
当一个线程获取独占锁失败后,会被切换到内核状态而被挂起,当线程获取到锁后又需要将切换内核状态而唤醒该线程,这样内存开销较大,影响并发性能。
自旋锁则是,当线程在获取锁时,如果该锁已经被其他线程占有,它不会别立即阻塞,在不放弃CPU使用权的情况下,多次尝试获取该锁(默认10次,可以使用-XX :PreBlockSpinsh参数设置),很有可能在后续几次尝试中其他线程释放了该锁。如果尝试的指定次数内没有释放锁则当前线程才会被挂起。JDK1.6引入了自适应自旋锁。
自旋锁是使用CPU时间来换取线程阻塞与调度的开销,但是可能这些CPU时间会白白浪费。
-
读写锁(ReadWriteLock):
为了提高程序性能Java提供了读写锁,在没有写的情况下,线程读操作是无阻塞的,在一定程度上提高了程序运行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,需要灵活运用。
7.锁的状态
锁的状态分为四种:无锁状态、偏向锁、轻量级锁、重量级锁。
锁升级:随着锁的竞争,锁可以从偏向锁升级为轻量级锁再升级为重量级锁(单向的)。
-
偏向锁
Hotspot作者经过研究发现大量情况下锁不仅不存在多线程竞争,而且往往是同一个线程获取该锁。偏向锁的目的是在某个线程获取锁之后,消除这个线程锁重入(CAS)的开销,看起来是让这个线程得到偏袒。在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径,轻量级锁的获取和释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS指令。偏向锁适用的场景是单个线程执行同步代码块。
-
轻量级锁
“轻量级”是相对于使用操作系统互斥量实现的传统锁而言的,轻量级锁不是用来代替重量级锁的,它的本意是在线程没有锁竞争的情况下,减少传统的重量级锁对性能带来的性能消耗。轻量级锁所适应的场景是多个线程交替执行同步代码块的情况下,如果同一时间访问同一个锁,那么轻量级锁就会膨胀为重量级锁。
-
重量级锁
依赖操作系统Mutex Lock所实现的锁称为重量级锁。synchronize是通过对象内部的监视器锁实现的,监视器锁本质又是依赖底层操作系统的Mutex Lock实现的。而操作系统实现线程之间的切换需要从用户状态转换到核心状态,状态之间的转换需要很长的时间,成本较高,所以synchronize性能较低。
8. volatile关键字
volatile确保对一个变量的更新对其他线程马上可见,当一个变量被声明为volatile时,线程在写入该变量时不会把值缓存到寄存器或者其他地方,而是把值刷新回主内存,当其他线程读取该共享变量,会直接从主内存重新获取最新值,而不是当前线程工作内存中的值。
volatile保证了内存可见性,避免重排,但是不保证操作的原子性(如果变量本身的操作就是原子的另当别论,例赋值操作)i++,不能替换synchronize。
9.如何在多线程间共享数据
Java里面多线程共享数据主要是通过共享内存的方式,共享内存需要:可见性、有序性和原子性。JVM解决了可见性和有序性,锁解决了原子性问题,理想情况下我们希望做到“同步”和“互斥”,有以下方法:
- 将数据抽象成一个类,将数据的操作作为类的方法,方法上加锁。
- Runnable对象作为一个类的内部类,共享数据作为这个类的共享变量,每个线程对共享数据的操作封装在外部类中,以便实现对数据的各个操作实现同步和互斥。
10.ThreadLocal作用(线程本地存储)
ThreadLocal叫做线程本地变量(本地存储),它提供了线程内部的局部变量,这种变量只在线程生命周期起作用,减少同一个线程内多个函数或组件之间公共变量的传递的复杂度。
-
ThreadLocalMap是ThreadLocal的一个内部类,Thread维护了ThreadLocalMap。
-
set方法:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } -
适用场景:数据库连接、Session管理等。
private static final ThreadLocal threadSession = new ThreadLocal(); public static Session getSession() throws InfrastructureException { Session s = (Session) threadSession.get(); try { if (s == null) { s = getSessionFactory().openSession(); threadSession.set(s); } } catch (HibernateException ex) { throw new InfrastructureException(ex); } return s; }