美文网首页
Java多线程编程(1)基础知识

Java多线程编程(1)基础知识

作者: 景知育德 | 来源:发表于2022-11-23 21:17 被阅读0次

    先说一下我最朴素的理解,进程是应用程序的实例,进程之间的通信代价比较高;而线程就要更加轻量化,可以方便地完成相互之间的通信。

    线程的创建

    在Java中,线程也是一个类,是一个抽象类,Thread。可以简单地通过new Thread()来创建一个线程对象,但是要重写其run()方法。

            Thread thread = new Thread() {
                @Override
                public void run() {
                    // ...
                }
            };
    

    在合适版本的Java中,可以用Lambda表达式。

            Thread thread = new Thread(() -> {
                // ...
            });
    

    像这样,就创建了一个线程对象thread

    除了用Thread抽象类来创建之外,还可以用Runnable接口。
    如果自己建一个类,继承于Thread,用static属性来保存公共数据,这样就把线程的控制和业务逻辑混合在一起了。

    public class Study1 {
        public static void main(String[] args) {
            int size = 4;
            String[] chineseNums = {"甲", "乙", "丙", "丁"};
            TicketWindow[] ticketWindows = new TicketWindow[size];
            for (int i = 0; i < size; i++) {
                ticketWindows[i] = new TicketWindow("窗口" + chineseNums[i]);
            }
            for (int i = 0; i < size; i++) {
                ticketWindows[i].start();
            }
            System.out.println("启动各柜台完毕!");
        }
    }
    
    class TicketWindow extends Thread {
        private final String name;
        private static final int MAX = 50;
        private static int index = 1; // 注意static
    
        public TicketWindow(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            while (index <= MAX) {
                System.out.println(name + ":" + (index++));
            }
        }
    }
    

    Runnable的对象可以作为Thread的参数,这样有一个好处,就是共享数据。可以把一个Runnable对象作为多个Thread的参数,那实际上这些线程都共享了这个Runnable对象的数据。

    public class Study2 {
        public static void main(String[] args) {
            int size = 4;
            String[] chineseNums = {"甲", "乙", "丙", "丁"};
            TicketWindowRunnable runnable = new TicketWindowRunnable();
            Thread[] threads = new Thread[size];
            for (int i = 0; i < size; i++) {
                threads[i] = new Thread(runnable, "窗口" + chineseNums[i]);
            }
            for (int i = 0; i < size; i++) {
                threads[i].start();
            }
            System.out.println("启动各柜台完毕!");
        }
    }
    
    /**
     * 改进之后,不再使用static的index
     */
    class TicketWindowRunnable implements Runnable {
        private static final int MAX = 50;
        private int index = 1; // 不需要static
    
        @Override
        public void run() {
            while (index <= MAX) {
                System.out.println(Thread.currentThread().getName() + ":" + (index++));
            }
        }
    }
    

    理想的情况下,上述代码会让甲乙丙丁四个窗口,发放1~50的票据。这里的index不需要静态,因为它是TicketWindowRunnable对象的一个属性,是唯一的。

    不得不指出,上述代码不是线程安全的,会有号码重复、遗漏,甚至会有号码溢出。

    线程一定是由另一个线程创建的,那个创建它的线程称为“父线程”。如果没有显式地指定Group,那么新创建的子线程会和父线程在同一个Group之中,拥有相同的优先级,相同的daemon

    wait

    wait一般放在循环体里,含义是一直等,直到等待到满足某一条件为止。

    例如

        synchronized void setProductId(int id) {
            // 一直等 等到writeable为真为止
            while (!writeable) {
                try {
                    wait();
                } catch (InterruptedException ignored) {
    
                }
            }
            productId = id;
            writeable = false;
            notify();
        }
    
        synchronized int getProductId() {
            // 一直等 等到writeable为假为止
            while (writeable) {
                try {
                    wait();
                } catch (InterruptedException ignored) {
    
                }
            }
            writeable = true;
            notify();
            return productId;
        }
    

    wait让当前线程处于阻塞状态,直到其它的线程notify或notifyAll,这个时候才会尝试接触阻塞。是工程上常用notifyAll。

    这方面可以参考
    java wait()方法用法详解

    守护线程

    守护线程(Daemon Thread)会自动退出,这一自动退出的时机就是其它非守护线程都退出了,它就会自动退出。

    比如一辆大巴车,上面的乘客都是普通线程(非守护线程),而公交车司机是守护线程。当乘客全部下车之后,司机就会自动下车。这样的线程就是守护线程。

    Java中的垃圾回收线程,就是典型的守护线程。

    Thread API

    sleep

    让当前线程暂停,进入休眠,可以设定休眠时间。休眠时,不会放弃monitor锁的所有权。

    在JDK1.5之后,可以用TimeUnit代替Thread.sleep。

    例如

     TimeUnit.SECONDS.sleep(1);
    

    即休眠1秒。TimeUnit有单位,而sleep只有毫秒和纳秒,不太方便。

    yield

    提醒调度器我愿意放弃当前的CPU资源,如果CPU资源不紧张,则会忽略这种提醒。

    interrupt

    使用wait、sleep、join等方法会让线程进入阻塞状态。如果这个时候另外一个线程来打断它(使用interrupt方法),就会打断这种阻塞。

    打断一个线程并不意味着这个线程生命周期的结束,而是仅仅打断了阻塞状态。被打断了之后,会抛出一个InterruptedException的异常。

    如果一个线程已经是死亡状态,则任何打断都会被忽略。

            thread.start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println(thread.isInterrupted()); // false
            thread.interrupt();
            System.out.println(thread.isInterrupted()); // true
    

    以上代码所示,在调用interrupt()打断之前,输出为false,之后为true。

    如果我们在thread之中加入了捕获异常的内容。

            Thread thread = new Thread(() -> {
                while (true) {
                    try {
                        TimeUnit.MINUTES.sleep(10);
                    } catch (InterruptedException e) {
                        System.out.println("A");
                    }
                }
            });
            thread.start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println(thread.isInterrupted()); // false
            thread.interrupt();
            TimeUnit.SECONDS.sleep(1);
            System.out.println(thread.isInterrupted()); // false
    

    那么两次得到的结果就都是false了。因为sleep这个可中断方法,在捕捉到中断信号之后,会擦除interrupt标识。

    isInterrupted仅用于判断是否被打断,不会擦除标识。
    interrupted用于判断是否被打断,之后再擦除interrupt标识。

    join

    在a线程的代码里调用b.join(),表示a开始等b,直到b线程结束生命周期。

        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            });
            thread.start();
            thread.join(); // 注意
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    

    会先输出Thread-0 0 Thread-0 1 ……,全部输出完毕后,再输出main 0……

    如果把// 注意所在行的代码删除,那么就会是Thread-0和main交替输出。

    例子

    现在欲用n个线程读取m个API。我们创建并start了各个线程之后,都需要join,等待这n个线程都结束,才汇总数据。

    优先级

    setPriority()getPriority

    优先级介于1(含)和10(含)之间,而且设置的优先级不能大于线程所在group的优先级。如果给线程设定大于group优先级的优先级,则会被group的最大优先级取而代之。

    相关文章

      网友评论

          本文标题:Java多线程编程(1)基础知识

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