Java面向对象编程-第13章学习笔记
2016-01-16 23:20
405 查看
第13章 多线程
进程是指运行中的应用程序,每个进程都有自己独立的内存空间。线程是指进程中的一个执行流程。当进程内的多个线程同时运行,则称为并发运行。线程与进程的主要区别在于:每个进程都需要操作系统为其分配独立的内存空间,而同一进程内的线程则在同一块地址空间中工作。一、Java线程的运行机制
Java虚拟机进程中,执行代码的任务由线程完成。
每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack):(1)程序计数器:当线程执行一个方法时,程序计数器指向方法区中下一条要执行代码的字节码。(2)方法调用栈:用来跟踪线程运行中的方法调用过程,栈中的元素称为栈帧。每当线程调用一个方法,则向其中压入一个新的栈帧。栈帧存储方法的参数、局部变量和运算过程中的临时数据。栈帧由三部分组成:局部变量区(存放局部变量和方法参数)、操作数栈(线程的工作区,存放运算过程中临时数据)、栈数据区(为线程执行指令提供相关信息)。
除了方法调用栈所处的栈区之外,运行时数据区还包括堆区和方法区:(1)堆区存放线程所操纵的以对象形式存放的数据,比如一个新创建的实例对象以及其成员变量和属性等。(2)方法区存放的是方法的字节码。
二、线程的创建和启动
创建线程有两种方式:扩展java.lang.Thread类和继承Runnable接口。
1、扩展java.lang.Thread类
Thread类表示线程类,包括两个主要方法:(1)run():包含线程运行时所执行的代码;(2)start():用于启动线程。
注意:Thread类的run()方法没有声明抛出异常,因此其子类的run()也不能声明抛出异常。
/* * 当执行该程序时,Java虚拟机先创建并启动主线程 * 主线程的任务是执行main()方法 * main()方法创建了一个Machine类的实例对象 * 再在main()方法中通过machine调用其start()方法 * 随即进入machine线程,执行其run()方法 */ public class Machine extends Thread{ public void run(){ for(int a=0;a<50;a++) System.out.println(a); } public static void main(String[] args){ Machine machine=new Machine(); machine.start(); } }
/* * 主线程与用户自定义的线程并发运行 */ public class Machine extends Thread{ public void run(){ for(int a=0;a<20;a++){ System.out.println(currentThread().getName()+":"+a); try{ sleep(100); }catch(InterruptedException e){ throw new RuntimeException(e); } } } public static void main(String[] args){ Machine machine1=new Machine(); Machine machine2=new Machine(); machine1.start(); machine2.start(); machine1.run(); //这里属于主线程,main()方法通过machine1调用其run()方法 } }
/* * 主线程和machine线程共同操作machine对象的实例变量a */ public class Machine extends Thread{ private int a=0; public void run(){ for(a=0;a<20;a++){ System.out.println(currentThread().getName()+":"+a); try{ sleep(100); }catch(InterruptedException e){ throw new RuntimeException(e); } } } public static void main(String[] args){ Machine machine=new Machine(); machine.start(); machine.run(); //这里属于主线程,main()方法通过machine1调用其run()方法 } }
注意:因为start()方法是用于启动线程,因此在Thread类的子类中不要轻易覆盖start()方法。
对于:
//该代码执行仍属于主线程 //new语句只是在堆区创建了一个包括实例变量a的Machine类的实例对象 Machine machine=new Machine(); //只有当执行start()方法后,才会启动Machine线程 //并在java栈区为其创建相应的方法调用栈 machine.start();
如果一定要在子类中覆盖start()方法可以在重写代码中首句加上”super.start()”。
注意:一个线程只能被启动一次。
2、实现Runnable接口
Java中一个类只能继承一个父类,当继承Thread类后无法继承其他类,因此Java提供了Runnable接口,实现了创建线程的第二种方式。
/* * 通过Runnable接口实现线程 * 主线程中先创建一个实现接口的类的实例对象 * 再以该对象为参数创建两个线程并启动 * 因为t1和t2均依赖于同一个实例对象,因此他们共享machine的实例变量a */ public class Machine implements Runnable { private int a=0; public void run(){ for(a=0;a<20;a++){ System.out.println(Thread.currentThread().getName()+":"+a); try{ Thread.sleep(200); }catch(InterruptedException e){ throw new RuntimeException(e); } } } public static void main(String[] args) { Machine machine=new Machine(); Thread t1=new Thread(machine); Thread t2=new Thread(machine); t1.start(); t2.start(); } }
三、线程的状态转换
1、新建状态
当通过new语句创建线程对象时,该线程对象处于新建状态,仅仅在堆区被分配了内存。
2、就绪状态
当一个线程对象创建后,其他线程调用其start()方法,则该线程进入就绪状态,java虚拟机会为其创建方法调用栈和程序计数器。处于就绪状态的线程位于可运行池,等待获得CPU使用权。
3、运行状态
处于运行状态的线程占用CPU,执行程序代码。对于只有一个CPU的计算机,任意时刻只有一个线程处于运行状态。
4、阻塞状态
阻塞状态是指线程因为某种原因放弃CPU,暂停运行。当线程处于阻塞状态时,Java虚拟机不会给该线程分配CPU,直到其重新进入就绪状态,才有机会进入运行状态。阻塞状态分为3种:(1)位于对象等待池中的阻塞状态:当线程处于运行状态时,当执行了某个对象的wait()方法,JVM就会把线程放到这个对象的等待池中。(2)位于对象锁池中的阻塞状态:当线程处于运行状态,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,JVM就会把这个线程放到这个对象的锁池中。(3)其他阻塞状态:比如当前线程执行了sleep()方法或者调用了其他线程的join()方法,或者发出了I/O请求。
5、死亡状态
当线程退出run()方法则进入死亡状态,该线程结束生命周期。有可能正常执行完run()方法,也有可能遇见异常而退出。Thread类的isAlive()方法判断一个线程是否活着,当其死亡或者处于新建状态时,返回false,其余返回true。
四、线程调度
计算机的单个CPU在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。Java虚拟机负责线程调度,即按照抢占式调度模型为多个线程分配CPU使用权。JVM优先让处于就绪状态的线程中优先级高的线程占用,如果多个线程优先级相同,则虚拟机随机分配。
处于运行状态的线程会一直运行,直到不得不放弃CPU,一般有以下几个原因:
(1)JVM让当前线程暂时放弃CPU,转到就绪状态。
(2)当前线程因为某些原因进入阻塞状态。
(3)当前线程运行结束。
如果希望明确让一个线程给另外一个线程运行的机会,可以采取:(1)调整各个线程的优先级;(2)让处于运行状态的线程调用Thread.sleep()方法;(3)让处于运行状态的线程调用Thread.yield()方法;(4)让处于运行状态的线程调用另一个线程的join()方法。
1、调整各个线程的优先级
Thread类有3个静态常量用来表示线程优先级:
(1)MAX_PRIORITY:取值为10,表示最高优先级;
(2)MIN_PRIORITY:取值为1,表示最低优先级;
(3)NORM_PRIORTY:取值为5,表示默认优先级。
//我这里测试居然实现不了优先... public class Machine extends Thread{ private static StringBuffer log=new StringBuffer(); private static int count=0; public void run(){ for(int a=0;a<20;a++){ log.append(currentThread().getName()+":"+a+" "); if(++count%10==0) log.append("\n"); } } public static void main(String[] args) throws Exception{ Machine machine1=new Machine(); Machine machine2=new Machine(); machine1.setName("m1"); machine2.setName("m2"); machine2.setPriority(Thread.MAX_PRIORITY); machine1.setPriority(Thread.NORM_PRIORITY); machine1.start(); machine2.start(); Thread.sleep(2000); System.out.println(log); } }
2、线程睡眠:Thread.sleep()方法
当一个线程在运行中执行了sleep()方法,它就会放弃CPU,转到阻塞状态。当结束睡眠(睡眠时间结束)时,将转到就绪状态。
线程在睡眠时如果被中断,会收到InterruptedException异常。
//主线程启动Sleeper线程 //Sleeper线程睡眠5秒钟 //主线程睡眠500毫秒后中断Sleeper线程的睡眠 public class Sleeper extends Thread{ public void run(){ try{ sleep(5000); System.out.println("sleep over!"); }catch(InterruptedException e){ System.out.println("sleep interrupted"); } System.out.println("End!"); } public static void main(String[] args) throws Exception { Sleeper sleeper=new Sleeper(); sleeper.start(); Thread.sleep(500); sleeper.interrupt(); } }
3、线程让步:Thread.yield()方法
当线程执行yield()方法时,如果此时有与该线程相同优先级的其他线程处于就绪状态,那么yield()方法将把当前线程转入就绪状态(放入可运行池中)并使另一个同等优先级线程运行。如果没有相同优先级的线程,则yield()什么都不做。
yield()方法与sleep()方法的区别:
(1)yield只给可运行池中具有相同优先级的线程机会,而sleep()方法不考虑其他线程优先级,只是让当前线程睡眠。
(2)线程执行yield()方法后转入就绪状态,而sleep()转入阻塞状态。
(3)sleep()会抛出interruptedException,而yield()没有声明抛出异常。
4、等待其他线程结束:join()方法
当前运行的线程可以调用另外一个线程的join()方法,当前线程将转入阻塞状态,直至另一个线程运行结束,它才由阻塞状态转入就绪状态。
public class Machine extends Thread{ public void run(){ for(int a=0;a<20;a++) System.out.println(getName()+":"+a); } public static void main(String[] args) throws InterruptedException { Machine machine1=new Machine(); machine1.setName("m1"); machine1.start(); System.out.println("Main:join The Machine1!"); machine1.join(); System.out.println("Main:End!"); } }
join()方法有两种重载形式:
(1)public void join();
(2)public void join(long timeout);//timeout设定阻塞时间
当阻塞时间超过timeout或者加入的线程运行结束时,调用线程恢复运行。
五、获得当前线程对象的引用
Thread类的currentThread()静态方法返回当前线程对象的引用。
public class Machine extends Thread{ public void run(){ for(int a=1;a<4;a++){ System.out.println(currentThread().getName()+":"+a); yield(); } } public static void main(String[] args) { Machine machine=new Machine(); machine.setName("mac"); machine.start(); //属于machine线程 machine.run(); //属于main线程下的machine对象 } }
运行结果如下:
main:1 mac:1 main:2 mac:2 main:3 mac:3
如果把上面代码中的currentThread.getName()改成this.getName(),则this指代的是当前对象,而非当前线程。运行结果如下:
mac:1 mac:1 mac:2 mac:2 mac:3 mac:3
六、后台线程
后台线程是指为其他线程提供服务的线程,也称为守护线程。
调用Thread类的setDaemon(true)可以把一个线程设置为后台线程。只有所有前台线程都结束后,后台线程才会结束。
主线程默认为前台线程,由前台线程创建的线程默认也是前台线程。
/* * 功能:Machine线程是前台线程,负责将其实例变量a不断增1 *在Machine线程start()方法中创建了一个匿名线程类,并将其实例设为后台线程 *该后台线程定期(每隔5毫秒)把Machine对象实例变量a设为0 */ /*执行描述 * 1、main中先创建Machine类的实例machine,并为之命名 * 2、执行Machine类的start()以启动machine线程 * 3、执行Machine类的run()方法 * 3.1进入run()方法中while代码体,打印输出,a自增,判断if,再执行yield() * 3.2执行yield后继续转入Machine的start()方法体内的daemon部分 * 3.3执行daemon线程, * 3.4reset()。然后sleep()转入阻塞状态; * 3.5转入machine线程 * 4、依次循环 * 4.1直至machine中计数器达到2000,machine线程结束 * 4.2每隔5毫秒daemon被唤醒转入就绪状态,当machine中yield()执行后其获得CPU然后再回到3.4 */ public class Machine extends Thread{ private int a; private static int count; public void start(){ super.start(); Thread daemon=new Thread(){ public void run(){ while(true){ reset(); try{ sleep(5); }catch(InterruptedException e){ throw new RuntimeException(); } } } }; daemon.setDaemon(true); daemon.start(); } public void reset(){a=0;} public void run(){ while(true){ System.out.println(getName()+":"+a+"--"+count); a++; if(count++==2000) break; yield(); } } public static void main(String[] args) throws Exception{ Machine machine=new Machine(); machine.setName("m1"); machine.start(); } }
七、定时器Timer
在JDK的java.util包中提供了Timer类,可以定时执行任务。TimerTask类表示定时器执行的一项任务,是一个抽象类,实现了Runnable接口。
八、线程同步
线程的同步是为了防止多个线程访问一个数据对象时,对数据产生的破坏。
每个Java对象只有一个同步锁,任何时候只允许一个线程拥有这把锁。(1)假如这个锁已经被其他线程使用,JVM就会把其他线程放到对象锁池中,这些线程进入阻塞状态。等到锁释放的时候,JVM会从锁池中随机选取一个线程占有这个锁,并转到就绪状态。(2)加入这个锁没有被其他线程使用,当前调用者线程就会获得这个锁,并开始执行同步代码块。(3)一般情况下,只有当同步代码块执行完毕才会释放锁。(4)如果一个方法中所有代码都是同步代码,则可以修饰这个方法为synchronized。(5)同步代码块和中断方式并不矛盾,同步代码快中也可以执行sleep()和yield()方法,但此时并没有释放锁,只是暂时放弃了CPU。(6)synchronized声明不会被继承。
例:(同步与并发)
/* * 3个人打水,每个人打十桶水,依次打水 * 每个人都需要等前面人打完10桶水才开始 * 注意synchronized的位置 */ public class Person extends Thread{ private Well well; public Person(Well well){ this.well=well; } public void run(){ synchronized(well){ //针对对象well的同步代码 for(int i=0;i<10;i++){ //打10桶水 well.withdraw(); yield(); } } } public static void main(String[] args){ Well well=new Well(); Person person[]=new Person[3]; for(int i=0;i<3;i++){ //创建10个Person线程 person[i]=new Person(well); } person[0].start(); person[1].start(); person[2].start(); } }
/* * 改为对Well类withdraw()方法修饰为synchronized * 注意synchronized的位置 */ public class Person extends Thread{ private Well well; public Person(Well well){ this.well=well; } public void run(){ for(int i=0;i<10;i++){ //打10桶水 well.withdraw(); yield(); } } public static void main(String[] args){ Well well=new Well(); Person person[]=new Person[3]; for(int i=0;i<3;i++){ //创建10个Person线程 person[i]=new Person(well); } person[0].start(); person[1].start(); person[2].start(); } } public class Well { private int water=1000; public synchronized void withdraw(){ //打一桶水 water--; System.out.println(Thread.currentThread().getName()+": water left:"+water); } }
一个线程安全的类需要满足以下条件:
(1)、这个类的对象可以同时被多个线程安全的访问。
(2)每个线程都能正常执行原子操作,得到正确结果。
(3)每个线程的原子操作完成后,对象处于逻辑合理状态。
在以下情况下,持有锁的线程会释放锁:
(1)执行玩同步代码块;
(2)执行同步代码块过程中,遇到异常而导致线程终止;
(3)执行同步代码块中,执行了锁所属对象的wait()方法。
九、线程通信
不同线程执行不同的任务,如果这些任务需要某种联系,则线程之间必须可以通信以确保任务达成,比如生产者和消费者之间,需要实现知道库存。java.lang.Object提供了两种线程通信的方法:
(1)wait():执行该方法的线程释放对象的锁,Java虚拟机把该线程放入对象等待池,该线程等待其他线程将它唤醒。
(2)notify():执行该方法的线程唤醒在对象等待池中等待的某个线程,JVM会从等待池中随机选择一个线程。
假设t1和t2两个线程共同操纵一个s对象,通信流程如下:(1)t1执行s的一个同步代码块时,t1持有s的锁,t2在s的锁池中等待;(2)t1在同步代码块中执行s.wait()方法,t1释放s的锁,进入对象s的等待池;(3)在对象s锁池中等待的t2获得了s的锁,执行s的另一个同步代码块;(4)t2在同步代码块中执行s.notify()方法,JVM把t1线程从对象s的等待池中移到s的锁池中,等待获得锁;(5)t2线程执行完同步代码块,释放锁,t1获得锁,继续执行其同步代码块。
相关文章推荐
- Eclipse中FindBugs插件的应用
- JavaWeb注册并把数据存入数据库
- SpringMVC基于代码的配置方式(零配置,无web.xml)
- spring注解自动注入bean
- JDK各个版本的新特性jdk1.5-jdk8
- 从头认识java-17.6 Callable、Future和FutureTask
- Java解析XML的三种方式
- 每天学习十分钟15之Java学习笔记
- java.lang.IllegalStateException: commit already called
- Spring原理简单分析
- 利用java语言将csv格式数据导入mysql数据库
- Java异常笔记整理
- java 中byte[] 数组的合并
- Java 入门 之 开发环境的安装
- JavaConfig @Import 和 @ImportResource注解的使用
- java操作xml
- 标识符、常量、进制、运算
- Eclipse关联Android系统源码的2种方法
- Vert.x 实现REST
- MyEclipse下安装插件方法(properties文件编辑器Propedit为例)