2-2-3-1、深入理解Java线程


Java线程

Java线程的实现方式

思考:Java中实现线程有几种方式
Java中实现线程的方式本质上只有一种:由于java中的线程依赖于操作系统线程的创建,因此,不管使用什么方式实现线程,最后都是通过操作系统的pthread_create方法来创建
但可以通过不同的场景对任务有不同的使用方式

使用 Thread类或继承Thread类

// 创建线程对象
Thread t = new Thread() {
	public void run() {
		// 要执行的任务
	}
};
// 启动线程
t.start();

实现 Runnable 接口配合Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程对象
  • Runnable 可运行的任务(线程要执行的代码)对象
Runnable runnable = new Runnable() {
    public void run(){
    // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();

使用有返回值的 Callable

class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());

使用 lambda

new Thread(()> System.out.println(Thread.currentThread().getName())).start();

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法

Java线程实现原理

思考:Java线程执行为什么不能直接调用run()方法,而要调用start()方法
由于Java的线程创建需要依赖于操作系统,因此直接调用run()方法其实只是简单的方法调用,而调用start()方法是告诉操作系统按照我提供的java线程对象创建一个线程A,并将它加入就绪列表,参与cpu调度,如果线程A获取到cpu的执行权之后,就会通过jvm调用java线程对象的run方法,完成对Java线程对象run方法的异步调用
对于线程创建的详情,查看Thread#start()源码分析

Java线程属于内核级线程

基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中

内核级线程(Kernel Level Thread ,KLT)

它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现

用户级线程(User Level Thread,ULT)

操作系统内核不知道应用线程的存在

在这里插入图片描述

Java线程的调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度

协同式线程调度

线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里

抢占式线程调度

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞

Java线程调度就是抢占式调度

希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统
例如:

/**
 * 一个简单卖票程序:多个窗口卖票
 */
public class SellTicketDemo implements Runnable {
    //车票
    private int ticket;
    public SellTicketDemo() {
        this.ticket = 1000;
    }
    @Override
    public void run() {
        while (ticket > 0) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        // 线程进入暂时的休眠
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 获取到当前正在执行的程序的名称,打印余票
                    System.out.println(Thread.currentThread().getName() + ":正在执行操作,余票:" + ticket--);
                }
            }
            Thread.yield();
        }
    }
    public static void main(String[] args) {
        SellTicketDemo demo = new SellTicketDemo();
        Thread thread1 = new Thread(demo, "thread1");
        Thread thread2 = new Thread(demo, "thread2");
        Thread thread3 = new Thread(demo, "thread3");
        Thread thread4 = new Thread(demo, "thread4");
        //priority优先级默认是5,最低1,最高10
        thread1.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        thread3.setPriority(Thread.MIN_PRIORITY);
        thread4.setPriority(Thread.MAX_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

Java线程的生命周期

Java 语言中线程共有六种状态,分别是:

  1. NEW(初始化状态)
  2. RUNNABLE(可运行状态+运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权

在这里插入图片描述


从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)

在这里插入图片描述


从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)

在这里插入图片描述

Thread常用方法

sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相同

yield方法

  • yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
  • 具体的实现依赖于操作系统的任务调度器

join方法

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
例如:

public class ThreadJoinDemo {
    public static void main(String[] sure) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("t begin");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t finished");
        });
        long start = System.currentTimeMillis();
        t.start();
        //主线程等待线程t执行完成
        t.join();
        System.out.println("执行时间:" + (System.currentTimeMillis() - start));
        System.out.println("Main finished");
    }
}

思考:如何正确优雅的停止线程
Java线程类型提供了停止线程的方法stop(),但是stop会有任务未执行完,强行停止的问题,一次,我们会借助中断标志位来进行优雅的停止线程

stop方法

stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止

public class ThreadStopDemo {
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(60000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        });
        thread.start();
        Thread.sleep(2000);
        // 停止thread,并释放锁
        thread.stop();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "等待获取锁");
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                }
            }
        }).start();
    }
}

stop会释放对象锁,可能会造成数据不一致

Java线程的中断机制

Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止

API的使用
  • interrupt():将线程的中断标志位设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle

中断机制简单应用示例:

/**
 * 中断机制
 */
public class ThreadInterruptTest {
    static int i = 0;
    public static void main(String[] args)  {
        System.out.println("begin");
        Thread t1 = new Thread(() -> {
            do {
                i++;
                System.out.println(i);
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //Thread.interrupted()  清除中断标志位
                //Thread.currentThread().isInterrupted() 不会清除中断标志位
                if (Thread.interrupted()) {
                    System.out.println("=========");
                }

            } while (i != 10);
        });
        t1.start();
        //不会停止线程t1,只会设置一个中断标志位 flag=true
        t1.interrupt();
    }
}

对于借助中断标志位来中断线程的方式如下:

public class ThreadStopDemo2 implements Runnable {
    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
                //重新设置线程中断状态为true
                Thread.currentThread().interrupt();
            }
        }
        System.out.println("线程停止: stop thread");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadStopDemo2());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}

注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况

阻塞线程的中断

针对上面的代码示例,我们可以看出,一般阻塞的线程都是需要捕获中断异常,一旦修改当前线程的中断标志位,当前线程就会收到中断信号,随即抛出中断异常,而我们的中断业务就需要在异常中进行处理

Java线程间通信

volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信
示例如下:

public class VolatileDemo {
    private static volatile boolean flag = true;
    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (flag) {
                    System.out.println("run on");
                    flag = false;
                }
            }
        }).start();
        new Thread(() -> {
            while (true) {
                if (!flag) {
                    System.out.println("run off");
                    flag = true;
                }
            }
        }).start();
    }
}

等待唤醒(等待通知)机制

  • 等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒
public class WaitDemo {
    private static final Object lock = new Object();
    private static volatile boolean flag = true;
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock){
                while (flag){
                    try {
                        System.out.println("wait start .......");
                        //等待
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait end ....... ");
            }
        }).start();
        new Thread(() -> {
            if (flag){
                synchronized (lock){
                    if (flag){
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //通知
                        lock.notifyAll();
                        System.out.println("notify .......");
                        flag = false;
                    }
                }
            }
        }).start();
    }
}
  • LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的
public class LockSupportTest {
    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();

        System.out.println("唤醒parkThread");
        //为指定线程parkThread提供“许可”
        LockSupport.unpark(parkThread);
    }
    static class ParkThread implements Runnable {
        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            // 等待“许可”
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}

管道输入输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现:
PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符

public class PipedTest {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }
    static class Print implements Runnable {
        private final PipedReader in;
        public Print(PipedReader in) {
            this.in = in;
        }
        @Override
        public void run() {
            int receive;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

Thread.join

join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的
详细示例查看上面的ThreadJoinDemo

协程

定义

协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源

在这里插入图片描述


子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。而协程的调用和子程序不同。协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行
例如以下代码:

def A():
 print '1'
 print '2'
 print '3'
def B():
 print 'x'
 print 'y'
 print 'z'

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:1 2 x y 3 z

协程的特点

协程的特点在于是一个线程执行,与多线程对比的优势:

  • 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率
  • 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值