编辑推荐: |
本文主要对线程进行了深度剖析。 希望能为大家提供一些参考或帮助。
文章来自于CSDN,由火龙果Linda编辑推荐。 |
|
一.线程和进程的区别和联系:
为啥要有线程?(1)并发编程,随着多核CPU的发展,成为刚需(2)多进程虽然也能实现并发编程效果,但是进程的创建和销毁,太重量了。
1.进程可以处理多并发编程的问题,但是进程太重,主要指它的资源分配和资源回收上,进程消耗资源多,速度慢,其创建、销毁、调度一个进程开销比较大。
2.线程的出现有效地解决了这一问题,既可以处理并发编程的问题,又因为它可以实现资源共享(主要指的是内存和文件描述符表),内存共享是指线程1new的对象在线程2,3,4里面可以直接用,文件描述符表共享指的是线程1打开的文件在线程2、3、4里面可以直接使用,第一个进程开销比较大,以后的进程都可以和第一个进程共享资源,这样就减少了资源的消耗,又提高了速度
举一个比较生动的例子:
有一个人要做一百只鸡,为了加快速度我们可以考虑一下再来一个人一起做,给每个人安排一间屋子,一个桌子
但是这样就比较耗费资源,这就是多进程。但是如果我们让两个人在同一间屋子同一个桌子上做,这样可以减少资源的开销了,屋子桌子共享
这就是 多线程。
3.进程里面包含多个线程,每个线程对应一个PBC,同一个进程里的PCB之间pid相同,同一个进程里的内存指针和文件描述符号表一样。
4.进程里面有多个线程,每个线程独立在CPU上调度,每个线程有自己的执行逻辑,线程操作是系统调度执行的基本单位。
5.增加线程的数量也不意味着可以一直提高速度,CPU的核心数量有限。
6.线程安全问题:因为线程之间可以资源共享,容易产生资源争夺的问题,会导致线程的异常问题,进而带走整个进程。
二.线程的创建:
主要有几种方式
(1)继承Thread类
(2)实现Runnable接口
(3)实现Callable接口
// 定义一个Thread类,相当于一个线程的模板 class MyThread01 extends Thread { // 重写run方法// run方法描述的是线程要执行的具体任务@Overridepublic void run() { System.out.println("hello, thread."); public class Thread_demo01 { public static void main(String[] args) { MyThread01 t = new MyThread01();
|
// 创建一个Runnable的实现类,并实现run方法 class MyRunnable01 implements Runnable { @Overridepublic void run() { System.out.println("hello, thread."); public class Thread_demo02 { public static void main(String[] args) { MyRunnable01 runnable01 = new MyRunnable01(); Thread t = new Thread(runnable01);
|
public class Thread_demo03 {public static void main(String[] args) { System.out.println(Thread.currentThread().getName());
|
public class Thread_demo04 {public static void main(String[] args) { Thread t = new Thread(new Runnable() { System.out.println(Thread.currentThread().getName());
|
public class Thread_demo05 { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println(Thread.currentThread().getName()); } catch (InterruptedException e) {
|
public class Mycallable implements Callable { public Object call() throws Exception { public static void main(String[] args) { FutureTask<Object>futureTask=new FutureTask<Object>(new Mycallable()); new Thread(futureTask).start();
|
三.Thread类的几个常见方法:
getId() 获取id
getName() 获取线程名称
getState()获取当前线程的状态
getPriority()优先级
isDaemon()是否后台线程
isAlive()是否存活
isInterrupted() 是否被中断
四.线程的执行:
1.线程之间是并发执行的,并且是抢占式调度的。
public static void main(String[] args) { Thread thread=new Thread(new Runnable() {
|
像这段代码我们无法确定是先打印“1”还是先打印“2”
2.start 方法和run()方法的区别:
(1)run()方法里面是要执行的任务,我们创建了一个thread,只是把任务梳理好了,start方法才真正去创建线程,让内核创建一个PCB,此时PCB才表示一个真正的线程,去执行run()方法里面的任务
(2)start方法只能执行一次,run方法可以执行多次
(3)start方法里面的run方法可以不执行完,就执行下面的代码,实现了线程的切换,而执行run()方法必须要把run方法执行完,继续执行下面的代码。
(4)start方法实现了多线程,而run方法没有实现多线程。
3.线程一旦执行完,内核里的pcb就会释放,操作系统里面的线程也就没了
public static void main(String[] args) { Thread thread=new Thread(new Runnable() {
|
像这段代码执行main方法的线程在执行完打印“2”这条语句后就没了,像thread线程执行完run方法,也就没了,但thread这个对象还存在,当它不指向任何对象时,就会被GC回收,之所以PCB消亡,而代码中thread对象还存在,是因为java中的对象的生命周期,自有其规则,这个生命周期和系统内核里的线程并非完全一致,内核里的线程释放的时候,无法保证java代码中thread对象也立即释放,因此此时需要通过特定的状态,来把thread对象标识成’无效‘,也是不能重新start的,一个线程,只能start一次。
4.isAlive()方法用来判断系统里面的线程是不是真正创建好了。
Thread thread=new Thread(new Runnable() { System.out.println(thread.isAlive()); } catch (InterruptedException e) { throw new RuntimeException(e);
|
如果thread的run还没跑,此时thread.isAlive()返回的是false,如果thread的run正在跑,此时返回的是true,如果thread里的run跑完了,此时返回的是false,此时内核里的pcb就释放了,操作系统里的线程也就没了。
5.public static void sleep(long millis)
让线程休眠,本质上就是让当前sleep的线程,暂时不参与CPU的调度执行(把这个线程PCB放到一个表示阻塞状态的队列中)等到sleep的时间到,操作系统会把这个PCB拿回到就绪队列中。
public static void main(String[] args) { Thread thread=new Thread(new Runnable() { for (i = 0; i < 3; i++) { System.out.println("hello"); } catch (InterruptedException e) { } catch (InterruptedException e) { throw new RuntimeException(e); System.out.println("world");
|
五.线程的终止:
我们通常使用interrupt()方法来终止线程,但是终不终止还是线程说了算。
线程可以通过不同的方式检查自己的中断状态,并采取适当的行动。其中常见的方法有两种:
使用 isInterrupted() 方法:该方法用于检查线程的中断状态,如果线程被中断,则返回 true,否则返回
false。你可以在线程的代码中定期调用 isInterrupted() 方法,以检查中断状态并决定是否终止线程的执行。
使用 Thread.interrupted() 静态方法:该方法用于检查当前线程的中断状态,并清除中断状态标志。如果线程被中断,则返回
true,否则返回 false。与 isInterrupted() 方法不同,Thread.interrupted()
方法会清除中断状态,所以你可以在检查中断状态的同时重置中断状态标志。
Thread提供了内置的标志位,可以使用isInterruptted方法判断标志位,使用interrupt方法来设置标志位(还能把线程从休眠中唤醒)
public static void main4(String[] args) { Thread thread=new Thread(new Runnable() { while(!Thread.currentThread().isInterrupted()) } catch (InterruptedException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e);
|
在这段代码中Thread.currentThread()表示来获取当前的线程的引用,isInterrupted()表示是否受到了要终止的通知。
一开始的时候isInterrupted()是false,进入到while循环里面,当执行main函数的线程,执行到thread.interrupt()的时候thread线程此时被通知要终止,此时isInterrupted()被设置为true,按照正常情况而言此时while循环是进不去的,但是如果线程在进行sleep,就会触发异常,把刚才的isInterrupted()再设置回false,此时还可以进行循环,这时要不要终止线程,其实取决于我们,如果加上一个break,跳出循环,那么线程也就执行完了。
线程的等待:控制两个线程的结束顺序:
Thread thread=new Thread(new Runnable() { } catch (InterruptedException e) { throw new RuntimeException(e);
|
原本main函数的线程在执行完start语句的时候,main函数线程要去执行下边的代码,tthread这个线程要去执行run()方法里面的任务,但是当main函数线程执行到hread.join()这条语句表示执行main函数的线程在这里等待一下,先等thread这个线程执行完,再执行。
六.操作系统的内核:
就绪队列:
这个链表里的PCB都是''随叫随到的状态'' ,就绪状态
阻塞队列:
是指当线程调佣sleep时,此时这个线程会进入休眠的状态,那么它会进入到阻塞队列中
这个链表的PCB,都是阻塞状态,不参与CPU的调度执行。PCB是使用链表来组织的,并不是简单的链表。一旦这个线程被唤醒,不再处于休眠的状态,这个PCB会回到就绪队列,但是不会马上就会被调度,要考虑到调度的开销,比如调用sleep(1000),对应的线程PCB就要在阻塞队列中待1000ms这么久,当这个PCB回到了就绪队列,会被立刻调度吗?虽然是sleep(1000),但是实际上要考虑到调度的开销,对应的线程是无法在唤醒之后就立即执行的,实际上的时间间隔大概率要大于1000ms。
七.线程的状态
反映的是当前线程的调度情况。
1.NEW 创建了Thread对象,但是还没调用start(内核里还没创建对应的PCB)
public static void main(String[] args) { Thread thread=new Thread(new Runnable() { System.out.println(thread.getState());
|
2.TERMINATED 表示内核中的pcb已经执行完了,但是Thread对象还在。
public static void main(String[] args) { Thread thread=new Thread(new Runnable() { } catch (InterruptedException e) { throw new RuntimeException(e); System.out.println(thread.getState());
|
3.RUNNABLE 可运行的包括两部分,正在CPU上执行的或者在就绪队列上的,随时可以去CPU上执行
public static void main(String[] args) { Thread thread = new Thread(new Runnable() { for (i = 0; i < 1000000; i++) { System.out.println("开始前"+thread.getState()); System.out.println("进行中"+thread.getState()); } catch (InterruptedException e) { throw new RuntimeException(e); System.out.println("结束了"+thread.getState());
|
4.WAITING wait方法触发的线程阻塞
5.TIMED_WAITING sleep触发的,线程阻塞
public static void main(String[] args) { Thread thread = new Thread(new Runnable() { } catch (InterruptedException e) { throw new RuntimeException(e); for (int i = 0; i < 100; i++) { System.out.println(thread.getState());
|
6.BLOCKED
synchronized 触发的,线程阻塞。
4 5 6 这三种状态都是阻塞状态,是由不同原因阻塞而成的。
八.多线程的意义:
我们可以通过代码来感受一下,单个线程和多个线程之间,执行速度的区别。
比如让实现一个变量自增200亿的操作。单个线程:
public static void main(String[] args) { long start=System.currentTimeMillis(); for(i=0;i<200_0000_0000L;i++) long end=System.currentTimeMillis(); System.out.println(end-start);
|
它的执行时间为:
而如果我们让两个线程去完成的话,一个线程干自增100亿的活,那么时间会不会缩短呢?
public static void main(String[] args) { Thread thread1=new Thread(new Runnable() { for(i=0;i<100_0000_0000L;i++) { Thread thread2=new Thread(new Runnable() { for(c=0;c<100_0000_0000L;c++) long beg=System.currentTimeMillis(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); long end=System.currentTimeMillis(); System.out.println(end-beg);
|
我们来看一下它执行完的时间:
我们可以看到两个线程一起完成这个自增操作确实要比一个线程完成这个自增操作花费的时间少,可能有人会问为什么两个线程执行的时间不是一个线程执行的时间的一半呢?首先呢多线程之所以块是因为它可以充分利用多核心CPU的资源,但是因为这两个线程在实际调度的过程中,这些次调度,有些是并发执行的(在一个核心上),有些是并行执行的(正在在两个CPU)上,我们没法报证这些次调度都是并行的,到底是多少次并发,多少次并行,取决于系统的配置。也取决于当前程序的运行环境。
下面我们再来看一下,当代码中出现两个join的时候,实际上的一个调度情况
long beg=System.currentTimeMillis(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); long end=System.currentTimeMillis(); System.out.println(end-beg);
|
我们看到当主函数main的线程执行完thread1.start这条语句时,thread1这个线程被真正创建,然后它去干它的活,main的线程执行thread2.start后,thread2这个线程也去干它的活,然后当main线程执行到thread1.join时,main函数线程停下来,等待thread1这个线程干完活,等thread1这个线程干完活后,main函数这个线程继续执行,这时候又遇到了thread2.join,需要等待thread2干完活,再继续执行,这里会存在一种情况,就是当thread2比thread1先干完活,那么main线程只需等待thread1干完活后执行就可以了,不需要再等待thread2了。多个线程同时执行,最终的时间,就是最慢的线程的时间,另外在谁中调用join,就是让谁这个线程等待,比如main调用t1,join就是让main来等待t1,如果是t2调用t1.join,就是让t2等待t1.
九.线程安全
线程安全问题本质上是内存安全,堆是共享内存,可以被所有线程访问,线程安全与之对应的也就是线程不安全问题,它指的是多线程在访问共享内存的时候,会出现预期结果和实际结果不符合的情况。
我们来看一下一个线程安全问题的代码:
public static int count=0; public static void main(String[] args) { Thread thread1=new Thread(new Runnable() { Thread thread2=new Thread(new Runnable() { } catch (InterruptedException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); System.out.println(count);
|
像这段代码,thread1和thread2分别对count执行了一万次自增操作,理论上最终打印出来count的值应该是两万。
但是我们看一下运行结果:
我们看到结果不是两万,下面我们详细地来介绍一下原因。
首先呢?我们先了解一下++操作本质上要分成三步:1.先把内存中的值,读取到CPU的寄存器中,也就是load操作2.把CPU寄存器里的数值进行+1运算
add 3.把得到的结果写到内存中,这三个操作,就是CPU上执行的三个操作,视为是机器语言。
如果是两个线程并发的执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序
就可能会产生一些结果上的差异,
这是一种可能的调度顺序,由于线程之间是随机 调度的,导致此处的调度顺序充满其他的可能性。
自增两次结果为2,这种情况是正确的,没有线程安全问题!!!
但是如果是这种情况:
这时我们进行了两次自增,count确为1,预期与实际不符,这时候线程是不安全的。
线程安全问题的原因:
1.根本原因:JMM内存模型
2.优化结构:多个线程同时修改一个变量会产生问题,一个线程修改一个变量,多个线程读取同一个变量,没事,多个线程修改多个不同的变量,也没事。
3.原子性:
如果修改操作是非原子性的,容易出现线程安全问题,count++可以拆分成load、add、save三个操作,如果++操作是原子性的,此时线程安全问题,也就解决了。
4.指令重排序
如何从原子性入手,解决线程安全问题。加锁!!!!通过加锁,把不是原子的,转成原子的。
加锁,说是保证原子性,其实不是说让这里的三个操作一次完成,也不是这三步操作过程中不进行调度,而是想让其他也想操作的线程阻塞等待了。
加锁的关键字为synchronized
public static int count=0; public synchronized static void add() public static void main(String[] args) { Thread thread1=new Thread(new Runnable() { Thread thread2=new Thread(new Runnable() { } catch (InterruptedException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); System.out.println(count);
|
如果我们在add()方法前面加上synchronized,此时我们再运行。
此时打印出来的就是20000,符合预期。
解决线程安全问题的方案:(1)用原子类比如AtomicInteger,LongAdder
(2)加锁,例如synchronized,ReentrantLock
(3)使用线程安全的集合:例如Hashtable,ConcurrentHashMap
(4)使用ThreadLocal本地变量
十.线程常见的调度方法
十一.线程的上下文切换
线程的上下文切换指的是在多线程运行时,操作系统从当前正在执行的线程中保存上下文(包括当前线程的寄存器、程序指针、栈指针等状态信息)。
十二.守护线程
含义:守护线程为所有线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆。
守护线程可以随时被中断,守护线程的终止是自身无法控制的。
守护线程的作用是什么?
举例:GC垃圾回收线程,就是一个守护线程,当我们的程序中不再有任何运行的Thread,程序就不再会产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开
应用场景:(1)为其他线程提供服务支持 (2)在任何情况下,程序运行结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来用,反之,如果一个正在执行某个操作的线程必须要正确关闭否则会出现不好的后果,那么这个线程就不能是守护线程,而是用户线程,通常都是些关键的事务,比如说数据库的录入或者更新,这些操作是不能被更新的。
thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个异常,我们就不能把常规线程设置为守护线程
在Daemon线程中产生的线程也是Daemon
守护线程不能访问固有的资源,比如读写操作,因为它会在任何一个操作中间发生中断
Java自带的多线程框架,比如ExecutorService,会把守护线程转化为用户线程,所以如果要使用后台线程就不能用java的线程池。
十三.线程间通信方式有哪些
(1)共享变量:共享变量指的是多个线程都可以访问和修改的变量,他们通常是在主线程上创建的,多个线程对同一个共享变量进行读写操作时,需要使用同步机制比如synchronized、lock等保证线程安全
public static void main56(String[] args) { ArrayList<Integer> list = new ArrayList<>(); Thread producer = new Thread(() -> for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + i); } catch (InterruptedException e) { Thread consumer = new Thread(() -> { Integer a = list.remove(0); System.out.println(Thread.currentThread().getName() + a); } catch (InterruptedException e) {
|
(2)管道通信:管道是一种基于文件描述符表的通信机制,形成一个单向通信的数据流管道,它通常用于只有两个进程或线程之间的通信,其中一个进程将数据写入到管道,另一个进程从管道的输入端口读取数据
public static void main22(String[] args) throws IOException { PipedInputStream inputStream=new PipedInputStream(); PipedOutputStream outputStream=new PipedOutputStream(); inputStream.connect(outputStream); Thread producer=new Thread(()->{ } catch (IOException e) { } catch (InterruptedException e) { Thread consumer=new Thread(()->{ Integer b=inputStream.read(); } catch (IOException e) { } catch (InterruptedException e) {
|
(3)信号量:信号量是一种计数器,用于控制多线程对资源的访问,当一个线程需要访问资源时,他需要申请获取信号量,如果信号量的计数器大于0,则可以访问资源,否则改线程就会等待
当线程结束访问资源后,需要释放信号量,并将计数器加1
public static void main23(String[] args) { ArrayList<Integer>list=new ArrayList<>(); Semaphore semaphore=new Semaphore(0); Thread producer=new Thread(()->{ System.out.println(Thread.currentThread().getName()+i); Thread consumer=new Thread(()->{ } catch (InterruptedException e) { Integer a=list.remove(0); System.out.println(Thread.currentThread().getName()+a);
|
(4)条件变量 :条件变量是一种通知机制,用于在多个线程之间的传递状态信息和控制信息,当某个线程需要等待某个条件变量发生改变时,他可以调用wait方法挂起,并且释放所占用的锁,当某个线程满足条件后,可以调用notify()方法或者signal()方法来通知改条件变量的线程继序执行
public static void main(String[] args) { ReentrantLock reentrantLock=new ReentrantLock(); Condition condition=reentrantLock.newCondition(); ArrayList<Integer>list=new ArrayList<>(); Thread producer=new Thread(()->{ System.out.println(Thread.currentThread().getName()+i); } catch (InterruptedException e) { Thread consumer=new Thread(()->{ Integer a=list.remove(0); System.out.println(Thread.currentThread().getName()+a); } catch (InterruptedException e) {
|
|