Java系列(十四)__多线程

1、进程与线程的区别

从操作系统来讲,可以被民用的系统最早是DOS,但是传统的DOS系统有一个特征:电脑出现病毒之后系统会死机,因为传统的DOS系统采用的是单进程的处理方式。而后来到了windows时代,那么采用的是多进程的处理方式,在同一个时间段上会有多个程序并发执行,轮流强占CPU资源。

 

         但是进程的启动和销毁还是很慢的。所以后来人们开始尝试在进程上做进一步的优化,那么就产生了线程的概念,即:线程是在进程基础之上扩充的。线程的启动和销毁将比进程更快,一个进程上是可以划分出多个线程的,而进程消失的话线程一定也会消失,Java是为数不多的支持多线程编程的语言之一。



2、实现多线程

如果要想实现多线程的开发,那么就必须像主类存在那样,也需要一个线程的主体类,但是这个类并不能单独定义,必须让其继承Thread类或者是实现Runnable接口(如果从实际的开发来讲一定是Runnable接口)。

2.1、继承Thread类

         线程的主体类只需要通过extends关键字继承Thread类,那么这个类就可以用于线程的控制,当继承了Thread类之中用户还需要覆写Thread类之中的run()方法“public void run()”。

范例:定义线程的主体类

class MyThread extends Thread {

    private String name ;  // 理解为对象的名字

    public MyThread(String name) {

        this.name = name ;

    }

    @Override

    public void run() { // 线程的主方法

        for (int x = 0 ; x < 10 ; x ++) {

            System.out.println(this.name + ",x = " + x);

        }

    }

}

         此时每一个MyThread类的对象都是一个线程,那么可以产生多个线程。

         但是当线程主体类定义完成后,必须通过对象进行访问,但是调用的并不是类中的run()方法,而是Thread类的start()方法:“public void start()”。调用此方法就相当于调用了run()方法。

范例:启动多线程

public class TestDemo {

    public static void main(String[] args) {

        MyThread mtA = new MyThread("线程A") ;

        MyThread mtB = new MyThread("线程B") ;

        MyThread mtC = new MyThread("线程C") ;

        mtA.start() ;

        mtB.start() ;

        mtC.start() ;

    }

}

         如果使用的是run()方法实际上只是一个方法的普通调用,将采用顺序的方式执行,而如果调用的是start()方法则表示将启动多线程,多个线程彼此之间并发运行。

疑问:通过简短的分析,应该已经知道多线程的基本运行过程了,但是为什么非要通过start()来调用run()呢?

         为了方便理解,打开关于Thread类中start()方法的定义:

    public synchronized void start() {

        if (threadStatus != 0)

            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;

        try {

            start0();

            started = true;

        } finally {

            try {

                if (!started) {

                    group.threadStartFailed(this);

                }

            } catch (Throwable ignore) {

            }

        }

    }

    private native void start0();

         本方法会抛出一个“IllegalThreadStateException”异常,但是如果是手工的使用throw抛出异常,应该使用try…catch处理才对。但是此时并没有强制性要求,因为通过观察“IllegalThreadStateException”继承结构:

java.lang.Object

         java.lang.Throwable

                   java.lang.Exception

                            java.lang.RuntimeException

                                     java.lang.IllegalArgumentException

                                               java.lang.IllegalThreadStateException

         因为RuntimeException子类,一般当一个线程被重复启动的时候就会抛出此异常。所以一个线程只能够启动一次。

         可以发现在调用start()方法的时候会自动调用start0()方法,而在start0()方法的声明处发现有一个native关键字,那么此关键字表示的是,将通过本地的原始函数进行代码的实现。

         线程的操作一定是需要进行CPU资源调度,每个操作系统的资源调度是不一样的,所以此方法实际上会由JVM实现,那么编写的时候只给出了一个抽象方法的名称,而具体的实现将根据不同的操作系统进行覆写,所以才实现可移植性。

         所以可以得出结论,当用户执行start()方法的时候意味着将进行操作系统的资源调配,调配之后才会执行run()方法,即:任何时候只要是启动多线程,都一定要使用Thread类之中的start()方法。

2.2、实现Runnable接口

         类与类之间的继承并不推荐去使用,但是类实现接口是一个推荐的功能,在Java里面为了解决多线程的单继承局限问题,所以也提供有一个Runnable接口,用户只需要让线程的主体类实现Runnable接口即可。

范例:观察Runnable接口的定义结构

public interface Runnable {

    public void run() ;

}

范例:使用Runnable定义线程主体类

class MyThread implements Runnable {

    private String name ;

    public MyThread(String name) {

        this.name = name ;

    }

    @Override

    public void run() { // 线程的主方法

        for (int x = 0 ; x < 10 ; x ++) {

            System.out.println(this.name + ",x = " + x);

        }

    }

}

         通过之前的分析可以得出结论,只要是线程的启动一定要依靠Thread类的start()方法,如果说现在一个类直接继承了Thread类,那么可以继承下start()方法,但是此时实现的是Runnable接口,那么将没有start()可以被继承。那么下面来观察Thread类中构造方法的定义:“public Thread(Runnable target)”。发现可以接收一个Runnable接口子类对象。

范例:启动多线程

public class TestDemo {

    public static void main(String[] args) {

        MyThread mtA = new MyThread("线程A") ;

        MyThread mtB = new MyThread("线程B") ;

        MyThread mtC = new MyThread("线程C") ;

        new Thread(mtA).start();

        new Thread(mtB).start();

        new Thread(mtC).start();

    }

}

         此时两者的功能是完全相同的,但是很明显,使用Runnable要比使用Thread更加的合理。

2.3、两种实现方式的区别(面试题)

         通过一系列的分析之后已经清楚了多线程的两种实现方式,但是这两种方式从结构上讲一定使用的是Runnable,可是除了这一点之外,还有其它的区别吗?

         那么首先来观察一下Thread类的定义:

public class Thread extends Object implements Runnable

         发现原来Thread类也实现了Runnable接口,于是一件很有意思的事情发生了。


 

         从结构上讲Thread是一个代理类的结构设计,但是又不那么完整。如果是一个纯粹的代理设计模式,那么用户应该调用的是Thread类的run()方法,但是现在调用的却是start()方法(并不是Runnable接口提供的方法),所以在整个操作之中虽然形式是代理结构,但是最终还是有差异的,而这个差异也是由于历史原因造成的。

         除了这个基本的联系之外,还有一点不算区别的小区别:使用Runnable接口实现的多线程要比使用Thread类实现的多线程更容易表示出数据共享的概念。

范例:编写一个简单的卖票程序,利用Thread类实现(产生三个线程对象一起卖票)

package cn.mldn.demo;

class MyThread extends Thread {

    private int ticket = 5 ;    // 假设有5张票

    @Override

    public void run() { // 线程的主方法

        for (int x = 0 ; x < 20 ; x ++) {

            if (this.ticket > 0) {

                System.out.println("卖票:ticket = " + this.ticket --);

            }

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        new MyThread().start();// 一个线程

        new MyThread().start();// 两个线程

        new MyThread().start();// 三个线程

    }

}

         这个时候发现每一个线程对象都有各自的五张票进行售卖,不符合要求。

范例:利用Runnable接口实现

package cn.mldn.demo;

class MyThread implements Runnable {

    private int ticket = 5 ;    // 假设有5张票

    @Override

    public void run() { // 线程的主方法

        for (int x = 0 ; x < 20 ; x ++) {

            if (this.ticket > 0) {

                System.out.println("卖票:ticket = " + this.ticket --);

            }

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread() ;

        new Thread(mt).start();// 一个线程

        new Thread(mt).start();// 两个线程

        new Thread(mt).start();// 三个线程

    }

}

面试题:请解释出多线程两种实现方式的区别?分别编写程序进行说明?

         · 多线程的两种实现方式:继承Thread类、实现Runnable接口;

         · 如果继承了Thread类,那么会受到单继承局限,而且不方便表示出数据共享的概念,Thread类是Runnable接口的子类;

         · 如果实现了Runnable接口,那么将不受到单继承的影响,同时可以方便的表示出数据共享的操作;

         · 但是不管使用何种方式,最终一定要通过Thread类的start()方法才可以启动多线程。



3、多线程操作方法

多线程的所有操作方法基本上都在Thread类之中定义的,所以研究方法也就是一个Thread类的翻译,那么下面主要看三组方法。

3.1、线程的命名和取得

         在整个多线程的操作之中,名称是最重要的,因为多线程的运行状态是不确定的,那么系统只能够根据名字来判断出是那一个线程,所以在为线程命名的时候不要有重复,更不要修改。

         在Thread类里面提供了如下的几个方法进行名称操作:

                   · 构造方法:public Thread(Runnable target, String name);

                   · 设置名字:public final void setName(String name);

                   · 取得名字:public final String getName()。

         问题是线程的运行状态不确定,所以要想取得线程的名字,只能够取得当前正在执行的线程。那么在这种情况下如果要想取得当前的线程对象,就必须依靠Thread类的方法:public static Thread currentThread()。

范例:取得线程名字

package cn.mldn.demo;

class MyThread implements Runnable {

    @Override

    public void run() { // 线程的主方法

        System.out.println(Thread.currentThread().getName());

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread() ;

        new Thread(mt).start();

        new Thread(mt).start();

        new Thread(mt,"带名字").start();

    }

}

         发现此时如果设置了线程名称,那么就返回设置的数据。如果没有设置线程名字会自动的进行命名,以保证每一个线程一定有一个名字。

范例:观察如下代码

package cn.mldn.demo;

class MyThread implements Runnable {

    @Override

    public void run() { // 线程的主方法

        System.out.println(Thread.currentThread().getName());

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread() ;

        new Thread(mt,"自定义线程").start();

        mt.run(); // 主方法之中进行方法调用

    }

}

         现在发现通过“mt.run()”执行的时候取得的线程名称是main,所以可以得出结论:主方法也是一个线程,而新的问题就出现了,一直在讨论线程,但是线程依附于进程,进程在那里啊?

         每当用户使用java命令解释一个类的时候,对操作系统而言,都会默认启动一个java的进程,而主方法只是这进程之中的一个线程而已。

问题:每一个JVM运行时至少启动几个线程呢?

         · 至少启动两个线程:主线程、GC垃圾回收线程。

3.2、线程的休眠

         线程的休眠指的是让程序暂缓执行,休眠方法:public static void sleep(long millis) throws InterruptedException;

范例:观察休眠

package cn.mldn.demo;

class MyThread implements Runnable {

    @Override

    public void run() { // 线程的主方法

        for (int x = 0; x < 100; x++) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println(Thread.currentThread().getName() + ",x = " + x);

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread();

        new Thread(mt, "线程A").start();

        new Thread(mt, "线程B").start();

        new Thread(mt, "线程C").start();

        new Thread(mt, "线程D").start();

        new Thread(mt, "线程E").start();

    }

}

         由于线程资源的间隔太短,那么可以简单的理解为:多个线程有可能会同时进入到方法执行。

3.3、线程的优先级

         从理论上讲线程的优先级越高,越有可能先执行。但是这也只是可能,如果要想进行线程的优先级操作,可以使用如下的两个方法:

                  · 设置优先级:public final void setPriority(int newPriority);

                   · 取得优先级:public final int getPriority()。

         对于优先级在Thread类之中定义了三个常量:

                   · 最高优先级:public static final int MAX_PRIORITY,10;

                   · 中等优先级:public static final int NORM_PRIORITY,5;

                   · 最低优先级:public static final int MIN_PRIORITY,1。

范例:观察优先级改变

package cn.mldn.demo;

class MyThread implements Runnable {

    @Override

    public void run() { // 线程的主方法

        for (int x = 0; x < 20; x++) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println(Thread.currentThread().getName() + ",x = " + x);

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread();

        Thread t1 = new Thread(mt, "线程A") ;

        Thread t2 = new Thread(mt, "线程B") ;

        Thread t3 = new Thread(mt, "线程C") ;

        t1.setPriority(Thread.MIN_PRIORITY);

        t3.setPriority(Thread.MAX_PRIORITY);

        t1.start();

        t2.start();

        t3.start();

    }

}

问题:请问主线程的优先级是多少呢?

public class TestDemo {

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getPriority());

    }}

         主线程只是个中等优先级。




4、线程同步与死锁


4.1、线程的同步

         当多个线程访问同一资源的时候一定需要考虑到同步问题,那么下面首先通过一个简单的卖票程序,来观察不同步的情况。

范例:观察问题

package cn.mldn.demo;

class MyThread implements Runnable {

    private int ticket = 8; // 一共8张票

    @Override

    public void run() { // 线程的主方法

        for (int x = 0; x < 20; x++) {

            if (this.ticket > 0) {

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println(Thread.currentThread().getName()

                        + ",ticket = " + this.ticket--);

            }

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread() ;

        new Thread(mt,"票贩子A").start();

        new Thread(mt,"票贩子B").start();

        new Thread(mt,"票贩子C").start();

        new Thread(mt,"票贩子D").start();

        new Thread(mt,"票贩子E").start();

        new Thread(mt,"票贩子F").start();

    }

}

         那么在这个时候就发生了不同步的情况,而发生的原因也很简单。对于整个卖票过程实际上是两步完成的:

· 第一步:使用if语句判断是否有票;

· 第二步:进行票数的修改。

         但是此时有可能会出现这样一种情况:如果说现在只有最后一张票了,可以满足if条件判断,所以线程可以通过if语句拦截,可是在修改票之前出现了一个延迟操作,那么有可能在这个延迟的过程之中,又出现了其它线程进入到方法之中,由于此时没有修改票数,那么这个线程也可以满足if条件判断,后面的线程依次类推,所以当休眠时间一过,进行票数修改的时候,都将在已有的票数上修改,自然就有可能出现负数。

         那么既然已经清楚了问题的产生原因,下面就必须进行问题的解决,问题的解决关键是需要一把锁。如果要想上好责把“锁”,则可以采用两种方式完成:同步代码块、同步方法。

范例:使用同步代码块

         同步代码块是使用synchronized关键字定义的代码块,但是在进行同步的时候一定要设置好一个同步对象,所以这个同步对象一般使用当前对象this表示。

package cn.mldn.demo;

class MyThread implements Runnable {

    private int ticket = 8; // 一共8张票

    @Override

    public void run() { // 线程的主方法

        for (int x = 0; x < 20; x++) {

            synchronized (this) {

                if (this.ticket > 0) {

                    try {

                        Thread.sleep(1000);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                    System.out.println(Thread.currentThread().getName()

                            + ",ticket = " + this.ticket--);

                }

            }

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread();

        new Thread(mt, "票贩子A").start();

        new Thread(mt, "票贩子B").start();

        new Thread(mt, "票贩子C").start();

        new Thread(mt, "票贩子D").start();

        new Thread(mt, "票贩子E").start();

        new Thread(mt, "票贩子F").start();

    }

}

         此时的程序执行速度变得明显缓慢了,因为需要一个个排队进行,就好比你们上网和去银行取钱。一定是上网快。虽然性能变慢了,但是数据的安全性提高了,也就是说异步处理(不加synchronized)和同步处理(使用synchronized)的区别也在于此。

范例:同步方法实现

         如果在一个方法的声明上使用了synchronized关键字,则表示此方法是一个同步方法。

package cn.mldn.demo;

class MyThread implements Runnable {

    private int ticket = 8; // 一共8张票

    @Override

    public void run() { // 线程的主方法

        for (int x = 0; x < 20; x++) {

            this.sale();

        }

    }

    public synchronized void sale() {

        if (this.ticket > 0) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println(Thread.currentThread().getName()

                    + ",ticket = " + this.ticket--);

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        MyThread mt = new MyThread();

        new Thread(mt, "票贩子A").start();

        new Thread(mt, "票贩子B").start();

        new Thread(mt, "票贩子C").start();

        new Thread(mt, "票贩子D").start();

        new Thread(mt, "票贩子E").start();

        new Thread(mt, "票贩子F").start();

    }

}

         同步的核心意义:一个线程要等待另一个线程执行完毕。

4.2、死锁

         死锁实际上是在项目运行过程之中产生的一种问题,所以来讲下面的代码只是为大家演示死锁出现的状况,但是代码本身没有任何研究的意义。

         所谓的死锁就是指的互相等待的情况,下面编写代码观察。

package cn.mldn.demo;

class JinBo {

    public synchronized void tell(XiePengXiang xpx) {

        System.out.println("小金子说:给我5000W,放了你儿子,见钱放人!");

        xpx.get();

    }

    public synchronized void get() {

        System.out.println("小金子得到钱,放了小刁。");

    }

}

class XiePengXiang {

    public synchronized void tell(JinBo j) {

        System.out.println("小谢子说:先放了我儿子,给你5000W,见人给钱!");

        j.get();

    }

    public synchronized void get() {

        System.out.println("小谢子亏了钱,救了儿子。");

    }

}

public class TestDemo implements Runnable {

    private JinBo jin = new JinBo() ;

    private XiePengXiang xpx = new XiePengXiang() ;

    public static void main(String[] args) {

        new TestDemo() ;

    }

    public TestDemo() {

        new Thread(this).start() ;

        jin.tell(xpx);

    }

    @Override

    public void run() {

        xpx.tell(jin);

    }

}

结论:多个线程访问同一资源的时候一定要考虑到线程的同步,但是同步会影响程序的性能,同时会提升数据的安全性。过多的同步(synchronized)会有可能出现死锁。



5、线程经典操作案例:生产者与消费者


本程序的核心结构如下:首先定义两个类,一个是生产者线程,另外一个消费者线程类,生产者每生产完一个数据之后,消费者要取走这些数据,那么假设现在的数据有两种:

                   · title = 小动物,content = 草泥马;

                   · title = 小金子,content = 不是好孩子。

范例:代码基本模型

package cn.mldn.demo;

class Message {

    private String title ;

    private String content ;

    public void setTitle(String title) {

        this.title = title;

    }

    public void setContent(String content) {

        this.content = content;

    }

    public String getContent() {

        return content;

    }

    public String getTitle() {

        return title;

    }

}

class Productor implements Runnable {

    private Message msg ;

    public Productor(Message msg) {

        this.msg = msg ;

    }

    @Override

    public void run() {

        for (int x = 0; x < 100; x++) {

            if (x % 2 == 0) {

                this.msg.setTitle("小动物");

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                this.msg.setContent("草泥马");

            } else {

                this.msg.setTitle("小金子");

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                this.msg.setContent("不是好孩子");

            }

        }

    }

}

class Customer implements Runnable {

    private Message msg ;

    public Customer(Message msg) {

        this.msg = msg ;

    }

    @Override

    public void run() {

        for (int x = 0 ; x < 100 ; x ++){

            try {

                Thread.sleep(100);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println(this.msg.getTitle() + "," + this.msg.getContent());

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        Message msg = new Message() ;

        new Thread(new Productor(msg)).start(); ;

        new Thread(new Customer(msg)).start(); ;

    }

}

         遗憾的是,这个时候出现了两个问题:

                   · 数据错位;

                   · 数据重复取出与重复设置。

5.1、解决数据错位问题

         要想解决数据的错位,使用同步处理即可,所以代码修改如下。

package cn.mldn.demo;

class Message {

    private String title ;

    private String content ;

    public synchronized void set(String title,String content) {

        this.title = title ;

        try {

            Thread.sleep(100);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        this.content = content ;

    }

    public synchronized void get() {

        try {

            Thread.sleep(10);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println(this.title + "," + this.content);

    }

}

class Productor implements Runnable {

    private Message msg ;

    public Productor(Message msg) {

        this.msg = msg ;

    }

    @Override

    public void run() {

        for (int x = 0; x < 100; x++) {

            if (x % 2 == 0) {

                this.msg.set("小动物","草泥马");

            } else {

                this.msg.set("小金子","不是好孩子");

            }

        }

    }

}

class Customer implements Runnable {

    private Message msg ;

    public Customer(Message msg) {

        this.msg = msg ;

    }

    @Override

    public void run() {

        for (int x = 0 ; x < 100 ; x ++){

            this.msg.get();

        }

    }

}

public class TestDemo {

    public static void main(String[] args) {

        Message msg = new Message() ;

        new Thread(new Productor(msg)).start(); ;

        new Thread(new Customer(msg)).start(); ;

    }

}

         现在的数据没有任何错乱,但是重复的操作更严重了。

5.2、解决数据重复操作

         如果要想解决重复的操作必须加入等待与唤醒机制。而这样的处理机制是由Object类提供的方法支持,在Object类之中有如下的方法可以使用:

                   · 等待:public final void wait() throws InterruptedException;

                   · 唤醒第一个:public final void notify();

                   · 唤醒全部:public final void notifyAll(),谁的优先级高就先执行。

范例:解决问题

class Message {

    private String title ;

    private String content ;

    private boolean flag = true ;

    // flag = true:表示可以生产,但是不能取走

    // flag = false:表示可以取走,但是不能生产

    public synchronized void set(String title,String content) {

        if (this.flag == false) {   // 不能生产

            try {

                super.wait();

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

        this.title = title ;

        try {

            Thread.sleep(100);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        this.content = content ;

        this.flag = false ;

        super.notify();

    }

    public synchronized void get() {

        if (this.flag == true) {

            try {

                super.wait();

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

        try {

            Thread.sleep(10);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println(this.title + "," + this.content);

        this.flag = true ;

        super.notify();

    }

}

         此部分的代码没有一个绝对的掌握标准,会了总是好的,如果觉得之前的概念已经没问题了,可以好好看看。

面试题:请解释sleep()和wait()的区别?

         · sleep()是Thread类定义的方法,在休眠之后可以自动唤醒;

         · wait()是Object类定义的方法,等待之后必须使用notify()或notifyAll()手工唤醒。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值