您的位置:首页 > 编程语言 > Java开发

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获得锁,继续执行其同步代码块。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: