Java入门:线程

相关概念

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

为什么一个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对象,并指定所属线程组

来个实栗吧:


Java入门:线程_第1张图片
DownLoadThread.java
Java入门:线程_第2张图片
ThreadTest.java

实现Runnable接口

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

Java入门:线程_第3张图片
DownLoadThread2.java
Java入门:线程_第4张图片
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):设置线程的优先级
Java入门:线程_第5张图片
设置线程的优先级
Java入门:线程_第6张图片
设置线程的优先级

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

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

yield实例:


Java入门:线程_第7张图片
yield实例

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

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


Java入门:线程_第8张图片
join实例

输出结果:

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

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

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


Java入门:线程_第9张图片
线程守护

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

来一个非常有意思例子:


Java入门:线程_第10张图片
中断线程实例

跑起来会输出:

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

终止线程
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():返回该线程所属的线程组

老习惯,例子:


Java入门:线程_第11张图片
线程组实例

输出:

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

线程同步

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

一个卖书的例子:


Java入门:线程_第12张图片
卖书

输出结果:

第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关键字和对象的锁标志配合完成数据的保护。
就像这个样子去保护:

Java入门:线程_第13张图片
synchronized

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

Java入门:线程_第14张图片
卖书的案例

输出如下:

第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就可以这样写了:

Java入门:线程_第15张图片
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()的例子:

Java入门:线程_第16张图片
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):安排指定的任务从指定的延迟后开始进行重复的固定延迟执行

很普通的例子:


Java入门:线程_第17张图片
Timer案例

死锁

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

你可能感兴趣的:(Java入门:线程)