《Java高并发编程详解》前三章之认识线程、深入理解线程、线程API

继承 Thread类和实现Runnable接口的区别
  • 继承Thread类时,只能创建不同的类,线程类间的资源不可共享,而实现Runnable后,可以共享线程资源。

一个号码机有四个出票口,总共只能出10张票,现在用java线程实现。

public class NumberMachine {
    public static void main(String[] args) {
        TicketWindow t1 = new TicketWindow("1号", "1号启动");
        TicketWindow t2 = new TicketWindow("2号", "2号启动");
        TicketWindow t3 = new TicketWindow("3号", "3号启动");
        TicketWindow t4 = new TicketWindow("4号", "3号启动");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class TicketWindow extends Thread {
    private static final int MAX = 10;
    //注意这里的int类型不是static的
    private int index = 1;
    private String name;

    public TicketWindow(String name, String msg) {
        this.name = name;
        System.out.println(msg);
    }

    @Override
    public void run() {
        while (index <= MAX) {
            System.out.println(name + ": 您叫到的号码是 " + (index++));
            try {
                TimeUnit.MICROSECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上面代码中的index变量不是static的,因此运行达不到想要的结果。每个出口都能输出1到10是个数字。

1号启动
2号启动
3号启动
3号启动
1号: 您叫到的号码是 1
2号: 您叫到的号码是 1
3号: 您叫到的号码是 1
4号: 您叫到的号码是 1
1号: 您叫到的号码是 2
4号: 您叫到的号码是 2
3号: 您叫到的号码是 2
2号: 您叫到的号码是 2
3号: 您叫到的号码是 3
1号: 您叫到的号码是 3
2号: 您叫到的号码是 3
4号: 您叫到的号码是 3
3号: 您叫到的号码是 4
4号: 您叫到的号码是 4
1号: 您叫到的号码是 4
2号: 您叫到的号码是 4
3号: 您叫到的号码是 5
4号: 您叫到的号码是 5
1号: 您叫到的号码是 5
2号: 您叫到的号码是 5
3号: 您叫到的号码是 6
4号: 您叫到的号码是 6
1号: 您叫到的号码是 6
2号: 您叫到的号码是 6
3号: 您叫到的号码是 7
2号: 您叫到的号码是 7
1号: 您叫到的号码是 7
4号: 您叫到的号码是 7
3号: 您叫到的号码是 8
4号: 您叫到的号码是 8
1号: 您叫到的号码是 8
2号: 您叫到的号码是 8
4号: 您叫到的号码是 9
1号: 您叫到的号码是 9
3号: 您叫到的号码是 9
2号: 您叫到的号码是 9
4号: 您叫到的号码是 10
2号: 您叫到的号码是 10
3号: 您叫到的号码是 10
1号: 您叫到的号码是 10

现在用实现Runnable接口的类来实现

package topic1;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Knox
 * @Date: 2018/11/22 11:17 AM
 * @Description: You Know
 * @Version 1.0
 */
public class NumberMachine {
    public static void main(String[] args) {
//        TicketWindow t1 = new TicketWindow("1号", "1号启动");
//        TicketWindow t2 = new TicketWindow("2号", "2号启动");
//        TicketWindow t3 = new TicketWindow("3号", "3号启动");
//        TicketWindow t4 = new TicketWindow("4号", "3号启动");
//        t1.start();
//        t2.start();
//        t3.start();
//        t4.start();

        TicketWindow rw = new TicketWindow("票口", "票口");
        new Thread(rw, "1号").start();
        new Thread(rw, "2号").start();
        new Thread(rw, "3号").start();
        new Thread(rw, "4号").start();


    }
}

class TicketWindow implements Runnable {
    private static final int MAX = 50000;
    //注意这里的int类型不是static的
    private int index = 1;
    private String name;

    public TicketWindow(String name, String msg) {
        this.name = name;
        System.out.println(msg);
    }

    @Override
    public void run() {

        while (true) {
            synchronized (this) {
                if (index > MAX)
                    break;
                System.out.println(Thread.currentThread().getName() + ": 您叫到的号码是 " + (index++));
            }
        }
    }
}

注意上面的的代码我加了同步(synchronized),如果不加同步的话,会发省生并发异常,导致结果错误。

守护线程
  • 如果设置一个线程是守护线程,则在Jvm进程中所有非守护线程结束生命周期后,所有守护线程都结束生命周期, 所以守护线程一般用于后台执行的任务,比如当我关闭游戏客户端的时候,我希望所有数据传输线程都结束,那么我就可以把数据传输相关的线程设置为守护线程。
  • java用setDeamon(true)将线程设置为守护线程只能发生在start之前

java代码

public class DeamonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("working...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        System.out.println("main线程结束生命周期。");

    }
}
yield

Thread 的 yield是一个静态方法,文档上是说当一个线程调用yield方法时,该线程将倾向于让出CPU和其他资源,自己从RUNNING状态转化为RUNNABLE状态,但是到底让不让出资源,还是CPU调度说了算。

以下代码如果诸事掉Thread里面的条件判断,则有时打印0,1,有时打印1,0。若把注释去掉,更倾向打印0,1(感觉没啥用)

public class Yield {

    public static void main(String[] args) {
        IntStream.range(0, 2).mapToObj(Yield::create).forEach(Thread::start);
    }

    private static Thread create(int index) {
        return new Thread(() -> {
            //if (index == 0)
            //   Thread.yield();
            System.out.println(index);
        });
    }
}
priority

我发现设置优先级对线程的执行并没有可见的影响,比如下面的代码,我设置t1的优先级小于t2,但是在运行的时候并没有很大的影响,甚至,t1的打印次数有时会t2多。
setPriority对CPU也是一种hint,如果CPU忽略了它,那这个优先级就没用了。

public class Priority {
    private static int index = 1;
    private static int max = 100;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            printT("t1");
        });
        Thread t2 = new Thread(() -> {
            printT("t2");
        });
        t1.setPriority(1);
        t2.setPriority(10);
        t2.start();
        t1.start();
    }
    private static void printT(String T) {
        while (index <= max) {
            synchronized (Thread.class) {
                if (index > max) break;
                System.out.println(index + "\t" + T);
                index++;
            }
        }
    }
}
  • 线程的优先级不大于它所在线程组的优先级,如果设置优先级大于线程组优先级,将会设置成线程组优先级。
interrupt()
  • 当线程正在执行可被中断的方法时,会尝试捕获InterruptedException,在执行该方法时,一但被其他线程执行该线程的interrupt方法,就会跑出异常。
  • 如果该线程没有执行可被中断的线程或者线程已经死亡,则线程会忽略该中断。
public class Interrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("Fuck! I am be interrupt!");
            }
        });
        t1.start();
        TimeUnit.MICROSECONDS.sleep(4);
        t1.interrupt();
    }
}
isInterrupted()

判断线程是否被打断过,如果之前执行了interrupt方法,则返回True。该方法不会对interrupt状态做修改。

interrupted()

判断线程是否被打断过,返回True或False,同时重置interrupt。

join()

在一个线程中执行另一个线程的join()方法,此时,本线程将会进入BLOCKED状态,直到另一个线程执行结束后,才会继续执行本线程。如下代码。

public class Join {
    
    public static void main(String[] args) throws InterruptedException {
        List<Thread> threads = IntStream.range(0, 2).mapToObj(Join::create).collect(Collectors.toList());
        threads.forEach(Thread::start);
        for (Thread thread : threads) {
            thread.join();
        }
        for (int k = 0; k < 20; k++) {
            System.out.println("线程 main : " + k);
        }
    }

    public static Thread create(int i) {
        return new Thread(() -> {
            for (int k = 0; k < 20; k++) {
                System.out.println("线程" + i + " : " + k);
            }
        });
    }
}

上面这段代码,当线程0和线程1完全打印结束后,才会打印main线程。

join是个很有用的方法,比如Task任务的拓扑处理,先执行某些线程之后才能执行此线程。现实生活中有很多应用场景。