相关概念
进程是操作系统管理的,每个进程都拥有自己独立的内存空间,拥有自己独立的一整套变量,进程和进程之间不共享内存。
多线程线程是同一个进程中的多个线程,他们共享内存和变量。
线程是轻量级的进程,线程是进程的组成部分,是进程中某个单一顺序的控制流,又称为轻量进程。
进程是线程组成的,线程只能在一个进程中的内部执行。创建线程的资源消耗比创建进程小很多。进程与进程之间,在内存方面是独立的,而同一个进程的各个线程之间,是共享内存同一内存块的。
为什么一个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
对象,并指定所属线程组
来个实栗吧:
实现Runnable
接口
上面通过继承Thread
类实现线程虽然可以用,但是也有缺点,就是类不能再继承其他类了,因为Java类只能继承一个父类嘛。但是!实现Runnable
接口来创建线程就没有这么个问题辣。
来第二个实栗:
让这个实例跑起来,观察一下他的输出结果,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实例:
线程控制:就要你等我,这么骄傲没毛病
当前等待另一个线程完成的方法:join()
方法。当调用join()
时,调用线程将阻塞,直到目标线程完成为止,调用线程在目标线程结束后才能重新得到运行。
join()
通常由使用线程的程序调用,用于将主程序划分成许多子程序,每个子程序分配一个线程。当所有的子程序都得到运行后,再调用主程序来进一步操作。
isAlive()
方法,是用来判断线程是否在活动状态,返回布尔值。如果线程已经运行结束,将返回false
。
一个简单的例子,主要是操作体会一下过程吧。
输出结果:
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
关键字和对象的锁标志配合完成数据的保护。
就像这个样子去保护:
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
就可以这样写了:
线程通信
当然,在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()
的例子:
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)
:安排指定的任务从指定的延迟后开始进行重复的固定延迟执行
很普通的例子:
死锁
死锁就是所有的线程都无法运行,整个程序处于阻塞状态,并且不可以恢复到运行状态。一旦发生死锁,线程就没有运行的意义了。
两个线程互相拿到了对方需要的资源,此时两个线程都不会放弃自己已经拿到的资源,这时程序就无法继续运行下去,死锁就出现了。我们写程序要注意防止死锁情况的出现。