Java线程-毕向东老师视频回顾 及 继承Thread和实现Runnable的区别

我们知道创建线程有两种方法:继承Thread类,和实现Runnable接口,有什么区别呢?
首先,我们要回顾一下Java线程的知识:

第三部分 多线程编程

1. Java线程模型

  1. 线程是进程中的内容,是程序中的执行路径,或者叫一个控制单元;线程在控制着进程的执行;
  2. Java VM启动时会有一个进程java.exe;其中至少有一个线程负责java程序的运行,该线程存在于main()方法中,称之为主线程;
  3. 实际上,jvm默认启动的还有负责垃圾回收机制的线程;
  4. 主线程被jvm开启,其余的线程需要在子线程中开启;
  5. 多个线程实际上是不能同时执行的(和进程相同),在多个线程之间随机(优先级相同的情况下)切换;在某一个时刻,只有一个进程中的某一个线程在运行(多核CPU不同),线程间切换具有随机性;

1.1 线程状态

  1. 被创建
  2. 运行(running)
  3. 挂起(suspend)、冻结,阻塞(block)、临时状态:
    注意理解具备执行资格和具备执行权的区别!
    • 具备执行资格,但是放弃了执行权
    • sleep(time);
    • wait();
  4. *恢复(resume):恢复到临时状态(没有执行权)或者到运行状态(具备执行权)
    • notify();
  5. 终止(terminate)、消亡:

    • stop();
    • run()方法结束;
  6. 注意:suspend() stop() resume()在新特性中均已过时。

1.2 特点

  • 优先级:线程的优先级用来决定何时从一个运行的线程切换到另一个,即“上下文转换”(context switch);
  • 同步性:管程(monitor)或者理解为锁(Lock类),在进程同步性的老模式基础上实行的另一种方法,防止共享的资源被多个线程操纵;
  • 消息传递:多线程间通信通过调用所有对象都有的预先确定的方法;
  • Thread类和Runable接口:
    • getName 获得线程名称(setName 改变线程名称);
    • getPriority 获得线程优先级;
    • isAlive 判断进程是否仍在运行;
    • join 等待一个线程终止;
    • run 线程的入口点;
    • sleep 在一段时间内挂起线程,延迟1ms;
    • start 通过调用运行方法来启动线程;

2. 创建线程

2.1 主线程

  • 主线程在程序启动时自动创建,由一个Thread对象控制,调用其currentThread()方法(Thread类的公有静态成员)获得主线程的引用:
    static Thread currentThread()
  • 调用主线程示例:
class CurrentThreadDemo{
    public static void main(String[] args){
        Thread t = Thread.currentThread();
        System.out.println("Current thread: " + t);
        t.setName("BJTShang’s Thead");
        System.out.println("After name changed: " + t);
        try{
            for(int n=5;n>0;n--){
                System.out.println(n);
                Thread.sleep(1000);
            }
        }catch(InterruptedException e){
            System.out.println("Main thread interruped");
            }
    }
}

输出:
Current thread: Thread[main,5,main]
After name changed: Thread[BJTShang’s Thead,5,main]
5
4
3
2
1
在本程序中,对主线程的引用通过调用currentThread()方法获得,用局部变量t保存该引用;通过调用Thread类的各子方法setName,sleep控制主线程。线程信息输出格式:Thread[线程名称,优先级,组名称]

2.2 创建线程(重点)

通过实例化一个Thread对象来创建新线程:继承Thread类,或者实现Runnable接口;

1. 继承Thread类

重写新线程的入口run()方法,调用start方法启动新线程;(通常主线程必须是结束运行的最后一个进程:保证主线程运行足够时间
1. seName()getName()方法:默认名称“Thread-编号”;
2. 也可以构造使用super调用Thread父类带字符串参数构造方法的构造方法;

3. ```
    class Sub extends Thread{
        Sub(String name){
            super(name);//调用父类Thread的构造方法,在创建Sub类对象时,自定义线程名称
        }
    }
    ```
class SubThread extends Thread{
    public void run(){
        System.out.println("SubThread Run");
    }

    public static void main(String[] agrs){
        SubThread st = new SubThread();//创建好一个线程;
        st.start();//start()方法两个作用:启动该线程对象,调用该对象中的run()方法;
        //st.run();main方法中的主线程调用run方法,但是没有启动SubThread线程;依然属于单线程程序;
    }
}

Thread类中的run方法,用于存储线程要运行的方法(主线程要运行的代码存放于main方法)

卖票被卖出多张的问题

class Ticket extends Thread{
    private int ticket = 100;
    //private static int ticket = 100; 将ticket设置成静态,可以防止同一张票被卖出多张:相同的数据(实际上每个线程在内存中数据的位置是不同的(每一个线程都单独开辟了一块内存区域),是不同的数据,只是数值相同)被多个线程共享
    //如果定义成static,则实现了与类的静态绑定,避免数据被共享,但是:static的生命周期太长,占用资源
    public void run(){
        while (true){
            if (ticket > 0)
            {
                System.out.println(currentThread().getName() + "..." + ticket--);
            }
        }
    }
    public static void main(String[] args) 
    {
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        Ticket t4 = new Ticket();
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

2. 实现Runnable方法

1. 定义类实现Runnable接口;
2. 覆盖Runnable接口中的run方法;
    * 将线程要执行的代码存放在该run方法中
3. 通过Runnable子类对象,创建Thread类线程对象;
4. 将Runnable接口子类对象作为参数传递给Thread类的构造函数;
    * 因为要创建的线程所执行的代码run方法属于Runnable接口的子类对象,如果想让新创建的线程去执行run方法中的代码,必须将其所属对象传递给新线程;
5. 调用Thread类的start方法开启线程并调用Runnable接口子类中实现的run方法;

3. 那么,继承Thread类和实现Runnable接口创建新线程有什么区别?

  1. 避免了单继承的局限性,因此通常使用实现Runnable的方式创建多线程
  2. 同时,由于使用的是将run方法所属对象传给Thread对象,因此,当多个线程使用同一个run方法时,可以传入同一个对象,而不会发生数据冲突(因为在内存中只有一个run方法所属对象,多个线程共享同一块内存区域),但同时也会因为抢夺共享的数据,而引起安全问题,因此,下面要介绍的线程同步非常重要;
class Ticket implements Runnable{
    private int ticket = 100;
        public void run(){
        while (true){
            if (ticket > 0)
            {
                System.out.println(Thread.currentThread().getName() + "..." + ticket--);
            }
        }
    }
    public static void main(String[] args) 
    {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

3. 线程同步,线程锁

两个或两个以上的线程需要共享资源,需要一种办法来确定资源在某一时刻仅被一个线程占用,否则会出现安全问题,例如:

class Ticket implements Runnable{
    private int ticket = 10;
        public void run(){
        while (true){
            if (ticket > 0){
                try{
                    Thread.sleep(300);//此时间内,多个线程可以同步进入run方法,出现负票 
                }catch (Exception e){
                    e.printStackTrace();
                 }
                System.out.println(Thread.currentThread().getName() + "..." + ticket--);
            }
        }
    }
    public static void main(String[] args){
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
  • 共享数据安全性:
  • 核心思想就是:当a线程执行一段代码时(这段代码中存在对数据的改变操作),别的线程必须等待a线程执行完,才可以执行这段代码。
  • 同步的关键是“锁”的概念,JDK新特性中甚至加入了Lock类,稍后将对此说明;
  • 必须清楚共享数据在哪里使用;

  • 使用synchronized关键字:进入某一对象的管程,就是调用被synchronized关键字修饰的方法;在此同步方法内部,所有试图调用该方法的同实例的其他线程必须等待;直到拥有管程的线程从同步方法中退出,就可以将对象(锁)的控制权放弃给其他等待的线程;那些等待的线程被放在线程池中,并根据等待的先后顺序依次被放入管程中。

    • 使用同步方法:eg. synchronized void call(String msg)
    • 使用同步代码块,将对这个类定义的方法的调用放入一个synchronized块: synchronized(object){};
      • 一个同步块确保每个线程运行前都等待先前的一个线程结束;
    • 同步方法用的是什么锁?使用的锁是this,如果使用不同的锁,会出安全问题;
    • 同步代码块使用的是什么锁?使用的锁是object对象锁,可以使用任意类的对象,因为任意对象都有对象锁;
      • 如果同步方法被static修饰,使用的锁不是this(静态方法中不能有this),而是类名.class的Class类对象;

单例设计模式,总结:

线程死锁:避免
* 出现:同步中嵌套同步;

class Test implements Runnable{
    private boolean flag;
    Test(boolean flag){
        this.flag = flag;
    }
    public void run(){
            if(flag){
                synchronized(MyLock.locka){
                    System.out.println("if locka");
                    synchronized(MyLock.lockb){
                        System.out.println("if loakb");
                    }
                }
            }
            else{
                synchronized(MyLock.lockb){
                    System.out.println("else lockb");
                    synchronized(MyLock.locka){
                        System.out.println("else locka");
                    }
                }
            }
        }

}
class MyLock {
    static Object locka = new Object();
    static Object lockb = new Object();
}

class DeadLockTest {
    public static void main(String[] args) 
    {
        Thread t1 = new Thread(new Test(true));
        Thread t2 = new Thread(new Test(false));
        t1.start();
        t2.start();
    }
}

4 线程间通信

多个线程在操纵一个资源

class Res{
    String name;
    String gender;
}

class Input implements Runnable{
    private Res r;
    Input(Res r){
        this.r = r;
    }
    public void run(){
        int x = 0;
        while (true){
            if(x==0){
            r.name = "晨晨";
            r.gender = "男的";
            }else{
                r.name = "红题";
                r.gender = "女的";
            }
            x = (x+1)%2;
        }
    }
}
class Output implements Runnable{
    private Res r;
    Output(Res r){
        this.r = r;
    }
    public void run(){
        while(true){
            System.out.println(r.name + "..." + r.gender);
        }
    }
}

class ThreadCom{
    public static void main(String[] agrs){
        Res s = new Res();
        Input i = new Input(s);
        Output o = new Output(s);
        Thread t1 = new Thread(i);
        Thread t2 = new Thread(o);

        t1.start();
        t2.start();
    }
}

Input生产数据的同时,Output在消费数据,不安全。解决方法:给两个线程加上同一个锁;

class Res{
    String name;
    String gender;
}

class Input implements Runnable{
    private Res r;
    Input(Res r){
        this.r = r;
    }
    public void run(){
        int x = 0;
        while(true){
            synchronized(r){    
                if(x==0){
                    r.name = "BJT";
                    r.gender = "男的";
                }
                else{
                    r.name = "Shenqidemao";
                    r.gender = "女的";
                }
                x = (x+1)%2;
            }
        }
    }
}

class Output implements Runnable{
    private Res r;
    Output(Res r){
        this.r = r;
    }
    public void run(){
        while(true){
            synchronized(r){//两个线程使用同一个锁,对象相同就行,r对象引用也可以换成Input.class
                System.out.println(r.name + "..." + r.gender);
            }
        }
    }
}

class ThreadCom{
    public static void main(String[] agrs){
        Res s = new Res();
        Input i = new Input(s);
        Output o = new Output(s);
        Thread t1 = new Thread(i);
        Thread t2 = new Thread(o);

        t1.start();
        t2.start();
    }
}

等待唤醒机制
* 远离轮询:经典的序列问题:一个线程正在生产数据而另一个线程在使用、消费数据,在轮询系统中,消费者在等待生产者生产数据时浪费CPU周期,而生产者也必须等待消费者工作结束才能继续启动,浪费更多CPU时间;
* wait(),notify(),notifyAll()方法实现线程间通信
- 仅在synchronized方法中才能被调用,因为要对持有监视器(锁)的线程操作
- wait():被调用的线程放弃管程,进入睡眠,放弃执行权,直到其他线程进入相同管程并调用notify(),这些wait()状态的锁按照先后顺序存放在线程池中;
- notify():恢复相同对象中(也就是使用同一个锁)第一个调用wait()的线程;
- notifyAll():恢复相同对象中所有调用wait()的线程;根据优先级判定运行顺序;
- 以上三个方法都定义在Object类中,因为这些方法在操纵同步中的线程时,都必须要标识该线程的锁,只有同一个锁上的睡眠,可以被同一个锁上的notify唤醒;而锁可以是任意对象,因此这些方法定义在Object类中;

5 理解消费者生产者模型,就可以理解多线程!

synchronized{
    while(true){
        wait();
    }
    ...
    false;
    notifyAll();
}
  • JDK1.5中提供了多线程升级解决方案
    • 将同步synchronized替换成了现实的Lock操作;
    • 将Object监视器中的wait,notify,notifyAll方法,替换成了Condition对象,该对象可以使用Lock锁进行获取,可以只唤醒Lock对象中的特定Condition对象;注意:释放锁的动作一定要执行,使用try{}finally{}

6 线程其他方法

6.1 isAlive()、join()、setDaemon()

  • 一个线程如何知道另一线程已经结束?
  • 在线程中调用isAlive()判定当前进程是否结束/Alive;
    final boolean isAlive()
  • 更常用:调用join()要求当前线程等待直到指定的线程参与;
    final void join() throws InterruptedException

6.2 线程优先级

  • 线程调度决定优先级高的线程获得更多CPU时间,当然,获得的CPU时间依然取决于别的因素(一个实行多任务处理的操作系统如何更有效的利用CPU时间);例如,当低优先级线程运行时,一个高优先级的线程被恢复(从沉睡中或者I/O等待中),高优先级的线程将抢占低优先级所使用的CPU;
  • 设置线程优先级/获得当前的优先级设置:
    final void setPriority(int level)
    final int getPriority()
    当涉及调度时,Java的执行可以有本质上不同的行为,获得跨平台的线程行为的方法是自动放弃对CPU的控制;
  • 示例:两个不同优先级的线程,运行与具有优先权的平台
    //Copyright @BJTShang
    class clicker implements Runnable{
        int click = 0;
        Thread t;
        private volatile boolean running = true;
        public clicker(int p){
            t = new Thread(this);
            t.setPriority(p);
        }

        public void run(){
            while(running){
                click++;
            }
        }

        public void start(){
            t.start;
        }

        public void stop(){
            running = false;
        }
    }

    class HiLoPri{
        public static void main(String[] args){
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            clicker hi = new clicker(Thread.NORM_PRIORITY + 2);
            clicker lo = new clicker(Thread.NORM_PRIORITY - 2);
            lo.start();
            hi.start();
            try{
                Thread.sleep(10000);
            }catch (InterruptedException e){
                System.out.println("Main thread interrunpted.");
             }
            lo.stop();
            hi.stop;
            try{
                hi.t.join();
                lo.t.join();
            }catch(InterruptedException e){
                System.out.println("InterruptedException caught");
             }

             System.out.println("Low-priority thread: " + lo.click);
             System.out.println("High-priority thread: " + hi.click);
        }
    }

running前的关键字volatile用来确保running的值在下面的循环中每次都得到验证;

6.3 stop()、suspend()、resume()方法已过时

  • 如何停止线程?run()方法结束;
    • 多线程运行,运行代码通常是循环结构的;
    • 控制住循环,就可以让run()方法结束,也就是线程结束;
    • 当线程使用同步或者锁时需要注意线程有可能一直处于wait()状态,需要用interrupt()方法打断;

你可能感兴趣的:(java,继承,线程,回顾)