爱站网,网站注册便宜,搜索推广代运营,无锡宜兴网站建设什么是线程
在讨论什么是线程前有必要先说下什么是进程#xff0c;因为线程是进程中的一个实体#xff0c;线程本身是不会独立存在的。
进程是代码在数据集合上的一次运行活动#xff0c;是系统进行资源分配和调度的基本单位#xff0c;线程则是进程的一个执行路径#…什么是线程
在讨论什么是线程前有必要先说下什么是进程因为线程是进程中的一个实体线程本身是不会独立存在的。
进程是代码在数据集合上的一次运行活动是系统进行资源分配和调度的基本单位线程则是进程的一个执行路径一个进程中至少有一个线程进程中的多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的但是CPU资源比较特殊它是被分配到线程的因为真正要占用CPU运行的是线程所以也说线程是CPU分配的基本单位。
在Java中当我们启动main函数时其实就启动了一个JVM的进程而main函数所在的线程就是这个进程中的一个线程也称主线程。 个进程中有多个线程多个线程共享进程的堆和方法区资源但是每个线程有自己的程序计数器和栈区域。
程序计数器是一块内存区域用来记录线程当前要执行的指令地址。
那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU执行的基本单位而CPU一般是使用时间片轮转方式让线程轮询占用的所以当前线程CPU时间片用完后要让出CPU,等下次轮到自己的时候再执行。
那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU时的执行地址的待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。
另外需要注意的是如果执行的是native方法那么 pc计数器记录的是undefined地址只有执行的是Java代码时pc计数器记录的才是下一条指令的地址。
另外每个线程都有自己的栈资源用于存储该线程的局部变量这些局部变量是该线程私有的其他线程是访问不了的除此之外栈还用来存放线程的调用栈帧。
堆是一个进程中最大的一块内存堆是被进程中的所有线程共享的是进程创建时分配的堆里面主要存放使用new操作创建的对象实例。
方法区则用来存放JVM加载的类、常量及静态变量等信息也是线程共享的。
线程创建与运行 当创建完thread对象后该线程并没有被启动执行直到调用了start方法后才真正启动了线程。
其实调用start方法后线程并没有马上执行而是处于就绪状态这个就绪状态是指该线程已经获取了除CPU资源外的其他资源等待获取CPU资源后才会真正处于运行状态。
一旦run方法执行完毕该线程就处于终止状态。
使用继承方式的好处是在run()方法内获取当前线程直接使用this就可以了无须使用Thread.currentThread()方法
不好的地方是Java不支持多继承如果继承了Thread类那么就不能再继承其他类。
另外任务与代码没有分离当多个线程执行一样的任务时需要多份任务代码而Runable则没有这个限制。
下面看实现Runnable接口的run方法方式。 RunableTask可以继承其他类。但是上面介绍的两种方式都有一个缺点就是任务没有返回值。
下面看最后一种即使用FutureTask的方式。 线程通知与等待
wait()函数
当一个线程调用一个共享变量的wait()方法时该调用线程会被阻塞挂起直到发生下面几件事情之一才返回
其他线程调用了该共享对象的notify()或者notifyAll()方法其他线程调用了该线程的interrupt()方法该线程抛出InterruptedException异常返回。
另外需要注意的是如果调用wait()方法的线程没有事先获取该对象的监视器锁则调用wait()方法时调用线程会抛出IIlegalMonitorStateException异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?
执行synchronized同步代码块时使用该共享变量作为参数。
synchronized(共享变量){}调用该共享变量的方法并且该方法使用了synchronized修饰。
synchronized void add(int a,int b){
}另外需要注意的是一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知或者被中断或者等待超时这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生但要防患于未然做法就是不停地去测试该线程被唤醒的条件是否满足不满足则继续等待也就是说在一个循环中调用wait()方法进行防范。
退出循环的条件是满足了唤醒该线程的条件。 如上代码是经典的调用共享变量wait()方法的实例首先通过同步块获取obj上面的监视器锁然后在while循环内调用obj的wait()方法。
下面从一个简单的生产者和消费者例子来加深理解。
如下面代码所示其中queue为共享变量生产者线程在调用queue的wait()方法前使用synchronized关键字拿到了该共享变量queue的监视器锁所以调用wait()方法才不会抛出IlegalMonitorStateException异常。如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程这里使用循环就是为了避免上面说的虚假唤醒问题。
假如当前线程被虚假唤醒了但是队列还是没有空余容量那么当前线程还是会调用wait()方法把自己挂起。
当前线程只会释放当前共对象的锁当前线程持有的其他共的监视器锁并不会被释放。
当一个线程调用共享对象的wait()方法被阻塞挂起后如果其他线程中断了该线程则该线程会抛出InterruptedException异常并返回。 wait(long timeout)函数
该方法相比wait()方法多了一个超时参数它的不同之处在于如果一个线程调用共享对象的该方法挂起后没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒那么该函数还是会因为超时而返回。
如果将timeout设置为0则和wait方法效果一样因为在wait方法内部就是调用了wait(0)。
需要注意的是如果在调用该函数时传递了一个负的timeout则会抛出IllegalArgumentException异常。
wait(long timeout,int nanos)函数
在其内部调用的是wait(long timeout)函数如下代码只有在nanos0时才使参数timeout递增1。
notify()函数
一个线程调用共享对象的notify()方法后会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。
一个共享变量上可能会有多个线程在等待具体唤醒哪个等待的线程是随机的。
此外被唤醒的线程不能马上从wait方法返回并继续执行它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后被唤醒的线程也不一定会获取到共享对象的监视器锁这是因为该线程还需要和其他线程一起竞争该锁只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似wait系列方法只有当前线程获取到了共享变量的监视器锁后才可以调用共享变量的notify()方法否则会抛出IllegalMonitorStateException异常。
notifyAll()函数
不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
个需要注意的地方是在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放入阻塞集合则该线程是不会被唤醒的。
等待线程执行终止的join方法
在项目实践中经常会遇到一个场景就是需要等待某几件事情完成后才能继续往下执行比如多个线程加载资源需要等待多个线程全部加载完毕再汇总处理。
Thread类中有一个join方法就可以做这个事情前面介绍的等待通知方法是Object类中的方法而join方法则是Thread类直接提供的。join是无参且返回值为void的方法。
线程A调用线程B的join方法后会被阻塞当其他线程调用了线程A的interrupt()方法中断了线程A时线程A会抛出InterruptedException异常而返回。
让线程睡眠的sleep方法
Thread类中有一个静态的sleep方法当一个执行中的线程调用了Thread的sleep方法后调用线程会暂时让出指定时间的执行权也就是在这期间不参与CPU的调度但是该线程所拥有的监视器资源比如锁还是持有不让出的。
指定的睡眠时间到了后该函数会正常返回线程就处于就绪状态然后参与CPU的调度获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
让出CPU执行权的yield方法
Thread类中有一个静态的yield方法当一个线程调用yield方法时实际就是在暗示线程调度器当前线程请求让出自己的CPU使用但是线程调度器可以无条件忽略这个暗示。
我们知道操作系统是为每个线程分配一个时间片来占有CPU的正常情况下当一个线程把分配给自己的时间片使用完后线程调度器才会进行下一轮的线程调度而当一个线程调用了Thread类的静态方法yield时是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调用yield方法时当前线程会让出CPU使用权然后处于就绪状态线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
线程中断
Java中的线程中断是一种线程间的协作模式通过设置线程的中断标志并不能直接终止该线程的执行而是被中断的线程根据中断状态自行处理。
void interrupt()方法
中断线程例如当线程A运行时线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。
设置标志仅仅是设置标志线程A实际并没有被中断它会继续往下执行。
如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起这时候若线程B调用线程A的interrupt()方法线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
boolean isInterrupted()方法
检测当前线程是否被中断如果是返回true,否则返回false;
public boolean isInterrupted(){//传递false,说明不清除中断标志return isInterrupted(false);
}boolean interrupted()方法
检测当前线程是否被中断如果是返回true,否则返回false。
与isInterrupted不同的是该方法如果发现当前线程被中断则会清除中断标志并且该方法是static方法可以通过Thread类直接调用。
另外从下面的代码可以知道在interrupted()内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
public static boolean interrupted(){//清除中断标志return currentThread().isInterrupted(true);
}理解线程上下文切换
在多线程编程中线程个数一般都大于CPU个数而每个CPU同一时刻只能被一个线程使用为了让用户感觉多个线程是在同时执行的CPU资源的分配采用了时间片轮转的策略也就是给每个线程分配一个时间片线程在时间片内占用CPU执行任务。
当前线程使用完时间片后就会处于就绪状态并让出CPU让其他线程占用这就是上下文切换从当前线程的上下文切换到了其他线程。
那么就有一个问题让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有 当前线程的CPU时间片使用完处于就绪状态时当前线程被其他线程中断时。
线程死锁
什么是线程死锁
死锁是指两个或两个以上的线程在执行过程中因争夺资源而造成的互相等待的现象在无外力作用的情况下这些线程会一直相互等待而无法继续运行下去。 线程A已经持有了资源2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源而进入了死锁状态。
那么为什么会产生死锁呢?学过操作系统的朋友应该都知道死锁的产生必须具备以下四个条件。
互斥条件指线程对已经获取到的资源进行排它性使用即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源则请求者只能等待直至占有资源的线程释放该资源。请求并持有条件指一个线程已经持有了至少一个资源但又提出了新的资源请求而新资源已被其他线程占有所以当前线程会被阻塞但阻塞的同时并不释放自己已经获取的资源。不可剥夺条件指线程获取到的资源在自己使用完之前不能被其他线程抢占只有在自己使用完毕后才由自己释放该资源。环路等待条件指在发生死锁时必然存在一个线程一资源的环形链即线程集合{T0,T1,T2,…,Tn}中的T0正在等待一个T1占用的资源T1正在等待T2占用的资源……Tn正在等待已被T0占用的资源。
如何避免线程死锁
要想避免死锁只需要破坏掉至少一个构造死锁的必要条件即可但是学过操作系统的读者应该都知道目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系使用资源申请的有序性原则就可以避免死锁那么什么是资源申请的有序性呢?
让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致其实资源分配有序性就是指假如线程A和线程B都需要资源1,2,3,…,n时对资源进行排序线程A和线程B只有在获取了资源n-1时才能去获取资源n。
我们可以简单分析一下为何资源的有序分配会避免死锁比如上面的代码假如线程A和线程B同时执行到了synchronized(resourceA),只有一个线程可以获取到resourceA上的监视器锁假如线程A获取到了那么线程B就会被阻塞而不会再去获取资源B,线程A获取到resourceA的监视器锁后会去申请resourceB的监视器锁资源这时候线程A是可以获取到的线程A获取到resourceB资源并使用后会放弃对资源resourceB的持有然后再释放对resourceA的持有释放resourceA后线程B才会被从阻塞状态变为激活状态。
所以资源的有序性破坏了资源的请求并持有条件和环路等待条件因此避免了死锁。
守护线程与用户线程
Java中的线程分为两类分别为daemon线程(守护线程)和user线程(用户线程)。
在JVM启动时会调用main函数main函数所在的线程就是一个用户线程其实在JVM内部同时还启动了好多守护线程比如垃圾回收线程。
那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时JVM会正常退出而不管当前是否有守护线程也就是说守护线程是否结束并不影响JVM的退出。
言外之意只要有一个用户线程还没结束正常情况下JVM就不会退出。 main线程运行结束后JVM会自动启动一个叫作DestroyJavaVM的线程该线程会等待所有用户线程结束后终止JVM进程。
如果你希望在主线程结束后JVM进程马上结束那么在创建线程时可以将其设置为守护线程如果你希望在主线程结束后子线程继续工作等子线程结束后再让JVM进程结束那么就将子线程设置为用户线程。
ThreadLocal
多线程访问同一个共享变量时特别容易出现并发问题特别是在多个线程需要对一个共享变量进行写入时。
为了保证线程安全一般使用者在访问共享变量时需要进行适当的同步。
同步的措施一般是加锁这就需要使用者对锁有一定的了解这显然加重了使用者的负担。那么有没有一种方式可以做到当创建一个变量后每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadLocal就可以做这件事情虽然ThreadLocal并不是为了解决这个问题而出现的。
ThreadLocal是JDK包提供的它提供了线程本地变量也就是如果你创建了一个ThreadLocal变量那么访问这个变量的每个线程都会有这个变量的一个本地副本。
当多个线程操作这个变量时实际操作的是自己本地内存里面的变量从而避免了线程安全问题。
创建一个ThreadLocal变量后每个线程都会复制一个变量到自己的本地内存。
ThreadLocal的实现原理
首先看一下ThreadLocal相关类的类图结构。 Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量而ThreadLocalMap是一个定制化的Hashmap。
在默认情况下每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。
其实每个线程的本地变量不是存放在ThreadLocal实例里面而是存放在调用线程的threadLocals变量里面。
也就是说ThreadLocal类型的本地变量存放在具体的线程内存空间中。
ThreadLocal就是一个工具壳它通过set方法把value值放入调用线程的threadLocals里面并存放起来当调用线程调用它的get方法时再从当前线程的threadLocals变量里面将其拿出来使用。
如果调用线程一直不终止那么这个本地变量会一直存放在调用线程的threadLocals变量里面所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法从当前线程的threadLocals里面删除该本地变量。
另外Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量。 下面简单分析ThreadLocal的set、get及remove方法的实现逻辑。
void set(T value) 代码(1)首先获取调用线程然后使用当前线程作为参数调用getMap(t)方法getMap(Thread t)的代码如下
ThreadLocalMap getMap(Thread t){return t.threadLocals};可以看到getMap(t)的作用是获取线程自己的变量threadLocals,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变量。
T get() void remove() 在每个线程内部都有一个名为threadLocals的成员变量该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用value则为我们使用set方法设置的值。
每个线程的本地变量存放在线程自己的内存变量threadLocals中如果当前线程一直不消亡那么这些本地变量会一直存在所以可能会造成内存溢出因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。
ThreadLocal不支持继承性
同一个ThreadLocal变量在父线程中被设置值后在子线程中是获取不到的。
这应该是正常现象因为在子线程thread里面调用get方法时当前线程为thread线程而这里调用set方法设置线程变量的是main线程两者是不同的线程自然子线程访问时返回null。
那么有没有办法让子线程能访问到父线程中的值?答案是有。
InheritableThreadLocal类
为了解决上节提出的问题InheritableThreadLocal应运而生。
InheritableThreadLocal继承自ThreadLocal,其提供了一个特性就是让子线程可以访问在父线程中设置的本地变量。 InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。
由代码(3)可知InheritableThreadLocal重写了createMap方法那么现在当第一次调用set方法时创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。
由代码(2)可知当调用get方法获取当前线程内部的map变量时获取的是inheritableThreadLocals而不再是threadLocals。
综上可知在InheritableThreadLocal的世界里变量inheritableThreadLocals替代了threadLocals。
下面我们看一下重写的代码(1)何时执行以及如何让子线程可以访问父线程的本地变量。这要从创建Thread的代码说起打开Thread类的默认构造函数。 InheritableThreadLocal类实例的set或者get方法设置变量时就会创建当前线程的inheritableThreadLocals变量。
当父线程创建子线程时构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。
那么在什么情况下需要子线程可以获取父线程的threadlocal变量呢?情况还是蛮多的比如子线程需要使用存放在threadlocal变量中的用户登录信息再比如一些中间件需要把统一的id追踪的整个调用链路记录下来。
其实子线程使用父线程中的threadlocal方法有多种方式比如创建线程时传入父线程中的变量并将其复制到子线程中或者在父线程中构造一个map作为参数传递给子线程但是这些都改变了我们的使用习惯所以在这些情况下InheritableThreadLocal就显得比较有用。