锁
一、概念
在计算机科学中,锁(lock)与互斥(metex)是一种同步机制,用于在许多线程执行时对资源的限制。
死锁
一、概念
是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁
二、产生死锁的必要条件
1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放;
2、请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
3、不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
4、环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源;
总结一下:死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁,只有B一个去,不要2个,打十个都没问题;单资源呢?只有13,A和B也只会产生激烈竞争,打得不可开交,谁抢到就是谁的,但不会产生死锁。同时,死锁还有几个要求,①、争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁;②、争夺者拿到资源不放手。
三、解决死锁方式
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能的避免、预防和接触死锁;
解决死锁关键是保证拿锁的顺序一直,有两种解决办法:
1、内部通过顺序比较,确定拿锁的顺序;
2、采用尝试拿锁的机制;
其中常见的避免死锁的算法有有序资源分配法、银行家算法;
活锁
一、概念
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程
二、产生活锁的例子
public class TryLock {
private static Lock lc13 = new ReentrantLock();//第一个锁
private static Lock lc14 = new ReentrantLock();//第二个锁
//先尝试拿lc13 锁,再尝试拿lc14锁,lc14锁没拿到,连同lc13 锁一起释放掉
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while(true){
if(lc13.tryLock()){
System.out.println(threadName +" get 13");
try{
if(lc14.tryLock()){
try{
System.out.println(threadName +" get 14");
System.out.println("fisrtToSecond do work------------");
break;
} finally {
lc14.unlock();
}
}
} finally {
lc13.unlock();
}
}
// 标注①
// Thread.sleep(r.nextInt(3));
}
}
//先尝试拿lc14锁,再尝试拿lc13锁,lc13锁没拿到,连同lc14锁一起释放掉
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while(true){
if(lc14.tryLock()){
System.out.println(threadName +" get 14");
try{
if(lc13.tryLock()){
try{
System.out.println(threadName +" get 13");
System.out.println("SecondToFisrt do work------------");
break;
} finally {
lc13.unlock();
}
}
} finally {
lc14.unlock();
}
}
// 标注②
// Thread.sleep(r.nextInt(3));
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
public void run(){
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述例子中,标注①和标注②注释掉的话,两个线程间就会产生活锁的现象;
三、解决活锁的办法
如二例子中,每个线程休眠随机数,错开拿锁的时间;
ThreadLocal
一、概念
ThreadLocal是一个关于创建线程局部变量的类。 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
二、与synchonized的比较
ThreadLocal和synchonized都用于解决多线程并发访问。可是ThreadLocal与synchonized有本质的差别。synchronized是利用锁的机制,使变量和代码块在某一时刻仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一事件访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
三、使用
1、查看JDK中ThreadLocal类源码很简单,只有4个方法;
public T get()
该方法返回当前线程锁对应的线程局部变量。
public void set(T value)
设置当前线程的线程局部变量的值。
public void remove()
将当前线程局部变量的值删除,目的使为了减少内存的占用,该方法时JDK1.5新增的方法。需要指出的时,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显示调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected T initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用get()或set(T)时才执行,并且仅执行一次。ThreadLocal中的缺省实现直接返回一个null。
2、public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么线程都能够并发访问这个变量,对它进行写入、读取操作,都是线程安全。
四、实现解析
1、数据结构
2、源码解析
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*
* 返回当前线程的this副本中的线程局部变量的值。如果当前线程的变量没有值,
* 它首先通过调用initialValue方法作为初始化返回的值
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的成员变量ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从ThreadLocalMap中获取当前ThreadLocal为key的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到Entry了
if (e != null) {
// Entry的value就是我们需要的了
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 返回initialValue的初始值
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
// 从这里可以看出每个线程有自己的ThreadLocalMap变量,看下图,果然如此
return t.threadLocals;
}
从上面可知,我们的ThreadLocal的线程变量都是放到ThreadLocalMap中,现在对ThreadLocalMap来一探究竟,不多说,上源码
ThreadLocalMap是ThreadLocal的静态内部类,然后Thread类中有一个这个类型的成员变量,所以getMap是直接返回Thread的成员变量。
ThreadLocalMap有个Entry类型的数组,这个Entry也就是我们从上述源码获取到的e,Entry是ThreadLocalMap的静态内部类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型的key,一个是Object类型的value。map.getEntry(this)则是获取某个ThreadLocal对应的值,set方法就是更新或赋值响应的ThreadLocal对应的值。
3、回顾
每个Thread对象都有一个自己的ThreadLocalMap成员变量,当线程对一个ThreadLocal对象进行操作时,操作的都是该线程对象里的ThreadLocalMap对象,对ThreadLocal对象进行get或set操作时,首先会通过Thread.currentThread()方法拿到当前线程对象,然后获取当前线程对象的ThreadLocalMap成员变量,当前,如果Map为空时,还会先进行map的创建、初始化等工作。ThreadLocalMap里有个Entry数组,Entry会保存以该ThreadLocal对象为key,设置的值为value的信息,通过每次获取对应ThreadLocal对象为key的Entry进行对线程局部变量的修改,从而达到线程间变量副本的并发安全。
至于ThreadLocal的数据结构算法,我们后面会在数据结构与算法专栏HashMap文章中进行讲解
五、ThreadLocal的内存泄露
每个Thread里面都有一个ThreadLocalMap,而ThreadLocalMap中真正承载数据的是一个Entry数组,Entry的Key是ThreadLocal对象的弱引用。
当我们不需要ThreadLocal时,GC会将ThradLocal变量置为null,没有任何强引用指向队中的ThreadLocal对象时,堆中的ThreadLocal将会被GC回收,使用弱引用对于ThreadLocal对象来说是不会发生内存泄露的。但是如果使用不当,比如当ThreadLocal使用完后,将栈中的ThreadLocal变量置为null,ThreadLocal对象会被回收,可是当前线程还在运行的话,key为null的value对象并不会被回收,因为这里还有一个线程对象的成员变量ThreadLocalMap在对Entry持有强引用,所以发生了内存泄露。
解决内存泄露
ThreadLocal也有考虑到该内存泄露问题,在set、get、remove的时候都有清除此线程的垃圾数据,比如Entry数组中key为null的value
*** 所以,我们在当前线程使用完ThreadLocal后,调用ThreadLocal的remove方法进行清除来达到降低内存泄露的风险;***
synchonized
一、概念
Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
二、字节码看根本
通过javap编译字节码后,如下图