美文网首页
Java入门:线程

Java入门:线程

作者: 我的袜子都是洞 | 来源:发表于2018-12-15 22:31 被阅读56次

    相关概念

    进程是操作系统管理的,每个进程都拥有自己独立的内存空间,拥有自己独立的一整套变量,进程和进程之间不共享内存。
    多线程线程是同一个进程中的多个线程,他们共享内存和变量。
    线程是轻量级的进程,线程是进程的组成部分,是进程中某个单一顺序的控制流,又称为轻量进程。
    进程是线程组成的,线程只能在一个进程中的内部执行。创建线程的资源消耗比创建进程小很多。进程与进程之间,在内存方面是独立的,而同一个进程的各个线程之间,是共享内存同一内存块的。

    为什么一个CPU可以同时执行那么多进程和线程?

    根本原因是,CPU将时间分隔成很多的小片,叫做时间片,每个进程分得一定的小片,得到分配的进程就可以运行,时间片用完就还下一个得到时间片的进程来运行。因为CPU的执行速快到飞起,每个时间片都贼短,所以我们就感觉是在同时执行。

    创建线程

    创建线程,首先需要定义线程类,定义线程类有两种主要方法:

    • 继承Thread
    • 实现Runable接口

    继承Thread

    定义一个线程只要它继承Thread类,那么它就是一个线程类。然后重写public void run()方法,run()方法里的代码就是线程要执行的代码块,方法run()称为线程体。
    Thread类封装了线程的行为,定义了很多控制线程的方法。
    Thread类的构造函数:

    • Thread():创建新的Thread对象
    • Thread(String name):创建线程并设定线程的名称
    • Thread(Runable target):根据target创建新的Thread对象
    • Thread(Runable target,String name):根据target创建新的Thread对象,并且指定线程名称
    • Thread(ThreadGroup group,Runable target):创建新的Thread对象,并指定所属线程组

    来个实栗吧:


    DownLoadThread.java ThreadTest.java

    实现Runnable接口

    上面通过继承Thread类实现线程虽然可以用,但是也有缺点,就是类不能再继承其他类了,因为Java类只能继承一个父类嘛。但是!实现Runnable接口来创建线程就没有这么个问题辣。
    来第二个实栗:

    DownLoadThread2.java ThreadTest2.java

    让这个实例跑起来,观察一下他的输出结果,main()方法的输出和新建线程的输出是交替出现的。
    通过实现Runnable接口的方法创建的线程,更加灵活,线程类本身还可以继承其他的类。而使用继承Thread类的方法创建的线程,得到线程的名字就更简单了。推荐使用Runnable方法,童嫂无欺。

    线程的调度和控制

    Java中一个线程从创建开始,有很多种状态,这些状态可以通过Thread类的一些相关方法进行控制转换。
    一个线程有如下的状态:

    • 新建状态(new):创建一个线程类的对象后,还没有调用start()方法的线程称为新建状态
    • 可运行状态(Runnable):调用start()方法后,系统为该线程分配了所需要的资源,但是还没有得到CPU的执行权,这是可运行状态。
    • 正在运行状态(Running):由虚拟机线程管理器调度,获得CPU的执行权,正在CPU上执行run()方法中的代码的线程。
    • 对象wait池等待状态:调用wait()方法后线程在对象的等待池中等待
    • 对象lock池等待状态:遇到synchronized关键代码段,无法获得对象的锁,则在该对象的lock池中等待
    • 其他阻塞状态:如执行sleep()方法或者遇到IO访问阻塞等
    • 结束状态(Dead):运行结束的线程

    Thread类的常用方法:

    • static Thread currentThread():返回当前正在执行的线程对象的引用
    • String getName():返回该线程的名称
    • int getPriority():返回线程的优先级
    • void interrupt():中断线程
    • static boolean interrrupted():测试当前线程是否已经中断
    • boolean isAlive():测试线程是否处于活动状态
    • boolean isDaemon():测试线程是否为守护线程
    • boolean isInterrupted():测试线程是否已经中断
    • void join():等待该线程中止
    • void join(long millis):等待该线程终止的时间最长为millis毫秒
    • void join(long millis,int nanos):等待该线程终止的时间最长为millis毫秒+nanos纳秒
    • void setDaemon(boolean on):讲线程标记为守护线程或用户线程
    • void setName(String name):改变线程名称,线程名称为name
    • void setPriority(int newPriority):更改线程的优先级
    • static void sleep(long millis):让当前正在执行的线程休眠millis毫秒
    • void stop():停止线程执行
    • String toString():返回该线程的字符串表示形式,包括线程的名称、优先级、线程
    • static void yield():暂停当前正在执行的线程对象,执行其他进程去

    线程的优先级:

    • Thread.MIN_PRIORITY:最小优先级1
    • Thread.MAX_PRIORITY:最大优先级10
    • Thread.NORM_PRIORITY:默认优先级5

    Java虚拟机根据线程的优先级来调度线程的执行,优先级越高的线程得到的运行机会就越多。如果排队等待运行的线程优先级相同,那么排在前面的线程将得到运行机会。
    使用下面的方法可以对优先级进行操作:

    • int getPriority():得到线程的优先级
    • void setPriority(int newPriority):设置线程的优先级
    设置线程的优先级 设置线程的优先级

    线程控制:让CPU休息一会
    就是使线程放弃CPU的执行一会。有三种情况:

    • 线程调用了yield(),sleep()方法
    • 由于当前线程进行访问,等待用户输入等操作,导致线程阻塞
    • 有高优先级的线程参与调度,导致当前线程放弃CPU

    yield实例:


    yield实例

    线程控制:就要你等我,这么骄傲没毛病
    当前等待另一个线程完成的方法:join()方法。当调用join()时,调用线程将阻塞,直到目标线程完成为止,调用线程在目标线程结束后才能重新得到运行。
    join()通常由使用线程的程序调用,用于将主程序划分成许多子程序,每个子程序分配一个线程。当所有的子程序都得到运行后,再调用主程序来进一步操作。
    isAlive()方法,是用来判断线程是否在活动状态,返回布尔值。如果线程已经运行结束,将返回false

    一个简单的例子,主要是操作体会一下过程吧。


    join实例

    输出结果:

    join现在的状态是:false
    我是JoinThread的线程
    跑起来后的状态是:true
    join运行结束,现在状态是:false
    程序运行结束
    

    线程守护神:Daemon线程
    它是为其他线程提供服务的线程,它一般应该是一个独立的线程,它的run()方法是一个无线循环。
    可以通过public boolean isDaemon()方法确定一个线程是否为守护线程。也可以通过public void setDaemon(boolean)方法设定一个线程为守护线程,注意设置守护进程方法要在线程启动方法start()之前。
    守护线程与其他线程的区别是,如果守护线程是唯一运行着的线程,程序会自动退出。
    典型的比如垃圾回收线程,如果虚拟机都退出了,那么为虚拟机提供收集内存垃圾线程就没有必要再运行了,它会自动停止运行。

    来个关于守护神的例子:
    守护神类里面写了一个无线循环输出,测试类里面有个循环,测试类的循环执行结束,守护神的运行也随即终止了。


    线程守护

    别睡了线程:中断线程
    对于睡眠状态sleep()或等待状态wait()的线程,如果我们需要中止其睡眠或者等待状态,可以调用interrupt()方法。
    如果线程在睡眠或等待状态下被调用了interrupt()方法,那么线程将抛出interruptException异常,需要将异常捕获并处理。
    如果线程没有睡眠或等待,调用interrupt()方法并不会产生异常,也不会对线程有任何的影响。

    来一个非常有意思例子:


    中断线程实例

    跑起来会输出:

    我就睡一会
    被吵醒了
    干嘛打断我休息?
    

    终止线程
    API中有如下方法,不过IDE会提示这个方法已过时,还是建议大家不要使用,按照官方提示来吧。
    了解一下public final void stop()

    线程组

    线程组表示一个线程的集合。线程组也可以是包含其他线程组。线程组构成一棵树,每个线程组都有一个父线程组。
    ThreadGroup类表示一个线程组,构造函数如下:

    • ThreadGroup(String name):构建一个名字为name的新线程组
    • ThreadGroup(ThreadGroupparent,String name):创建一个名字为name的新线程组,它的父线程组为parent

    为啥要用线程组哦?
    因为方便控制,只需要单个命令即可完成对整个线程组的操作。有种号令全军如沐春风的感觉。
    线程组常用方法如下:

    • int getMaxPriority():返回此线程组的最高优先级
    • String getName():返回此线程组的名称
    • ThreadGroup getParent():返回此线程组的父线程组
    • void interrupt:中断此线程组中的所有线程
    • boolean isDaemon():测试此线程组是否为一个守护线程线程组
    • boolean isDestroyed:测试此线程组是否已经被摧毁
    • void setDaemon(bollean daemon):设置组里的线程都为守护线程
    • void setMaxPriority(int pri):设置线程组的最高优先级
    • getThreadGroup():返回该线程所属的线程组

    老习惯,例子:


    线程组实例

    输出:

    线程组名是:我的线程组
    线程名是:Thread-0
    th2线程的优先级为:5
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    我负责疯狂输出
    

    线程同步

    如果涉及多个线程访问同一个数据的情况,就容易出现问题。比如一个int型数组int[] a={2,1,4,3},如果线程a对它升序操作,另一个线程b对它降序操作。两个线程同时运行,a线程刚刚把它的2和1排好,正好发生了时间片轮换,就是这么突然狗屎运,b线程得到CPU时间片,马上把4和3放到前面。这样a线程和b线程访问共享的数据a,就会出现结果不正确的情况,这很尴尬。

    一个卖书的例子:


    卖书

    输出结果:

    第10本卖出者: tom 
    第10本卖出者:Third
    第10本卖出者:线程1
    第7本卖出者: tom 
    第6本卖出者:Third
    第5本卖出者:线程1
    第5本卖出者: tom 
    第4本卖出者:Third
    第3本卖出者: tom 
    第3本卖出者:线程1
    第1本卖出者: tom 
    第2本卖出者:Third
    

    这个运行结果表明数据出现异常,很是尴尬。我们如何解决这个问题呢?
    当然伟大的Java已经为我们考虑好了,Java语言设计了同步机制来保护数据。在任何一个Java对象上,都有一个锁标志。synchronized关键字和对象的锁标志配合完成数据的保护。
    就像这个样子去保护:

    synchronized

    sysnchronized包围的代码叫关键代码,当线程运行到关键代码处,首先要到o对象上去取得o对象的锁标志,然后才能执行代码。但是锁标志只有一个,线程取得锁标志后,运行关键代码,只有从关键代码离开时,才会归还锁标志。如果线程没有运行完成关键代码,锁标志不会归还。其他线程再次运行到synchronized处,无法从o上取得锁标志,就无法执行关键代码,只能静静的等待锁标志的归还。
    咱来改造一下上面的卖书的案例,看是不是如我们所愿解决好问题。

    卖书的案例

    输出如下:

    第10本卖出者:线程1
    第9本卖出者:线程1
    第8本卖出者:线程1
    第7本卖出者:线程1
    第6本卖出者:线程1
    第5本卖出者:线程1
    第4本卖出者:线程1
    第3本卖出者:线程1
    第2本卖出者:线程1
    第1本卖出者:线程1
    

    上面只看到线程1是因为案例计算复杂度小,不信你可以把i修改成100试试,三个线程就都能看到辣。
    上面的代码还可以再优化一下写法,因为:
    public synchronized void sell(){}它就相当于如下代码:

    public void sell()
    {
        synchronized(this)
        {
        
        }
    }
    

    这样写代码可以更清晰,便于理解,上面的SellBook就可以这样写了:

    SellBook案例升级

    线程通信

    当然,在synchronized关键代码中,也可以主动放弃对象的锁标志。就是通过wait()方法做到这一点。线程放弃锁标志后,线程进入了阻塞状态。
    所有的Java对象都有一个wait池,每个池都可以容纳线程。wait()notify()方法都有是Object中的方法。当线程执行了wait()方法后,线程就释放对象的锁标志并进入该对象的wait池中等待。直到notify()通知后才能运行。

    wait()方法

    wait()方法关键点:

    • wait()方法是Object对象的方法,不是Thread类的方法
    • wait()方法只可能在synchronized块中被调用
    • wait()方法被调用时,原来的锁对象释放锁,线程进入block状态
    • wait()notify()唤醒的线程从wait()后面的代码开始继续执行

    notidy()方法

    notidy()用来唤醒正在等待的线程,使线程可以重新运行。被唤醒的线程从当时wait候的代码开始执行,但是因为其wait时已经释放了锁标志,所以必须重新获得锁标志。
    notidy()方法关键点:

    • 只能在sysnchronized中被调用,即先获得对象锁标志。
    • notify()方法唤起锁标志对象的等待池中的一个线程。但是,如果有几个线程在等待列表中,它无法决定哪个线程被唤醒。调用notifyAll()方法可以让所有的等待线程被唤醒。

    使用notify()唤醒wait()的例子:

    notify()唤醒wait()

    notidyAll()方法

    notidy()方法注意事项:

    • 只能在synchronized中被调用,即先获得对象锁标志
    • notidy()方法唤起锁标志所属对象的等待池中的所有的等待线程

    Timer和TimerTask

    Timer是一种定时器工具。它可以用来启动TimerTask来执行任务一次或者反复多次。
    TimerTask是一个抽象类,表示一个可以被Timer执行的定时器任务。它实际上是一种特殊的线程,是Timer来定时启动执行一次任务或者重复执行某个任务。
    TimerTask类本身没有实现run()方法,其run()方法由子类实现。

    Timer类常用方法如下:

    • void schedule(TimerTask task,Date time):安排在指定的时间执行执行的任务
    • void schedule(TimerTask task,Date firstTime,long period):安排指定的任务在指定的时间开始进行重复的执行
    • void schedule(TimerTask task,long delay):安排在指定延迟后执行指定的任务
    • void wchedule(TimerTask task,long delay,long priod):安排指定的任务从指定的延迟后开始进行重复的固定延迟执行

    很普通的例子:


    Timer案例

    死锁

    死锁就是所有的线程都无法运行,整个程序处于阻塞状态,并且不可以恢复到运行状态。一旦发生死锁,线程就没有运行的意义了。
    两个线程互相拿到了对方需要的资源,此时两个线程都不会放弃自己已经拿到的资源,这时程序就无法继续运行下去,死锁就出现了。我们写程序要注意防止死锁情况的出现。

    相关文章

      网友评论

          本文标题:Java入门:线程

          本文链接:https://www.haomeiwen.com/subject/bfezhqtx.html