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

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

线程的创建

在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交替输出。

例子

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

优先级

setPriority()getPriority

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

你可能感兴趣的:(Java多线程编程(1)基础知识)