10线程知识点.docx
《10线程知识点.docx》由会员分享,可在线阅读,更多相关《10线程知识点.docx(29页珍藏版)》请在冰豆网上搜索。
10线程知识点
线程是JAVA的高级部分,我们的重点是理解JAVA中线程的概念,原理为今后的的学习高级部分,以及面试做准备。
然后是多线程的实现方式以及简单的启动,停止线程方法,为游戏做准备。
多任务处理:
什么是多任务呢?
多任务就是几个任务同时运行。
在我们上网的时候,即可以聊天,也可以听音乐,也可以浏览网页。
这就是多任务。
它有个特点:
就是在一个任务还没有做完的的时候,又去做另外的任务。
在程序里的多任务处理是怎么样的呢?
它也是指在同一个时候执行多个任务,它不必等到某个任务执行完之后,再去做其他的任务。
CPU在处理的时候,先是执行一个任务的一段代码,然后接着执行另一个任务的一段代码。
交替执行直到任务结束。
现实生活中这样的例子是很多的。
如:
有个柜台只有一个营业员。
这时候来了二个顾客。
那么这个营业员应该怎么样去应对呢?
她问第一个顾客要买什么商品,然后把商品给第一个顾客看;在第一个顾客看的时候,又去问第二个顾客要买什么商品,然后把商品给第二个顾客看。
在第二个顾客看时候,又去问第一个顾客对商品满不满意。
如果顾客说再看看,那么营业员又去问第二个顾客满不满意。
如果第二个顾客说满意,营业员就叫顾客付款,然后结束第二个顾客的营业任务,然后再去问第一个顾客满不满意。
如果第一个顾客也满意付款。
那么整个任务就结束了。
多任务处理的2种类型:
◆多任务处理有两种类型:
-基于进程
-基于线程
◆进程是指一种“自包容”的运行程序,有自己的地址空间;
当我们打开windows任务管理器的时候,就有一个进程的选项卡。
在这个选项卡里,我们可以发现有很多当前计算机正在运行的应用程序。
这些正在运行的每一个应用程序都叫一个进程。
它们都会占用一定的内存,都有自己的地址空间。
◆基于进程的特点是允许计算机同时运行两个或更多的程序。
在JAVA中是可以启动一个进程的。
就是说通过JAVA代码,我们可以打开另一个应用程序
代码示例:
//创建一个操作系统进程,用指定的的应用程序去打开指定的文件
ProcessBuilderprocess=newProcessBuilder(
"C:
/WINDOWS/NOTEPAD.EXE",//应用程序地址
"c:
/2.txt");//文件地址
try{
process.start();//启动进程
}catch(IOExceptione){
e.printStackTrace();
}
在上面的示例中,我们是开启了一个进程,用指定地址的应用程序(这里是记事本),去打开指定的文件(这里是C盘下的2.txt)
线程
和多进程不同,什么是多线程呢?
线程是进程内部单一的一个顺序控制流。
所以一个进程包含很多线程。
基于线程的多任务处理环境中,线程是最小的处理单位。
我们在使用word文档时,打开一个文件菜单,这就是开启了一线程,把一行字选中,加粗,这也是一个线程。
线程的概念其实很简单,线程就是程序运行时的路径,它决定将要执行什么代码。
所以当我们运行main方法时,其实就已经开启了一个线程了。
基于线程的多任务处理的优点
◆基于线程所需的开销更少
–在多任务中,各个进程需要分配它们自己独立的地址空间
–多个线程可共享相同的地址空间并且共同分享同一个进程
网站是个很典型的多任务处理的例子,当不同的用户同时去访问一个网站时,网站就需要进行多任务处理。
以前使用CGI(通用网关接口技术),CGI是一个多进程的技术,当一个用户去访问网站时,是开启一个进程,这样的话,当有多个用户同时访问时,那么就得开启多个进程。
每一个进程都有自己独立的地址空间,所以这样的网站内存很快就消耗掉了。
不但可连接的用户少,而且很容易受到恶意攻击。
在JAVA里,采用的servlet技术,是一个基于多线程的技术,共享同一个进程,共享相同的地址空间,这样的话,当有多个用户同时访问时,性能不会下降多少。
◆进程间调用涉及的开销比线程间通信多
◆线程间的切换成本比进程间切换成本低
那么什么又是单线程,什么又是多线程呢?
代码示例:
publicstaticvoidmain(String[]args){
a();
b();
c();
}
代码执行的顺序图如下:
在上面的例子里,大家可以看出,在main方法里执行时,必须要等到a()方法执行完了之后,才能执行b()方法。
b()方法执行完以后才能执行C()方法。
这样在等到一个方法执行完以后再执行另一个方法的模式叫单线程。
我以前用到的程序大部分都是单线程的。
那么多线程是怎么一回事呢?
在上面的例子,可以看出,多线程是多个方法同时执行,在a()方法执行时,同时执行b()方法,换句话说,b()方法不必等到a()方法执行完以后再去执行。
这就是多线程。
就是说在一个程序中,同时有多个执行路径存在。
《西游记》里孙语空去打妖精,这时候他一个人去打三个妖精,它要把一个妖精打死之后,才能去打第二个妖精,第二个妖精打死之后,再打第三个妖精。
这时候它是单线程的。
一个打妖精很累,它就拔三根毫毛,变成三个小孙悟空,一个小孙悟空打一个妖精,就是说它不必等到第一个妖精打死之后再去打第二个妖精,而是同时去打。
这就叫多线程。
◆在Java程序启动时,一个线程立刻运行,该线程通常称为程序的主线程。
也就是main方法运行时就产生一个主线程。
◆主线程的特点:
–最先开始
–最后结束
–由他产生其他子线程
–通常它必须最后完成执行,因为它执行各种关闭动作。
这在上例中,孙悟空就好比是主线程,它变出的三个小猴子是子线程。
小猴子是由孙悟空这个主线程产生的,小猴子打死妖精之后,由孙悟空再把小猴子回收成毫毛。
代码示例:
publicstaticvoidmain(String[]args){
//获得当前线程对象,即主线程
Threadt=Thread.currentThread();
System.out.println("当前线程是:
"+t);
//输出结果:
"当前线程是:
Thread[main,5,main]"
/*在中括号中第一个main指的是线程的名字,
*5指的是线程的优先级,
*第二个main是线程所在的线程组
*/
t.setName("主线程");//设置线程名字
System.out.println("当前线程名是:
"+t);
//输出结果是:
"当前线程名是:
Thread[主线程,5,main]"
try{
for(inti=0;i<3;i++){
System.out.println(i);
/*
*线程休眠1500毫秒
*就是线程暂时停止运行1500毫秒
*
*sleep是线程类Thread的静态方法,
*指的是当前线程休眠1500毫秒(这里是主线程)
*/
Thread.sleep(1500);
}
}
catch(InterruptedExceptione){
System.out.println("主线程被中断");}
}
在这个例子中,是单线程的程序,而main方法所在的线程就是主线程。
那么如何去通过这个主线程去启动一个子线程呢?
◆通过以下两种方法创建Thread对象:
1、-声明一个Thread类的子类,并覆盖run()方法。
publicclassThreads{
publicstaticvoidmain(String[]args){
Oneone=newOne();//创建线程对象
one.start();//启动线程
for(inti=65;i<75;i++){//打印字母
System.out.println((char)i);
}
}
}
classOneextendsThread{//继承Thread线程对象
publicvoidrun(){//启动线程后自动调用子类重写父类的run()方法
for(inti=0;i<10;i++){
System.out.println(i);
}
}
}
在上面的例子中定义了一个线程,并且重写了父类的run()方法。
然后在main方法这个主线程中启动这个线程,启动一个线程使用线程对象的start()方法。
这时候就用自动的调用父类的run()方法,循环打出一串数字。
在main主线程里也有一个循环,是打出字母。
因为是两个线程同时候运行,这时候打印的结果是数字和字线交替打印。
说明主线程不必等到子线程结束以后,就继续执行下面的语句。
有些同学可能会说了,既然启动线程是调用run()方法。
那么我直接调用run()方法不就行了,为什么要调用start()方法呢?
上面例子中,main()方法改为
Oneone=newOne();//创建线程对象
one.run();//启动线程
for(inti=65;i<75;i++){//打印字母
System.out.println((char)i);
}
不也一样吗?
这样的想法是错误的。
因为启动线程是使用start()方法。
这时候在主线程之外另外开启了一个线程,就是说主线程不必等到子线程结束以后再进行其他的动作。
所以这时候字母和数字循环打印。
而如果直接调用run()方法,其实只是普通方法的调用,并没有开启一个线程,所以是单线程的程序。
上面例子中,就不会是数字和字母交替打印,而是先把数字打印完毕,再打印字母。
2、声明一个实现Runnable接口的类,并实现run()方法。
publicclassThreads{
publicstaticvoidmain(String[]args){
Threadtwo=newThread(newTwo());//创建线程对象,而其中的参数是实现了Runnable接口的类对象。
two.start();//启动线程
}
}
classTwoimplementsRunnable{//实现Runnable接口,并实现这个接口定义的run()方法
publicvoidrun(){
System.out.println("线程启动");
}
}
上面的例子是由一个类去实现Runnable接口,然后实现Runnable里定义的run()方法。
然后再定义一个Thread线程类对象,将这个实现了Runnable接口的类对象作为参数传过去。
这样的话,就是把线程对象和这个实现了Runnable接口的类对象作了一个关联,在启动线程的时候,就是去执行实现了Runnable接口的类对象里重写的run()方法。
上面两种方法都可以实现线程的启动,但通常采用第二种方法启动线程,原因是如果一个类继承了另一个类的时候,那么就不能再继承其他类了,但接口可以多实现,所以实现接口的方式更加灵活一些。
线程启动之后,会有不同的状态,它有什么样的状态呢?
从上面的图可以看出线程经常处于暂时停止执行的状态,那么什么时候线程暂时停止执行呢?
◆线程:
-线程优先级比较低,因此它不能获得CPU时间。
-使用sleep()方法使线程睡眠。
-通过调用wait()方法,使线程等待。
-通过调用yield()方法,线程已显式出让CPU控制权。
(挂起)
-线程由于等待一个文件I/O事件被阻塞。
线程优先级
线程是有优先级的,就是说当两个线程同时启动或同时去访问一个对象时,优先级高的线程会拥有更高的访问权。
线程的优先级有1—10级,数字越大,优先级越高。
◆Java中的线程优先级是在Thread类中定义的常量
-NORM_PRIORITY:
值为5
-MAX_PRIORITY:
值为10
-MIN_PRIORITY:
值为1
◆缺省优先级为NORM_PRIORITY5级
◆有关优先级的方法有两个:
-finalvoidsetPriority(intnewp):
修改线程的当前优先级
-finalintgetPriority():
返回线程的优先级
代码示例:
publicclassThreads{
publicstaticvoidmain(String[]args){
Oneone=newOne();
Threadtwo=newThread(newTwo());
one.setPriority
(1);//设置第一个线程优先级为1级
two.setPriority(10);//设置第二个线程优先级为10级
one.start();//启动第一个线程
two.start();//启动第二个线程
}
}
classOneextendsThread{
publicvoidrun(){
System.out.println("第一个线程");
}
}
classTwoimplementsRunnable{
publicvoidrun(){
System.out.println("第二个线程");
}
}
上面例子运行的结果是“第二个线程第一个线程”。
虽然第一个线程比第二个线程先调用start()方法,但由于第一个线程的线程优先级比第二个线程低,所以优先启动第二个线程。
线程同步
《西游记》里孙悟空拔出两根毫毛,变出两个小猴子,一个小猴子去把妖精往死里打,而另一个小猴子去问妖精话。
这样的话就有问题了,如果第一个小猴子把妖精打死了,那么第二个猴子就没得问了。
在网站访问的时候,如果两个用户同时去访问同一个数据库的同一张表,如果一个用户去查询,而另一个用户去修改。
那么第一个用户访问到的数据是修改之前的数据,还是修改之后的数据呢?
所以,为了解决上面的问题,线程采用了同步的概念。
为了确保在任何时间点一个共享的资源只被一个线程使用,使用了“同步”。
就是说当两个线程同时访问同一个对象的时候,只允许一个线程访问。
所以使用同步可以保证任何时候只有一个线程访问一个方法或对象
◆线程同步,使用同步关键字synchronized来进行标识
代码示例:
需求:
模拟银行取款的业务。
有两个顾客去取钱的业务。
publicclassBankTest{
publicstaticvoidmain(String[]args){
Bankbank=newBank();//产生银行对象
Manman1=newMan(bank,1500);//产生第一个顾客对象
Manman2=newMan(bank,1300);//产生第二个顾客对象
try{
Thread.sleep(3000);//主线程休眠3000毫秒
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("银行最后余额"+bank.getMoney());//获得当前银行帐户最后余额
}
}
/**
*银行帐户类
*
*/
classBank{
/**
*当前帐户余额
*/
privateintmoney=2000;
/**
*返回当前金额
*/
publicintgetMoney(){
returnmoney;
}
/**
*设置当前金额
*@parampressMoney
*/
publicvoidsetMoney(intpressMoney){
if(pressMoney<=money){//当取走的钱小于当前金额时候,允许操作
try{
Thread.sleep(1000);//线程休眠1000毫秒
}catch(InterruptedExceptione){
//TODO自动生成catch块
e.printStackTrace();
}
money-=pressMoney;//存款减少
System.out.println("取走"+pressMoney);
}
else{//当取的钱大于帐户余额,拒绝操作
System.out.println("帐户已超支,操作失败");
}
System.out.println("最后"+money);//打印最后金额
}
}
/**
*顾客类
*@authorAdministrator
*
*/
classManextendsThread{
Bankbank;//银行帐户
intmoney;//顾客要取金额
Man1(Bankbank,intmoney){
this.bank=bank;
this.money=money;
this.start();
}
//调用帐户取钱方法
publicvoidputMoney(){
bank.setMoney(money);
}
/**
*线程run方法
*/
publicvoidrun(){
putMoney();
}
}
以上代码运行结果:
取走1300
取走1500
最后-800
最后-800
银行最后余额-800
上面例子中,对银行而言,是不允许帐户余额为负数的,所以在帐户类的setMoney()方法中,先判断取的钱是否大于帐户余额,如果大于帐户余额则操作失败。
但这时候,有两个顾客(Man类对象)线程同时对同一个帐户进行操作。
因为它们取的钱都小于帐户余额,这样的话,两个顾客都允许对帐户操作,这样帐户余额都会减少,因为两个顾客取钱的数目总和大于帐户余额,所以最后的结果帐户余额就是负数。
那么怎么防止这种情况呢?
解决办法就是在setMoney()方法前面加上一个同步的关键字synchronized。
使当前帐户类对象同步,这样在一线程在访问帐户时,另一个线程就不能再对帐户进行操作了。
上面代码改为:
/**
*设置当前金额
*@parampressMoney
*/
publicsynchronizedvoidsetMoney(intpressMoney){
…………
}
再运行时,结果就变成:
取走1500
最后500
帐户已超支,操作失败
最后500
银行最后余额500
上面的方法叫同步方法,使用同步方法的时候,锁住的是本身这个类对象,所以一旦这个类对象被一个线程使用,那个这个类对象就被锁住,那么其他的同步方法也不能被其他线程所访问了。
和同步方法对应的,还有同步块。
就是在一个方法里不要求全部同步,而只是在调用对象时,对这个对象同步,(锁住的是这个对象)这时候就得用同步块。
同步块写在方法里。
代码示例:
classOne{
voiddisplay(intnum){
System.out.print(""+num);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
System.out.println("中断");
}
System.out.println("完成");
}
}
classTwoimplementsRunnable{
intnumber;
Oneone;
Threadt;
publicTwo(Oneone_num,intn){
one=one_num;
number=n;
t=newThread(this);
t.start();
}
publicvoidrun(){
synchronized(one){//同步块,对one这个对象同步,当前线程对one这个对象进行访问时,就将这个对象锁住。
one.display(number);
}
}
}
死锁
◆当两个线程循环依赖于一对同步对象时将发生死锁。
例如:
一个线程进入对象ObjA上的监视器,而另一个线程进入对象ObjB上的监视器。
如果ObjA中的线程试图调用ObjB上的任何synchronized方法,就将发生死锁。
◆死锁很少发生,但一旦发生就很难调试
线程之间的通信:
在上面《西游记》里孙悟空拔出两根毫毛,变出两个小猴子,一个小猴子去把妖精往死里打,而另一个小猴子去问妖精话。
这时候就是两个线程去访问同一个对象,如果妖精这个对象加了同步的话,那么当问话的小猴子访问的时候,那么打妖精的小猴子就只有暂时停止运行,进行等待。
等到问话的小猴子问完了以后,就会通知打妖精的小猴子——我问完了,你可以把它打死了,这时候打妖精的小猴子就继续进行打妖精的任务。
象这样两个线程访问同一个对象,当一个线程访问时,其他线程进行等待,当访问线程放弃当前对象使用权时,通知其他线程这样的机制就叫线程之间的通信。
◆Java提供了一个精心设计的线程间通信机制,使用wait()、notify()和notifyAll()方法。
◆这些方法是作为Object类中的final方法实现的。
所以不能被子类重写
◆这三个方法仅在synchronized方法中才能被调用。
wait、notify、notifyAll都是Object类定义的方法,不要理解为线程类定义的方法,因为所有的类都是直接或间接继承于Object类的,所以所有的类都会拥有这三个方法。
◆wait()方法告知被调用的线程退出监视器并进入等待状态,直到其他线程进入相同的监视器并调用notify()方法。
◆notify()方法通知同一对象上第一个调用wait()线程。
◆notifyAll()方法通知调用wait()的所有线程,具有最高优先级的线程将先运行。
线程间通信最典型的例子就是生产者、消费者和仓库的关系。
代码示例:
publicclassCreates{
publicstaticvoidmain(String[]args){
仓库depot=new仓库();//创建仓库对象
/**
*生产者和消费者共享一个仓库资源
*/
new生产者(depot).start();
new消费者(depot).start();
}
}
class仓库{
booleanisfull;//判断当前仓库是否为空
}
class生产者extendsThread{
仓库depot;
生产者(仓库depot){
this.depot=depot;
}
/**
*生产方法
*/
privatevoidcreate(){
synchronized(