美文网首页多线程
Java中多线程的概述、实现方式、线程控制、生命周期、多线程程序

Java中多线程的概述、实现方式、线程控制、生命周期、多线程程序

作者: 清风沐沐 | 来源:发表于2016-11-01 22:31 被阅读81次

    多线程的概述

    • 进程
      • 正在运行的程序,是系统进行资源分配和调用的独立单位。
      • 每一个进程都有它自己的内存空间和系统资源。

    说起线程,它又分为单线程和多线程

    • 线程
    • 是进程中的单个顺序控制流,是一条执行路径
    • 一个进程如果只有一条执行路径,则称为单线程程序
    • 一个进程如果有多条执行路径,则称为多线程程序

    多线程的实现(1)

    如何实现多线程的程序呢?

    • 由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。

    • 方式1:继承Thread类

      • 步骤
        A:自定义类MyThread继承Thread类。
        B:MyThread类里面重写run()
        C:创建对象
        D:启动线程

    下面我们就自定义一个MyThread类继承Thread类启动线程

    public class MyThread extends Thread {
    
        @Override
        public void run() {
            // 自己写代码
            // 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环改进
            for (int x = 0; x < 100; x++) {
                System.out.println(x);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
    
    
            // 创建两个线程对象
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
    
            my1.start();
            my2.start();
        }
    }
    

    这样我们就创建并启动了两个线程
    start()方法:首先启动了线程,然后再由jvm去调用该线程的run()方法。
    那么,我们在继承Thread类之后,为什么要重写run()方法呢?

    • 因为不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。

    获取和设置线程名称

    • Thread类的基本获取和设置方法
      • public final String getName():获取线程的名称。
      • public final void setName(String name):设置线程的名称
    public class MyThread extends Thread {
    
        public MyThread() {
        }
    
        public MyThread(String name){
            super(name);
        }
    
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
            // 创建线程对象
            //无参构造+setXxx()
             MyThread my1 = new MyThread();
             MyThread my2 = new MyThread();
             //调用方法设置名称
             my1.setName("阿杜");
             my2.setName("杜鹏程");
             my1.start();
             my2.start();
    
            //带参构造方法给线程起名字
            // MyThread my1 = new MyThread("阿杜");
            // MyThread my2 = new MyThread("杜鹏程");
            // my1.start();
            // my2.start();
    
            //我们可以使用无参构造的方法,也可以使用带参构造的方法
        }
    }
    

    但是我们要获取main方法所在的线程对象的名称,该怎么办呢?
    遇到这种情况,Thread类提供了一个很好玩的方法:
    public static Thread currentThread():返回当前正在执行的线程对象
    System.out.println(Thread.currentThread().getName());
    这句话如果在main中执行,就会输出main。会返回当前执行的线程对象

    线程控制

    • public static void sleep(long millis):线程休眠
    • public final void join():线程加入
    • public static void yield():线程礼让
    • public final void setDaemon(boolean on):后台线程
    • public final void stop():中断线程
    • public void interrupt():中断线程

    public static void sleep(long millis):线程休眠

    public class ThreadSleep extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x + ",日期:" + new Date());
                // 睡眠1秒钟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class ThreadSleepDemo {
        public static void main(String[] args) {
            ThreadSleep ts1 = new ThreadSleep();
            ThreadSleep ts2 = new ThreadSleep();
    
            ts1.setName("阿杜");
            ts2.setName("杜鹏程");
    
            ts1.start();
            ts2.start();
        }
    }
    

    public final void join():线程加入,等待该线程终止

    public class ThreadJoin extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class ThreadJoinDemo {
        public static void main(String[] args) {
            ThreadJoin tj1 = new ThreadJoin();
            ThreadJoin tj2 = new ThreadJoin();
            ThreadJoin tj3 = new ThreadJoin();
    
            tj1.setName("中秋节");
            tj2.setName("国庆节");
            tj3.setName("圣诞节");
    
            tj1.start();
            try {
                tj1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            tj2.start();
            tj3.start();
        }
    }
    

    运行程序,我们发现名字为中秋节的线程走完了之后才开始走下面的两个线程。
    给那个线程用这个方法就是等待该线程终止后,再继续执行接下来的线程。

    public static void yield():线程礼让

    public class ThreadYield extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
                Thread.yield();
            }
        }
    }
    public class ThreadYieldDemo {
        public static void main(String[] args) {
            ThreadYield ty1 = new ThreadYield();
            ThreadYield ty2 = new ThreadYield();
    
            ty1.setName("阿杜");
            ty2.setName("杜鹏程");
    
            ty1.start();
            ty2.start();
        }
    }
    

    这个方法暂停当前正在执行的线程对象,并执行其他线程。
    让多个线程的执行更和谐,但是不能靠它保证一人一次。

    public final void setDaemon(boolean on):守护线程

    public class ThreadDaemon extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class ThreadDaemonDemo {
        public static void main(String[] args) {
            ThreadDaemon td1 = new ThreadDaemon();
            ThreadDaemon td2 = new ThreadDaemon();
    
            td1.setName("关羽");
            td2.setName("张飞");
    
            // 设置守护线程
            td1.setDaemon(true);
            td2.setDaemon(true);
    
            td1.start();
            td2.start();
    
            Thread.currentThread().setName("刘备");
            for (int x = 0; x < 5; x++) {
                System.out.println(Thread.currentThread().getName() + ":" + x);
            }
        }
    }
    

    运行程序可以看到,当刘备执行完5次后,张飞和关于也会执行完,并不会执行100次。
    将该线程标记为守护线程或用户线程。
    当正在运行的线程都是守护线程时,Java 虚拟机退出。
    该方法必须在启动线程前调用。

    **public final void stop():中断线程 **
    public void interrupt():中断线程

    这两个方法都是中断线程的意思,但是他们还是有区别的,我们来一起研究一下

    public class ThreadStop extends Thread {
        @Override
        public void run() {
            System.out.println("开始执行:" + new Date());
    
            // 我要休息10秒钟,亲,不要打扰我哦
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                // e.printStackTrace();
                System.out.println("线程被终止了");
            }
    
            System.out.println("结束执行:" + new Date());
        }
    }
    
    public class ThreadStopDemo {
        public static void main(String[] args) {
            ThreadStop ts = new ThreadStop();
            ts.start();
    
            // 你超过三秒不醒过来,我就干死你
            try {
                Thread.sleep(3000);
    //           ts.stop();
                ts.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    我们分别运行stop()方法和interrupt()方法。
    我们可以发现stop()方法执行后,该线程就停止了,不再继续执行了
    但是interrupt()方法执行后,它会终止线程的状态,还会继续执行run方法里面的代码。

    线程的生命周期图

    线程的生命周期图.png

    多线程的实现(2)

    • 方式2:实现Runnable接口
      • 步骤:
        • A:自定义类MyRunnable实现Runnable接口
        • B:重写run()方法
        • C:创建MyRunnable类的对象
        • D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
    public class MyRunnable implements Runnable {
    
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
                System.out.println(Thread.currentThread().getName() + ":" + x);
            }
        }
    }
    public class MyRunnableDemo {
        public static void main(String[] args) {
            // 创建MyRunnable类的对象
            MyRunnable my = new MyRunnable();
    
            // 创建Thread类的对象,并把C步骤的对象作为构造参数传递
    
            // Thread(Runnable target, String name)
            Thread t1 = new Thread(my, "阿杜");
            Thread t2 = new Thread(my, "杜鹏程");
    
            t1.start();
            t2.start();
        }
    }
    

    这样我们就实现了多线程的第二种启动方式

    多线程程序练习

    某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

    我们分别用两种实现多线程的方法来完成这个需求
    1.继承Thread类来实现

    public class SellTicket extends Thread {
    
        // 定义100张票
        private static int tickets = 100;
    
        @Override
        public void run() {
            // 定义100张票
            // 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
            // int tickets = 100;
    
            // 是为了模拟一直有票
            while (true) {
                if (tickets > 0) {
                    System.out.println(getName() + "正在出售第" +(tickets--) + "张票");
                }
            }
        }
    }
    public class SellTicketDemo {
        public static void main(String[] args) {
            // 创建三个线程对象
            SellTicket st1 = new SellTicket();
            SellTicket st2 = new SellTicket();
            SellTicket st3 = new SellTicket();
    
            // 给线程对象起名字
            st1.setName("窗口1");
            st2.setName("窗口2");
            st3.setName("窗口3");
    
            // 启动线程
            st1.start();
            st2.start();
            st3.start();
        }
    }
    

    这样我们就实现了三个窗口同时在出售这100张票的多线程程序

    2.实现Runnable接口的方式实现

    public class SellTicket implements Runnable {
        // 定义100张票
        private int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");
                }
            }
        }
    }
    
    public class SellTicketDemo {
        public static void main(String[] args) {
            // 创建资源对象
            SellTicket st = new SellTicket();
    
            // 创建三个线程对象
            Thread t1 = new Thread(st, "窗口1");
            Thread t2 = new Thread(st, "窗口2");
            Thread t3 = new Thread(st, "窗口3");
    
            // 启动线程
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    我们这个电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟,所以我们每次卖票延迟100毫秒

    while (true) {
                if (tickets > 0) {
                    // 为了模拟更真实的场景,我们稍作休息
                    try {
                        Thread.sleep(100); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");
                }
            }
        }
    

    这样我们模拟真是场景,稍作休息了,可是运行程序后,还是会出现下面两个问题。

    • 相同的票出现多次
      - CPU的一次操作必须是原子性的
    • 还出现了负数的票
      - 随机性和延迟导致的

    这里就牵扯到了线程的安全问题,线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。

    多线程安全问题

    如何解决多线程安全问题呢?

    • 把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

    解决线程安全问题实现(1)

    • 同步代码块
      • 格式:
        • synchronized(对象){ 需要同步的代码; }
      • 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

    我们多上面售票的代码进行改进

    public class SellTicket implements Runnable {
        // 定义100张票
        private int tickets = 100;
        //创建锁对象
        private Object obj = new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized (obj) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票");
                    }
                }
            }
        }
    }
    

    我们只要运用同步代码块的格式来解决线程的问题就可以,主要就是这里的对象,必须使用的是同一个锁对象。
    所以我们可以来总结一下同步的特点

    同步的特点

    • 同步的前提
      • 多个线程
      • 多个线程使用的是同一个锁对象
    • 同步的好处
      • 同步的出现解决了多线程的安全问题。
    • 同步的弊端
      • 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

    解决线程安全问题实现(2)

    我们 还有一种方法可以解决多线程的安全问题
    同步方法:就是把同步的关键字加到方法上

    private synchronized void sellTicket() {
                if (tickets > 0) {
                try {
                        Thread.sleep(100);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票 ");
                }
        }
    

    我们只要调用这个方法就可以了
    我们也可以让此方法为静态的方法

    private static synchronized void sellTicket() {
            if (tickets > 0) {
            try {
                    Thread.sleep(100);
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                        + "正在出售第" + (tickets--) + "张票 ");
            }
        }
    

    我们要来总结一下,同步代码块的锁对象可以时任意对象。
    但是,当把同步关键字加在方法上,它的对象是this
    当此方法为精态方法时,它的对象是类的字节码文件对象,也就是 类名.class

    相关文章

      网友评论

        本文标题:Java中多线程的概述、实现方式、线程控制、生命周期、多线程程序

        本文链接:https://www.haomeiwen.com/subject/vctvuttx.html