2. Java线程详解
2.1 Java线程的实现方式
- 通过继承Thread类实现(Thread类实现了Runnable接口)
- 实现Runnable接口(Runnable接口里就一个run方法)。
- 实现Callable接口(Callable接口里就一个call方法)。
1>、用FutureTask执行Callable接口的实现类,实现精准接收返回值。
2>、 用线程池的submit方法执行Callable接口的实现类,可以实现线程池并发的处理结果,方便对Callable实现类的执行方式做统一的管理。
4. 使用lambda表达式
new Thread(() ‐ > System.out.println(Thread.currentThread().getName())).start();
2.2 run方法和start方法的区别
Run方式是Runnale接口中定义的抽象方法经实现类实现的。主要用来处理要实现的逻辑,可以说就是一个普通方法。而Start方法是Thread类的中的方法,它里面调用了一个叫start0 的native方法。start0里面调用jvm的startThread的方法来创建一个子线程,并调用线程的run方法开始线程逻辑的处理。
2.3 Java线程的调度机制
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。
分时调度模型:指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。
抢占式调度模型:让可运行迟中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其它线程获取CPU的使用权。
java虚拟机默认采用抢占式调度模型,但在某些特定的需求下需要改变这种模型,由线程自己来控制CPU的调度。
2.4 多线程的六种状态
1. 新建状态(New)
2. 运行状态(Runnable):包含Running和Ready。调用start方法后进入的状态。
3. 无限期等待(Waiting):不会被分配cpu执行时间,需要显式唤醒。
进入此状态方式:
- 无参的Object.wait();
- 无参的Thread.join();
4. 限期等待(Timed Waiting):在一定时间后系统自动唤醒。
- 入参的Object.wait();
- 入参的Thread.join();
- 入参的Thrad.sleep();
5. 阻塞状态(Blocked):等待获取排它锁。
6. 结束状态(Terminated /'tɝmə,net/):线程执行完毕。
2.5 Thread常用方法
- sleep和wait的区别
- sleep是Thread类中的方法,而wait是Object的方法
- sleep可以在任何地方使用,而wait只能在synchronized方法或synchronized代码块中使用(lock.wait())。
- sleep的主要作用是使当前线程让出cpu资源,不会导致锁行为的改变;而wait不仅会让出cpu,而且还会释放已占有的同步锁资源。线程调用的wait方法后就进入了线程等待池中,直到线程完成了指定的等待时间或者锁对象调用了notify/notifyall方法后,线程才重新回到锁池中等待锁资源的获取。
- notify和notifyall的区别
Notify只会随机让一个处于等待池中的线程进入锁池中去竞争锁的机会,而notifyall会让所有处在等待池中的线程进入锁池去竞争锁资源,没有获取锁资源的线程也只能继续待在锁池中等待其他机会获取锁资源,而不能回到等待池中。
- yield方法
当调用Thread.yield函数时,会给线程调度器一个当前线程愿意让出cpu使用的暗示,但线程调度器可能会忽略这个暗示。yield()不会释放当前线程占用的锁资源。线程调用yield方法后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
- join方法
join方法用于阻塞当前线程,等到调用join方法的线程执行完毕后再来执行当前线程。一般用于等待异步线程执行完结果之后才能继续运行的场景。
Thread t = new Thread(new Runnable() { @Override public void run() { 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));
2.6 Thread中断方法
- stop方法
Thread类中方法,暴力中断线程,不论线程此时处于什么状态。
- interrupt实例方法
- 1.当线程状态在正常运行状态时,改变线程的中断标识为true(即isInterrupted()返回值为true),但不会对线程的运行产生任何影响。
- 2.当线程为阻塞状态时,抛出InterruptedException异常,将线程从阻塞状态中唤醒,线程恢复运行状态。不会改变线程的中断标识,
- interrupted静态方法
Interrupted是静态方法,它里面调用了currentThread.isInterrupted(true),它会返回当前线程的中断标识,并重置为false。
- isInterrupted实例方法
判断当前线程的中断标识,如果是true则返回true,是Flase则返回flase,不会线程的中断标识做处理。
while (!Thread.currentThread().isInterrupted() && more work to do) { do more work }
2.7 线程通信方法
- volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。一个变量被volatile修饰时,线程每次读取变量必须从主存中获取,每次修改变量也会立刻刷到主存。它时通过内存屏障实现了变量可见性和禁止指令重排序。
- 等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒。
public class WaitDemo { private static Object lock = new Object(); private static boolean flag = true; public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { 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(new Runnable() { @Override public void run() { if (flag) { synchronized (lock) { if (flag) { lock.notify(); System.out.println("notify ......."); flag = false; } } } } }).start(); } }
- LockSupport()
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"); 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 Piped { 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 = 0; try { while ((receive = System.in.read()) != ‐1){ out.write(receive); } } finally { out.close(); } } static class Print implements Runnable { private PipedReader in; public Print(PipedReader in) { this.in = in; } @Override public void run() { int receive = 0; try { while ((receive = in.read()) != ‐1){ System.out.print((char) receive); } } catch (IOException ex) { } } } }
- Thread.join
join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。
2.8 进程通信的方式:
- linux管道
- 通过linux的mkfifo命令来创建管道来实现两个进程的通讯,这种方式频繁交互数据。
- 消息队列
- 共享内存
- 比如一个微服务生成一个文件,让nginx挂到这个目录下读取。
- 信号量
- 比如 redis的分布式锁,多个进程利用这个分布式锁的有无信号可以实现通讯。
- socket通信
- 比如http、dubbo。