u08-线程基础

294 阅读7分钟

1. 进程与线程

概念:

  • 进程:OS进行分配和管理资源的基本单位。
    • 启动一个程序至少启动了一个进程。
    • 每启动一个进程,OS都会为其分配独立的数据空间,建立数据表来维护代码段、堆栈段和数据段。
    • 进程间切换开销大。
  • 线程:进程的一条执行路径,是CPU调度和分派的基本单位,也被称为轻量级进程。
    • 启动一个进程至少启动了一个线程。
    • 线程有自己的堆栈和局部变量,但没有独立的数据空间,但它们共享所在进程的代码和数据。
    • 线程间切换开销小。
  • 线程切换:CPU的工作就是从内存中将指令一条一条拿过来执行,如果没有指令,CPU就处于空闲状态,多线程模式下,每个线程都要争抢CPU,每个线程在CPU执行一会儿,执行完了,需要让出CPU资源,这个过程叫做线程切换。

1.1 前台线程

概念: 前台线程中,先执行主线程,然后JVM随机分配时间片,最后子线程交替运行,基本创建方式有两种:

  • 继承Thread类方式创建线程,方式比较单一,因为java只支持单继承。
    • 可以使用匿名内部类进行优化。
  • 实现Runnable接口方式创建线程,比较灵活,因为java支持多实现。
    • 可以使用匿名内部类进行优化。
    • 可以使用lambda表达式进行优化。
  • 方法:void start():手动启动线程,并不一定立刻执行,等JVM随机分配时间片。

源码: /javase-advanced/

  • src: c.y.thread.start.ForegroundThreadTest.java
/**
 * @author yap
 */
public class ForegroundThreadTest {

    private static class SubThread extends Thread {
        @Override
        public void run() {
            for (int i = 0, j = 10; i < j; i++) {
                System.out.println(i);
            }
        }
    }

    private static class SubRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0, j = 10; i < j; i++) {
                System.out.println(i);
            }
        }
    }

    @Test
    public void buildByThread() {
        SubThread thread = new SubThread();
        thread.start();
    }

    @Test
    public void buildByInnerThread() {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0, j = 10; i < j; i++) {
                    System.out.println(i);
                }
            }
        }.start();
    }

    @Test
    public void buildByRunnable() {
        SubRunnable subRunnable = new SubRunnable();
        Thread thread = new Thread(subRunnable);
        thread.start();
    }

    @Test
    public void buildByInnerRunnable() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0, j = 10; i < j; i++) {
                    System.out.println(i);
                }
            }
        });
        thread.start();
    }

    @Test
    public void buildByLambda() {
        new Thread(() -> {
            for (int i = 0, j = 10; i < j; i++) {
                System.out.println(i);
            }
        }).start();

    }

    @After
    public void after() throws IOException {
        System.out.println(System.in.read());
    }
}

1.2 后台线程

概念: 后台线程也叫守护线程,后台线程是依赖前台线程的,如果所有的前台线程都死了,那么后台线程自动退出(JVM就退出了),只要有一个前台线程活动,那么后台线程就活动。

  • 方法:void setDaemon(boolean on):是否将线程设置为后台线程,默认false。

源码: /javase-advanced/

  • src: c.y.thread.start.DaemonThreadTest.java
/**
 * @author yap
 */
public class DaemonThreadTest {

    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("daemon-thread...");
            }
        });

        // must before start()
        daemonThread.setDaemon(true);
        daemonThread.start();

        for (int i = 0, j = 30; i < j; i++) {
            System.out.println(i);
        }
    }
}

2. Thread常用API

2.1 线程状态

概念: 以下是和线程的信息,状态相关的API方法:

  • boolean isAlive():测试线程是否还活着。
  • static Thread currentThread():返回当前正在执行的线程对象的引用。
  • void setName(String name):设置该线程的名字,也可以在Thread构造时指定。
  • String getName():返回该线程的名称,默认以 thread- 为前缀。
  • void setPriority(int newPriority):设置线程优先级,尽量使用Thread优先级常量。
  • int getPriority():获取线程优先级,默认是5。

源码: /javase-advanced/

  • src: c.y.thread.start.ThreadApiTest.baseApi()
/**
 * @author yap
 */
public class ThreadApiTest {

    @Test
    public void baseApi() {
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread());
        }, "threadA");
        thread.setName("threadB");
        thread.setPriority(Thread.MAX_PRIORITY);
        System.out.println(thread.getPriority());
        System.out.println(thread.getName());
        System.out.println(thread.isAlive());
        thread.start();
        System.out.println(thread.isAlive());
    }

    @After
    public void after() throws IOException {
        System.out.println(System.in.read());
    }
}

2.2 线程睡眠

概念: sleep() 会阻塞当前线程,导致线程进入TIME_WAITING状态。

  • 方法:
    • static void sleep(long millis):Thread类中的静态方法,参数单位毫秒。
    • TimeUnit.SECONDS.sleep(1L):TimeUnit工具类中提供的sleep()的上层封装。
  • sleep()之后线程会回到就绪状态。

源码: /javase-advanced/

  • src: c.y.thread.start.ThreadApiTest.sleep()
/**
 * @author yap
 */
public class ThreadApiTest {

    @Test
    public void sleep() throws InterruptedException {
        System.out.println("thread-main: start...");
        Thread.sleep(2000L);
        System.out.println("thread-main: over...");

        System.out.println("thread-main: start...");
        TimeUnit.SECONDS.sleep(2L);
        System.out.println("thread-main: over...");
    }

    @After
    public void after() throws IOException {
        System.out.println(System.in.read());
    }
}

2.3 线程插队

概念: join() 一般用于临时加入到另一个线程中插队执行内容。

  • 方法:void join():写在哪个线程中,就插队哪个线程。
  • join() 方法如果写在 start() 之前,不报错,但是没有插队效果的。

源码: /javase-advanced/

  • src: c.y.thread.start.ThreadApiTest.join()
/**
 * @author yap
 */
public class ThreadApiTest {

    @Test
    public void join() throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0, j = 5; i < j; i++) {
                System.out.println(Thread.currentThread().getName());
            }
        };

        Thread threadA = new Thread(runnable, "threadA");
        Thread threadB = new Thread(runnable, "threadB");

        threadA.start();
        threadA.join();

        threadB.start();

        for (int i = 0, j = 5; i < j; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    @Test
    public void yeild() {

        new Thread(() -> {
            for (int i = 0, j = 5; i < j; i++) {
                Thread.yield();
                System.out.println(Thread.currentThread().getName());
            }
        }, "thread").start();

        for (int i = 0, j = 5; i < j; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    @After
    public void after() throws IOException {
        System.out.println(System.in.read());
    }
}

2.4 线程让步

概念: yeild() 用于让出一个时间片,效果不明显,就比如食堂排队打饭,我调用 join() 方法可以插队买饭,而我调用 yield() 方法是让给你一次机会,让你排在我的前面买饭。

  • 方法:static void yeild():让出被线程调度器选中的机会,即让出一次CPU资源。

源码: /javase-advanced/

  • src: c.y.thread.start.ThreadApiTest.yield()
/**
 * @author yap
 */
public class ThreadApiTest {

    @Test
    public void yeild() {

        new Thread(() -> {
            for (int i = 0, j = 5; i < j; i++) {
                Thread.yield();
                System.out.println(Thread.currentThread().getName());
            }
        }, "thread").start();

        for (int i = 0, j = 5; i < j; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    @After
    public void after() throws IOException {
        System.out.println(System.in.read());
    }
}

3. 线程生命周期

概念: 生命周期就是一个对象由生到死的过程,线程的生命周期分为六部分:

  • 初始(NEW):新创建了一个线程对象,调用start()后可以进入RUNNABLE状态。
  • 可运行(RUNNABLE):处于RUNNABLE状态的线程才有资格被线程调度器选中:
    • READY是就绪状态,如果被线程调度器选中执行,则进入RUNNING状态。
    • 如果由RUNNING状态退回到READY状态,则称线程被挂起。
  • 被阻塞(BLOCKED):
    • RUNNABLE状态的线程等待进入同步代码块的锁时的状态就是BLOCKED状态。
    • BLOCKED状态只有在线程获取了同步代码块的锁之后才能解除。
  • 等待(WAITING):
    • RUNNABLE状态的线程被调用了 wait()join()LockSupport.park() 后就处于WAITING状态。
    • 可以使用对应的 notify()notifyAll()LockSupport.unpark() 来解除一个线程的WAITING状态。
  • 计时等待(TIME_WAITING):
    • RUNNABLE状态的线程被调用了 sleep(t),wait(t)join(t)LockSupport.parkNanos(),,LockSupport.parkUntil() 后就处于TIME_WAITING状态。
    • TIME_WAITING可以在指定的时间内自行解除。
  • 终止(TERMINATED):表示该线程已经执行完毕。

源码: /javase-advanced/

  • src: c.y.thread.start.ThreadStateTest.java
/**
 * @author yap
 */
public class ThreadStateTest {
    /**
     * Blocked state is not convenient to test
     */
    @Test
    public void state() throws IOException, InterruptedException {

        Thread thread = new Thread(() -> {
            System.out.println("hello!");
        });
        System.out.println(thread.getState());

        thread.start();
        System.out.println(thread.getState());

        thread.join();
        System.out.println(thread.getState());
    }

    @After
    public void after() throws IOException {
        System.out.println(System.in.read());
    }
}

4. 线程调度类

概念: java.util.Timerjava.util.TimerTask 这两个类可以负责java中的定时和调度相关内容。

  • 开启定时器:void schedule(TimerTask task, long delay, long period)
    • param1:定时器具体任务,需要使用TimerTask的子类实现。
    • param2:延迟多少毫秒之后执行任务。
    • param3:每隔多少毫秒执行一次。
  • 关闭定时器:void cancel()

源码: /javase-advanced/

  • src: c.y.thread.start.TimerTest.java
/**
 * @author yap
 */
public class TimerTest {

    private static class AlarmClockTask extends TimerTask {

        private boolean ring;

        @Override
        public void run() {
            Date now = new Date();
            String pattern = "HH:mm:ss";
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
            String dateStr = simpleDateFormat.format(now);
            String str = "16:45:00";
            if (str.equals(dateStr)) {
                ring = true;
            }
            if (ring) {
                System.out.println("Got up.....");
            }
        }

        void setRing(boolean ring) {
            this.ring = ring;
        }
    }

    @SneakyThrows
    public static void main(String[] args) {
        Timer timer = new Timer();
        AlarmClockTask alarmClockTask = new AlarmClockTask();
        timer.schedule(alarmClockTask, 0, 1000);
        char exitFlag = 'q';
        if (System.in.read() == exitFlag) {
            timer.cancel();
            alarmClockTask.setRing(false);
        }
    }
}


5. JVM线程内存模型

概念:

  • 计算机底层通信模型:计算机底层的硬件(CPU)和软件(内存)之间是通过IO桥和总线来进行通信连接的:
    • CPU通过系统总线连接到IO-Bridge。
    • 内存通过内存总线连接到IO-Bridge。
    • 其他组成部分如USB,显卡,磁盘,网卡驱动等,通过IO总线连接到IO-Bridge。
  • CPU结构:CPU核心主要由运算器(负责计算过程),控制器(负责收发计算指令)和寄存器(保存计算中间信息)组成:
    • 一颗CPU中存在多个核心(双核,四核,八核等),每个核心都能独立运行至少一个线程。
      • intel发明了超线程技术,让一个核心可以模拟两个线程,但一般情况下,在效率方面来说,四核八线程远不及八核效率高。
    • 每个核心都独立有自己的一级缓存L1和二级缓存L2,所有核心共享一个三级缓存L3。
    • 多颗CPU共享同一个主内存。
  • CPU读取数据流程:
    • 先尝试从L1读取,读取成功返回数据,速度极快,一个CPU的L1越大,性能越高。
    • L1读取失败,则尝试从L2读取,读取成功返回数据,失败则从L3读取,读取成功返回数据。
    • L3读取失败,则从主存中读取,速度相对低。

6. volatile关键字

概念: volatile修饰的变量有两种效果,即保证可见性和禁止指令重排序。

6.1 保证可见性

概念: volatile的底层使用的是MESI协议,即CPU缓存一致性协议,所以当volatile修饰的变量被某个线程改动后,会原子性地将改动后的值写回主存,并会立刻通知其他所有同样在使用该变量的线程,请重新到主存中获取最新的值。

源码: /javase-advanced/

  • src: c.y.thread.start.EnsureVisibilityByVolatileTest.java
/**
 * @author yap
 */
public class EnsureVisibilityByVolatileTest {

    private static class VolatileDemo implements Runnable {

        private volatile boolean stop;

        @Override
        public void run() {

            // must be empty body to block the current thread
            // Always read the flag in L1 cache
            while (!stop) {

            }
            System.out.println("thread-sub: over");
        }
    }

    @SneakyThrows
    @Test
    public void ensureVisibility() {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(volatileDemo).start();
        TimeUnit.SECONDS.sleep(2L);
        volatileDemo.stop = true;
        System.out.println("thread-main: over");
    }

    @SneakyThrows
    @After
    public void after() {
        System.out.println(System.in.read());
    }
}

尽量仅使用volatile修饰基本数据类型,修饰引用数据类型时只对其本身的改动立即可见,对引用数据类型内部的属性的改变不生效。

6.2 禁止指令重排

概念: 只要最终的结果不影响,有时候JVM为了提高程序的运行速度,可能会将代码的指令进行重新的排序执行,但这也许会对我们的代码逻辑产生影响,而volatile会对指令添加内存屏障,所以volatile修饰的变量会禁止JVM对其进行指令重排序。

源码: /javase-advanced/

  • src: c.y.thread.start.CommandReorderingTest.java
/**
 * @author yap
 */
public class CommandReorderingTest {

    private volatile int x = 0, y = 0, a = 0, b = 0;

    @SneakyThrows
    @Test
    public void orderReorder() {

        int count = 0;

        while (true) {

            count++;

            Thread threadA = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread threadB = new Thread(() -> {
                b = 1;
                y = a;
            });

            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();

            System.out.printf("第%d次:x=%d,y=%d\n", count, x, y);

            if (x == 0 && y == 0) {
                break;
            } else {
                x = 0;
                y = 0;
                a = 0;
                b = 0;
            }
        }
    }

    @SneakyThrows
    @After
    public void after(){
        System.out.println(System.in.read());
    }
}

6.3 缓存行对齐

概念: CPU在读数据的时候,为了提高效率,不会只读取目标值,而是连带着目标值相邻的,共64个字节的内容一并进行读取和回写操作,整行的内容称为一个高速缓存行,灵活运行缓存行可以提高程序效率,比如尽量将读的数据 a 和写的数据 volatile b 人为分开到不同的缓存行中,否则频繁的对 b 进行改动会连带着 a 一起在缓存和主存中不断的来回传输,影响读数据a的效率。

源码: /javase-advanced/

  • src: c.y.thread.start.CacheLinePaddingTest
/**
 * @author yap
 */
public class CacheLinePaddingTest {

    private final static long COUNT = 1_0000_0000L;

    private static class Demo {
        //private long p1, p2, p3, p4, p5, p6, p7;
        private volatile long x = 0L;
        //private long p9, p10, p11, p12, p13, p14, p15;
    }

    private static Demo[] demos = new Demo[2];

    static {
        demos[0] = new Demo();
        demos[1] = new Demo();
    }

    @Test
    public void cacheLinePaddingEfficiency() {
        new Thread(() -> {
            long start = System.currentTimeMillis();
            for (long i = 0; i < COUNT; i++) {
                demos[0].x = i;
            }
            long end = System.currentTimeMillis();
            System.out.println(end - start);
        }).start();

        new Thread(() -> {
            long start = System.currentTimeMillis();
            for (long i = 0; i < COUNT; i++) {
                demos[1].x = i;
            }
            long end = System.currentTimeMillis();
            System.out.println(end - start);
        }).start();
    }

    @SneakyThrows
    @After
    public void after() {
        System.out.println(System.in.read());
    }
}