Java面向对象编程-第13章学习笔记

第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获得锁,继续执行其同步代码块。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值