300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > 并发编程之美(1)并发编程基础二

并发编程之美(1)并发编程基础二

时间:2021-12-31 19:58:16

相关推荐

并发编程之美(1)并发编程基础二

1…9线程死锁

1.9.1什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

线程A 己经持有了资源2 , 它同时还想申请资源l , 线程B 已经持有了资源l ,它同时还想申请资源2 , 所以线程l 和线程2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

为什么会产生死锁呢?用原书(并发编程之美)中的解释

死锁的产生必须具备以下四个条件。

互斥条件:指线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。

请求并持有条件:指一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。

不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。

环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合{T0 , T1 T2 ,…, Tn }中的T0 正在等待一个Tl 占用的资源, T1正在等待T2 占用的资源,……Tn 正在等待己被T0 占用的资源。

死锁例子

public class DedLock {//创建资源private static Object resourceA = new Object();private static Object resourceB = new Object();public static void main(String[] args) {new Thread(()->{//先给我们的资源A加锁synchronized (resourceA){System.out.println(Thread.currentThread()+"获得资源A");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread()+"等待获取资源B");//在对资源B加锁synchronized (resourceB){System.out.println(Thread.currentThread()+"获得资源B");}}}).start();new Thread(()->{//先给我们的资源A加锁synchronized (resourceB){System.out.println(Thread.currentThread()+"获得资源B");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread()+"等待获取资源A");//在对资源B加锁synchronized (resourceA){System.out.println(Thread.currentThread()+"获得资源B");}}}).start();}}

结果Thread[Thread-0,5,main]获得资源AThread[Thread-1,5,main]获得资源BThread[Thread-1,5,main]等待获取资源AThread[Thread-0,5,main]等待获取资源B

这就是典型的死锁例子,我们先锁了资源A,在资源A中又对资源B加锁,但是因为我们线程A睡了1s,期间我们的线程B已经对资源B先加锁了,线程B先对资源B加锁但是又想获取资源A,就导致两者互相争抢,又不释放自己的锁,导致死锁

他们满足上面我们说的死锁产生具备的条件么?

1.resourc eA 和re sourc eB 都是互斥资源,当线程A 调synchronized(resource A)方法获取到resourceA 上的监视器锁并释放前, 线程B 再调用synchronized(resourceA) 方法尝试获取该资源会被阻塞,只有线程A 主动释放该锁, 线程B 才能获得, 这满足了资源互斥条件

2.线程A 首先通过synchronized(resourceA) 方法获取到resourceA 上的监视器锁资源,然后通过synchronized(resourceB) 方法等待获取resourceB 上的监视器锁资源, 这就构成了请求并持有条件。

也就是请求B但是我还持有A

3.构成了资源的不可剥夺条件,就是我们线程A只要不是自己主动释放资源A的监视器锁,那么其他线程(线程B)是不会掠夺走的

4.环路等待条件就是,线程A锁了资源A但是请求资源B,但是线程B锁了资源B,又去请求资源A,形成闭合

那么如何避免线程死锁呢?

1.9.2如何避免线程死锁

字需要破坏形成死锁的一个必要条件即可。

目前我们只有请求并持有环路等待条件是可以被破坏的

造成死锁的原因其实和申请资源的顺序有很大关系, 使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程B 的代码进行如下修改

new Thread(()->{//先给我们的资源A加锁synchronized (resourceA){System.out.println(Thread.currentThread()+"获得资源B");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread()+"等待获取资源A");//在对资源B加锁synchronized (resourceB){System.out.println(Thread.currentThread()+"获得资源A");}}}).start();

结果

Thread[Thread-0,5,main]获得资源AThread[Thread-0,5,main]等待获取资源BThread[Thread-0,5,main]获得资源BThread[Thread-1,5,main]获得资源BThread[Thread-1,5,main]等待获取资源AThread[Thread-1,5,main]获得资源A

也就是说资源B没有被加锁,虽然两个线程都对资源A加锁了,但是某个线程先执行,好后获得B之后就是放了资源A,另一个线程能拿到了

这就是资源的有序分配,资源的有序性破坏了资源的请求并持有条件和环路等待条件, 因此避免了死锁。

1.10守护线程与用户线程

Java 中的线程分为两类,分别为daemon 线程(守护线程〉和user 线程(用户线程)。

在JVM启动的时候会调用main函数,main函数所在的线程就是一个用户线程

当然JVM内部同时还启动了好多守护线程,比如垃圾回收线程

关于垃圾回收线程后续会在JVM的笔记中详细提到

如何创建守护线程

public class daemon {public static void main(String[] args) {Thread thread = new Thread(()->{});//设置为守护线程thread.setDaemon(true);thread.start();}}

用户线程与守护线程的区别

区别之一是当最后一个非守护线程结束时, NM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响NM 的退出。言外之意,只要有一个用户线程还没结束, 正常情况下NM 就不会退出。

public class daemon {public static void main(String[] args) {Thread thread = new Thread(()->{for (;;){}});//启动子线程thread.start();System.out.println("主线程完毕");}}结果是主线程完毕

如上代码在main 线程中创建了一个thread 线程,在thread 线程里面是一个无限循环。从运行代码的结果看, main 线程已经运行结束了,那么JVM进程己经退出了吗?在IDE的输出结果右上侧的红色方块说明,JVM 进程并没有退出。另外,我们亦可以通过jps来查看

这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM 进程并不会终止。那么我们把上面的thread 线程设置为守护线程后,再来运行看看会有什么结果:

public class daemon {public static void main(String[] args) {Thread thread = new Thread(()->{for (;;){}});//设置为守护线程thread.setDaemon(true);//启动子线程thread.start();System.out.println("主线程完毕");}}结果是主线程完毕进程已结束,退出代码 0

在启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM进程己经终止了,执行***ps -eaf |grep java*** 也看不到JVM 进程了。在这个例子中, main 函数是唯一的用户线程, thread 线程是守护线程,当main 线程运行结束后, JVM 发现当前己经没有用户线程了,

就会终止JVM 进程。

由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM进程。

main 线程运行结束后, JVM会自动启动一个叫作DestroyJava VM 的线程, 该线程会等待所有用户线程结束后终止JVM进程。下面通过简单的JVM代码来证明这个结论

int JNICALLJavaMain(void * args)//执行Java 中的ma 工n函数(*env) - >CallStaticVoidMethod(env , mainClass, mainID, mainArgs) ;//main 函数返回值ret = (*env)->ExceptIonOccurred(env) == NULL ? 0: 1 ;//等待所有非守护线程结束, 然后销毁♂月4进程LEAVE();}

LEAVE 是C 语言里面的一个宏定义,具体定义如下。

#define LEAVE () \do {\if ( (*vm) >DetachCurrentThread(vm) 1= JNI_OK ) { \JLI_ReportErrorMessage(JVM_ERROR2) ; \ret = l ; \}\if (JNI_TRUE) { \( *vm)->DestroyJavaVM (vm); \return ret; \}\} while_(JNI FALSE)

该宏的作用是创建一个名为DestroyJava VM 的线程,来等待所有用户线程结束

在Tomcat 的NIO 实现NioEndpoint 中会开启一组接受线程来接受用户的连接请求,以及一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?

下面我们看一下NioEndpoint 的startlntemal 方法。

public void startInternal() throws Exception{if(!running){running = true;paused = false;...//创建处理线程pollers = new Poller[get PollerThreadCount () ] ;for (int i=O ; i<pollers . length; i++) {pollers [i] = new Poller () ;Thread pollerThread =new Thread (pollers [i],getName () +"- Client Poller-"+ i ) ;pollerThread.setPriority (threadPriority ) ;pollerThread.setDaemon(true );//声明为守护线程pollerThread.start() ;}}protected final void startAcceptorThreads() {int count= getAcceptorThre adCount();acceptors= new Acceptor[count];for (int i = O;i<count; i ++) {acceptors [i] = createAcceptor () ;String threadName = getName ()+"- Acceptor-" +i;acceptors[i].setThreadName(threadName) ;Thread t =new Thread(acceptors[i],threadName) ;t.setPriority(getAcceptorThreadPriority()) ;t.setDaemon(getDaemon());//设置是否为守护线程,默认为守护线程t . start() ;}}private boolean daemon = true;public void setDaemon(boolean b) { daemon= b ; }public boolean getDaemon () { return daemon ; }

在如上代码中,在默认情况下, 接受线程和处理线程都是守护线程, 这意味着当tomcat 收到shutdown 命令后并且没有其他用户线程存在的情况下tomcat 进程会马上消亡,而不会等待处理线程处理完当前的请求。

总结

如果你希望在主线程结束后JVM 进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

1.11 ThreadLocal

多钱程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如下图所示

我们的同步方式一般的都是用加锁的方式来实现同步,那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?====>ThreadLocal

什么是ThreadLocal

ThreadLocal 是JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如下图所示。

1.11.1 ThreadLocal使用示例

public class ThreadLocalTest {static void print(String str){//1.1打印当前线程本地内存中localVariable变量的值System.out.println(str+":"+localVairable.get());//1.2清除当前线程池本地内存中的localVairable变量//localVairable.remove();}//2.创建ThreadLocal变量static ThreadLocal<String> localVairable = new ThreadLocal<>();public static void main(String[] args) {new Thread(()->{//3.1设置线程A中本地变量localVairable的值localVairable.set("线程A的localVairable");//3.2调用打印函数print("线程A");//3.3打印本地变量的值System.out.println("线程A的localVairable清除后:"+localVairable.get());}).start();new Thread(()->{//3.1设置线程A中本地变量localVairable的值localVairable.set("线程B的localVairable");//3.2调用打印函数print("线程B");//3.3打印本地变量的值System.out.println("线程B的localVairable清除后:"+localVairable.get());}).start();}}

结果是:线程A:线程A的localVairable线程B:线程B的localVairable线程A的localVairable清除后:线程A的localVairable线程B的localVairable清除后:线程B的localVairable

我们使用了set设置了localVariable 的值,这其实是设置A线程本地内存的一个副本,这个副本B是访问不了的。

上面的例子是我们没有执行清除本地内存副本的操作,我们放开注释执行,结果变为

线程A:线程A的localVairable线程B:线程B的localVairable线程A的localVairable清除后:null线程B的localVairable清除后:null

1.11.2 ThreadLocal的实现原理

相关类图

书中说道

Thread 类中有一个threadLocals和一个inheritableThreadLocals, 它们都是ThreadLocalMap 类型的变量, 而ThreadLocalMap 是一个定制化的Hashmap 。在默认情

况下, 每个线程中的这两个变量都为null,源码中就有体现

protected T initialValue() {return null;}

只有当前线程第一次调用ThreadLocal 的set 或者get 方法时才会创建它们。

其实每个线程的本地变量不是存放在ThreadLocal 实例里面,

而是存放在调用线程的threadLocals 变量里面。也就是说, **ThreadLocal 类型的本地变量存放在具体的线程内存空间中。**ThreadLocal 就是一个工具壳,

它通过set 方法把value 值放入调用线程的threadLocals 里面并存放起来, 当调用线程调用它的get 方法时,再从当前线程的threadLocals 变量里面将其拿出来使用。如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal 变量的remove 方法,从当前线程的threadLocals 里面删除该本地变量。

另外, Thread 里面的threadLocals 为何被设计为map 结构?很明显是因为每个线程可以关联多个ThreadLocal 变量。

1 void set(T value)

public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//将当前线程作为key,去查找对应的线程变量ThreadLocalMap map = getMap(t);//如果存在这个线程对应的变量if (map != null)map.set(this, value);else//如果对应的线程变量不存在,就新建,将key,value保存createMap(t, value);}

getMap(Thread t)的源码如下。

ThreadLocalMap getMap(Thread t) {//传进来一个线程,返回这个线程的变量threadLocalsreturn t.threadLocals;}

可以看到, getMap(t)的作用是获取线程自己的变量threadLocal s, threadlocal 变量被绑定到了线程的成员变量上

如果getMap(t)的返回值不为空,则把value 值设置到threadLocals 中,也就是把当前变量值放入当前线程的内存变量threadLocals 中

threadLocals 是一个HashMap 结构, 其中key 就是当前ThreadLocal 的实例对象引用, value 是通过set 方法传递的值。

如果getMap(t)返回空值则说明是第一次调用set 方法,这时创建当前线程的threadLocals 变量。下面来看**createMap(t, value)**做什么。

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

也就是说当前线程没有threadLocals这个变量时,她就new一个

ThreadLocalMap线程作为键,设置的内容为值

T get()

public T get() {//获取当前线程Thread t = Thread.currentThread();//查找当前线程存不存在对应的threadLocals变量ThreadLocalMap map = getMap(t);//如果threadLocals变量不为null,则返回对应本地变量的值if (map != null) {//ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {//取出我们的值 @SuppressWarnings正压警告注解@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果没有threadLocals这个变量,则初始化一个return setInitialValue();}

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}

private T setInitialValue() {//初始化为nullT value = initialValue();//获得当前线程Thread t = Thread.currentThread();//查找当前线程存不存在对应的threadLocals变量ThreadLocalMap map = getMap(t);if (map != null)//有的话值设置为nullmap.set(this, value);else//没有的话创建,createMap(t, value);return value;}

如果当前线程的threadLocal s 变量不为空, 则设置当前线程的本地变量值为null , 否则调用createMap 方法创建当前线程的createMap 变量。

3.void remove()

public void remove() {//查询该线程有没有绑定的threadLocals变量ThreadLocalMap m = getMap(Thread.currentThread());//如果有,移除if (m != null)m.remove(this);}

总结

​ 在每个线程内部都有一个名为threadLocals 的成员变量, 该变

量的类型为Hash Map , 其中key 为我们定义的ThreadLocal 变量的this 引用, value 则为我们使用set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals 中,如果当前线程一直不消亡, 那么这些本地变量会一直存在,所以可能会造成内存溢出, 因此使用完毕后要记得调用ThreadLocal 的remove 方法删除对应线程的threadLocals 中的本地变量。在高级篇要讲解的只JC 包里面的ThreadLocalRandom , 就是借鉴ThreadLocal 的思想实现的, 后面会具体讲解。

1.11.3 ThreadLocal不支持继承性

首先看一个例子

public class TestThreadLocal {//创建线程变量public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();public static void main(String[] args) {//2.设置线程变量threadLocal.set("你好,world");//3.启动子线程new Thread(()->{System.out.println("子线程:"+threadLocal.get());}).start();//4.输出主线成的变量值System.out.println("main:"+threadLocal.get());}}

结果是:

main:你好,world子线程:null

也就是说,同一个ThreadLocal 变量在父线程中被设置值后, 在子线程中是获取不到的。根据上节的介绍,这应该是正常现象,因为在子线程thread 里面调用get 方法时当前线程为thread 线程,而这里调用s et 方法设置线程变量的是main 线程,两者是不同的线程,自

然子线程访问时返回null 。

那如果我们把set放到子线程的函数体里面,结果就会是

main:null子线程:你好,world

那么有没有办法让子线程能访问到父线程中的值? 答案是有。

1.11.4 lnheritableThreadLocal 类

InheritableThreadLocal继承自ThreadLocal , 其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

下面看一下InheritableThreadLocal 的代码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {//1protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}}

Inheritab I e ThreadLocal 继承了ThreadLocal ,并重写了三个方法

InheritableThreadLocal 重写了createMap 方法, 那么现在当第一次调用set 方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是threadLocals 。

当调用get 方法获取当前线程内部的map 变量时, 获取的是inheritableThreadLocals 而不再是threadLocals 。

也就是说我们用了lnheritableThreadLocal 类的话,threadLocals 变成了inheritableThreadLocals

childValue

下面我们看一下重写的代码( 1 )何时执行, 以及如何让子线程可以访问父线程的本地变量。这要从创建Thread 的代码说起,打开Thread 类的默认构造函数,代码如下。

public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);}

private void init (ThreadGroup g , Runnable target , String name,long stacksize , AccessControlContext ace) {//(4 )获取当前线程 这里获取的是我们的main线程Thread parent = currentThread( );//(5 )如采父线程的inheritableThreadLocals变量不为nullif (parent.inheritableThreadLocals != null )//(6 )设置子线程中的inheritableThreadLocals变量this.inheritableThreadLocals =ThreadLocal.createinheritedMap(parent.inheritableThreadLocals);this .stackSize = stackSize;tid = nextThreadID() ;}

代码在创建线程时,在构造函数里面会调用in it 方法。代码( 4 )获取了当前线程(这里是指main 函数所在的线程,也就是父线程〉。

然后代码( 5 )判断main 函数所在线程里面的inheritableThreadLocals 属性是否为null 。

前面我们讲了InheritableThreadLocal 类的get 和set 方法操作的是inheritableThreadLocals ,所以这里的inheritableThreadLocal 变量不为null ,因此会执行代码( 6 )。下面看一下createlnh eritedMap 的代码

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}

可以看到,在createlnheri tedMap 内部使用父线程的inheritableThreadLocals 变量作为构造函数创建了一个新的ThreadLocalMap 变量, 然后赋值给了子线程的inheritableThreadLocals 变量

下面我们看看在ThreadLocalMap 的构造函数内部都做了什么事情

private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {//调用重写的方法Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}

在该构造函数内部把父线程的inheritabl eThreadLoca l s 成员变量的值复制到新的ThreadLoca!Map 对象中,其中l代码( 7 )调用了Inheri tab leThreadLocal 类重写的代码( 1 ) 。

总结

InheritableThreadLocal 类通过重写代码。〉和( 3 ) 让本地变量保存到了具体线程的inheritableThreadLocal s 变量里面,那么线程在通过InheritableThreadLocal 类实例的set 或者get 方法设置变量时,就会创建当前线程的inheritableThreadLocals 变量。当父

线程创建子线程时,构造函数会把父线程inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals 变量里面。

把1.11.3 节中的代码( 1 )修改为

public class TestThreadLocal {//创建线程变量public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {//2.设置线程变量threadLocal.set("你好,world");//3.启动子线程new Thread(()->{System.out.println("子线程:"+threadLocal.get());}).start();//4.输出主线成的变量值System.out.println("main:"+threadLocal.get());}}

结果为main:你好,world子线程:你好,world

那么在什么情况下需要子线程可以获取父线程的threadlocal 变量呢?

情况还是蛮多的,比如子线程需要使用存放在threadlocal 变量中的用户登录信息,再比如一些中间件需要把统一的id 追踪的整个调用链路记录下来。

其实子线程使用父线程中的threadlocal 方法有多种方式, 比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritabI e ThreadLocal 就显得比较有用。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。