当前位置: 首页 > 后端技术 > Java

并发编程:线程

时间:2023-04-01 19:25:02 Java

大家好,我是奋斗在互联网上的农民工小黑。前段时间公司面试招人,发现很多小伙伴虽然有两三年的工作经验,但是对Java的一些基础知识并没有扎实的掌握,于是小黑决定开始分享一些Java基础知识-与您相关的内容。本期我们首先从Java的多线程说起。好了,进入正题,首先我们来看看什么是进程和线程。ProcessVSThread进程是计算机操作系统中线程的集合,是系统资源调度的基本单位。一个正在运行的程序,如QQ、微信、音乐播放器等,一个进程中至少包含一个线程。线程是计算机操作系统中能够调度操作的最小单元。线程实际上是一段顺序运行的代码。比如我们音乐播放器中的字幕显示和声音播放是两个独立运行的线程。了解了进程和线程的区别之后,我们再来看看并发和并行的概念。ConcurrencyVSParallel当有多个线程在运行时,如果系统只有一个CPU,假设CPU只有一个核心,实际上不可能同时运行多个线程,只能分CPU运行时间分成若干个时间段,然后将时间段分配给各个线程执行。当一个时间段的线程代码运行时,其他线程处于挂起状态。我们称这种方法为Concurrent。当系统有多个CPU或一个CPU有多核时,线程的操作可能不是并发的。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程。两个线程不占用CPU资源,可以同时执行。这种方法称为并行(Parallel)。看完上面这段话,是不是觉得似懂非懂?什么并发?什么并行?什么李子?什么冬梅?别着急,小黑先用一个通俗的例子来解释一下并发和并行的区别,然后看完上面这段话,相信大家都能看懂。你吃到一半,电话来了,你吃完饭才接电话,说明你不支持并发或并行;吃一口饭,接一个电话,吃一口饭,接一个电话,说明你支持并发;你吃到一半,电话来了,你姐接电话,你一直在吃饭,你姐在接电话,这叫水货。总结一下,并发的关键是看你有没有能力同时处理多个任务,而不是同时处理;并行性的关键是看你是否可以同时处理多个任务。“(另一个CPU或核心)的存在(怎么感觉像是在骂人)。Java中的线程作为Java中的一种高级计算机语言,它也有进程和线程的概念。我们使用Main方法启动一个Java程序,实际上启动了一个Java进程,其中至少包含2个线程,另外一个是GC线程,用于垃圾回收。在Java中,线程通常是通过Thread类来创建的。接下来,让我们看看如何去做。线程创建方法如果想在Java代码中自定义一个线程,可以继承Thread类,然后创建自定义类的一个对象,调用该对象的start()方法启动。公共类ThreadDemo{publicstaticvoidmain(String[]args){newMyThread().start();}}classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("这是我自己定义的线程");}}或者实现java.lang.Runnable接口,在创建Thread类的对象时,将自定义java.lang.Runnable接口的实例对象作为参数传递给Thread,然后调用start()方法启动.publicclassThreadDemo{publicstaticvoidmain(String[]args){newThread(newMyRunnable()).s}}classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println("这是我的自定义线程");}}在实际开发过程中,是创建Thread的子类还是实现Runnable接口呢?事实上,没有确定的答案。我个人更喜欢实现Runnable接口。在后面要学习的线程池中,同样管理着Runnable接口的实例。当然,我们也需要根据实际场景灵活运用。线程的启动和停止从上面的代码中,我们其实已经看到,线程的启动可以通过在线程创建后调用start()方法来实现。新的MyThread().start();注意,从上一节的代码可以看出,我们自定义的Thread类重写了父类的run()方法,那么直接调用run()方法就可以启动一个线程了吗?答案是不。直接调用run()方法和普通的方法调用没有区别,不会启动新的线程执行,所以这里要注意。那么如何停止一个线程呢?我们看Thread类的方法,有一个stop()方法。@Deprecated//已弃用。publicfinalvoidstop(){SecurityManagersecurity=System.getSecurityManager();如果(安全!=null){checkAccess();如果(这!=Thread.currentThread()){security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);}if(threadStatus!=0){resume();}stop0(newThreadDeath());}但是从这个方法我们可以看出,添加了@Deprecated注解,也就是说这个方法已经被JDK弃用了。被弃用的原因是因为线程会通过stop()方法强行停止,这对于运行在线程中的程序来说是不安全的,就像你拉屎一样,别人强行让你停止。到底是夹断还是不夹断(这个例子有点恶心,但是很形象哈哈)。所以如果需要停止编队,就不能使用stop方法。那我们应该如何合理的停止一个线程呢?主要有两种方法:第一种方法:利用标志位终止线程类MyRunnableimplementsRunnable{privatevolatilebooleanexit=false;之后当前线程可以看到改变后的值(可见性)@Overridepublicvoidrun(){while(!exit){//循环判断标志位,是否退出System.out.println("Thisismyowndefined线”);}}publicvoidsetExit(booleanexit){this.exit=exit;}}publicclassThreadDemo{publicstaticvoidmain(String[]args){MyRunnablerunnable=newMyRunnable();新线程(可运行).start();runnable.setExit(真);//修改标志位,退出线程}}在线程中定义一个标志位,通过判断标志位的值来决定是否继续执行,在主线程中修改标志位的值,达到停止线程的目的线。第二种方法:使用interrupt()来中断线程线程t=新线程(可运行);t.开始();线程.睡眠(10);t.中断();//尝试中断线程}}classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){for(inti=0;i<100000;i++){System.out.println("线程正在执行~"+i);}}}这里需要注意的一点是,interrupt()方法不会像使用标志位或stop()方法那样立即停止线程。如果运行上面的代码会发现线程t不会被中断。那么线程t怎么停止呢?这时候就要注意Thread类的另外两个方法了。公共静态布尔中断();//判断是否中断,并清除当前中断状态privatenativebooleanisInterrupted(booleanClearInterrupted);//判断是否被打断,使用ClearInterrupted判断打断状态是否清除,那么我们修改一下上面的代码。publicclassThreadDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{MyRunnablerunnable=newMyRunnable();线程t=新线程(可运行);t.开始();线程.睡眠(10);();}}classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){for(inti=0;i<100000;i++){//if(Thread.currentThread().isInterrupted()){if(Thread.interrupted()){休息;}System.out.println("线程正在执行~"+i);}}}这时候线程t就会被中断。说到这里,大家其实都会产生疑惑。这个方法和上面传flags的方法好像没什么区别。他们都判断一个状态,然后决定是否结束执行。两者有什么区别?这其实涉及到另外一个东西,叫做线程状态。如果线程t处于sleep()或wait()中,如果使用了flag,则不能立即中断线程。只能等待sleep()结束或者Wait()被唤醒后才能中断。但是第二种方式,线程休眠时,如果调用interrupt()方法,会抛出异常InterruptedException,然后线程继续执行。线程的状态通过上面线程停止方法的对比,我们知道线程除了有运行和停止两种状态外,还有wait()和sleep()等方法,可以让线程进入等待或睡眠状态。那么线程的具体状态是什么呢?其实我们可以通过代码找到一些答案。在Thread类中,有一个枚举类State,它定义了线程的6种状态。publicenumState{/***未启动线程的线程状态*/NEW,/***可运行状态*/RUNNABLE,/***阻塞状态*/BLOCKED,/***等待状态*/WAITING,/***超时等待状态*/TIMED_WAITING,/***终止状态*/TERMINATED;}那么线程中这六个状态是如何变化的呢?什么时候是RUNNABLE,什么时候是BLOCKED,我们用下图来说明线程是如何看到状态变化的。线程状态详解初始化状态(NEW)当一个Thread实例是new出来的时候,线程对象的状态就是初始化(NEW)状态。可运行状态(RUNNABLE)调用start()方法后,线程进入可运行状态。注意runnable状态并不代表一定要运行,因为操作系统的CPU资源需要轮流执行(也就是初始的Concurrent),它要等待操作系统调度,而它只有在被调度的时候才会开始执行,所以这里刚好达到READY状态,说明有资格被系统调度;系统调度该线程后,该线程将进入运行(RUNNING)状态。在此状态下,如果本线程获得的CPU时间片用完,或者调用了yield()方法,则会重新进入就绪状态,等待下一次调度;当一个睡眠线程被通知()时,它会进入就绪状态;停放的线程(Thread)再次被停放(Thread),进入就绪状态;等待超时的线程将进入就绪状态;当同步代码块或同步方法获取到锁资源时,会进入就绪状态;线程调用sleep(long)、join(long)等方法时超时等待(TIMED_WAITING),或者锁对象在同步代码中调用wait(long)、LockSupport.arkNanos(long)、LockSupport.parkUntil(long)这些方法都会使线程进入超时等待状态。等待(WAITING)等待状态和超时等待状态的区别是没有指定等待时间,比如Thread.join(),锁对象调用wait(),LockSupport.park()等方法会让线程进入等待状态。阻塞(BLOCKED)阻塞状态主要发生在获取某些资源时。在获取成功之前,会进入阻塞状态。得知获取成功后,会在runnable状态进入ready状态。终止(TERMINATED)终止状态很好理解,就是当前线程执行结束,此时进入终止状态。这时候线程对象可能还活着,但是没有办法让它再次执行。所谓的“线程”是无法复活的。线程的重要方法从上一节我们看到线程状态之间变化的方法调用有很多,比如Join()、yield()、wait()、notify()、notifyAll(),这么多方法,具体作用是什么,一起来看看吧。我们上面提到的start()、run()、interrupt()、isInterrupted()、interrupted()方法想必大家已经看懂了,这里不再赘述。/***sleep()方法是让当前线程休眠一定时间,会抛出InterruptedException中断异常。*该异常不是运行时异常,必须捕获并处理。当线程在sleep()中处于休眠状态时,如果被中断,就会出现这个异常。*一旦中断,抛出异常,标志将被清除。如果不处理,则在下一个周期开始时无法捕捉到中断。所以一般在异常处理的时候设置flag。*sleep()方法不会释放任何对象的锁资源。*/publicstaticnativevoidsleep(longmillis)throwsInterruptedException;/***yield()方法是静态方法,一旦执行,就会让当前线程放弃CPU。放弃CPU不代表当前线程不执行,还会有CPU资源的竞争。*如果一个线程不重要或者优先级低,可以使用该方法将资源让给重要的线程。*/publicstaticnativevoidyield();/***join()方法意味着无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。*/publicfinalvoidjoin()throwsInterruptedException;/***join(longmillis)给出了一个最长等待时间,如果目标线程在给定时间后还在执行,则当前线程将停止等待并继续执行。*/publicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException;以上方法都是Thread类中的方法,从方法签名可以看出,sleep()和yield()方法是静态方法,join()方法是成员方法。wait()、notify()、notifyAll()这三个方法是Object类中的方法。这三个方法主要用在线程间竞争共享资源的同步方法或同步代码块中。沟通。/***让当前线程等待,直到另一个线程调用对象的notify()方法或notifyAll()方法。*/publicfinalvoidwait()throwsInterruptedException/***唤醒正在等待对象监视器的单个线程。*/publicfinalnativevoidnotify();/***唤醒所有在对象监视器上等待的线程。*/publicfinalnativevoidnotifyAll();有一个wait()、notify/notifyAll()的典型案例:producerconsumer,这个案例可以加深大家对这三个方法的印象。场景是这样的:假设有一家肯德基(肯德基给你多少,金拱门价格我翻一倍),有汉堡卖。为了汉堡的新鲜度,店员做的时候不会超过10个,然后就会有顾客来买汉堡。当汉堡的数量达到10个时,店员就得停止制作,当数量为0时,即卖完了,顾客就得等待新的汉堡制作。我们现在使用两个线程,一个用于生产,一个用于购买,来模拟这种场景。代码如下:classKFC{//汉堡数量inthamburgerNum=0;publicvoidproduct(){synchronized(this){while(hamburgerNum==10){try{this.wait();}catch(InterruptedExceptione){e.printStackTrace();}}System.out.println("制作一个汉堡包"+(++hamburgerNum));this.notifyAll();}}publicvoidconsumer(){synchronized(this){while(hamburgerNum==0){try{this.wait();}catch(InterruptedExceptione){e.printStackTrace();}}System.out.println("卖了一个汉堡包"+(hamburgerNum--));this.notifyAll();}}}publicclassProdConsDemo{publicstaticvoidmain(String[]args){KFCkfc=newKFC();新线程(()->{对于(inti=0;我<100;i++){kfc.product();}},"员工").start();newThread(()->{for(inti=0;i<100;i++){kfc.consumer();}},"customer").start();}}从上面的代码可以看出,这三个方法是要和wait()一起使用的,notify/notifyAll()方法是Object方法的localfinal,不能被重写。wait()阻塞当前线程,前提是必须先获取锁,一般与synchronized关键字一起使用。当线程执行wait()方法时,会释放当前锁,然后让出CPU,进入等待状态。由于wait()和notify/notifyAll()是在synchronized代码块中执行的,也就是说当前线程肯定已经获取到了锁。只有在执行notify/notifyAll()时,才会唤醒一个或多个等待线程,然后继续执行,直到执行完synchronized代码块的代码或者中途遇到wait(),再次释放Lock。需要注意的是,notify/notifyAll()唤醒休眠的线程后,线程会在上次执行后继续执行。所以,在做条件判断的时候,不能用if来判断。假设有多个客户购买。如果被惊醒后不做判断就直接购买,可能已经被别的顾客购买了,所以一定要用while来判断。被唤醒后再做判断。最后再强调一下我们上面提到的wait()和sleep()的区别。sleep()可以随时随地执行,不一定在同步代码块中执行,所以在同步代码块中调用不会释放锁,而wait()方法的调用必须在同步代码中才会释放锁.好了,今天的内容就到这里。我是小黑,下次见。喜欢的朋友可以关注小黑的公众号,定期分享干货。

猜你喜欢