《实战Java高并发程序》——第2章 Java并行程序基础
进程和线程
简单的话来说,在Windows中看到的后缀为.exe的文件都是程序。不过程序是"死"的、静态的。当双击这个.exe文件的时候,这个.exe文件中的执行就会被加载,你就能得到一个有关这个程序的进程。进程是"活"的,或者说是正在被执行的。
进程中可以容纳若干线程。
那进程和线程之间究竟是一种什么关系呢?简单地说,进程是一个容器。比如一件漂亮的小别墅,别墅里有卧室、厨房、书房、洗手间等,当然还有一家三口住在里面。这时一家三口在家里爱去哪里就去哪里、爱干什么就干什么(进程中有三个活动线程),小别墅就像一个进程,家里的厨房、书房就像这个进程占有的资源。
- 进程
- 当一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程
- 进程可以视作程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图),也有的程序只能启动一个实例进程(例如网易云音乐)
- 线程
- 一个进程之内可以分为一到多个线程
- 一个线程就是一个指令流,将指令流中的一条条指令按一定的顺序交给CPU执行
- 二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供内部的线程共享
- 进程间的通信比较复杂
- 同一台计算机的进程通信称为IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HHTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般要比进行上下文切换低
线程是轻量级进程,是程序执行的最小单位。使用多线程而不是多进程去进行并发程序的设计,是因为线程的切换和调度的成本远远小于进程。
线程的6种状态
Java的线程有6种状态:初始(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED)
NEW状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()方法执行时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程会暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时间限制的等待。那么等待的线程究竟在等什么呢?一般来说,WAITING的线程是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。
注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态。
线程运行的原理
栈与栈帧
JVM中由堆、栈、方法区所组成。栈内存起始就是给线程用的,每个线程启动,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,(每个栈帧)对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,(最顶上的那个栈帧)对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为一些原因导致CPU不再执行当前的线程,转而执行另外一个线程代码,例如:
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait等方法
当Context Switch发送时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条JVM执行的执行地址,是线程私有的。
- 状态包括程序计数器、虚拟机栈帧中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch频繁切换会影响性能
初始线程:线程的基本操作
线程的创建
创建新执行线程有两种方法:
- 将类声明为Thread类的子类(继承Thread类)
- 类实现Runnable接口
方法一:继承Thread类
public class ThreadStyle extends Thread {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();
}
class T1 extends Thread{
@Override
public void run() {
System.out.println("通过继承Thread类的方式创建线程");
}
}
}
线程Thread中有一个run()方法,start()方法会新建一个线程并让这个线程去执行run()方法
t1.run();也是可以正常执行的,但是却不能新建一个线程,而是在当前线程中调用run()方法,此时的run()方法会被当做一个普通的方法被调用。
注意:不要用run()方法来开启新线程,它只会在当前线程(上述例子是main线程)中串行执行run()方法
在默认情况下,线程Thread的run()方法什么都没有做,因此,这个线程一启动就马上结束了。
如果想让线程做点什么就必须重写run()方法,把"任务"填充进去
// 匿名内部类的写法
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("Hello, I am t1");
}
};
t1.start();
方法二:实现Runnable接口
public class RunnableStyle {
public static void main(String[] args) {
// 声明一个Runnable接口实例
T2 t2 = new T2();
Thread thread = new Thread(t2);
thread.start();
}
}
class T2 implements Runnable {
@Override
public void run() {
System.out.println("通过实现Runnable接口的方式创建线程");
}
}
Runnable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable {
public abstract void run();
}
Thread类有一个非常重要的构造方法:public Thread(Runnable targe);
它传入一个Runnable接口的实例,在调用start()方法时,新的线程就会执行Runnable.run()方法。
实际上,默认的Thread.run()方法就是这么做的:
public void run() {
if (target != null) {
target.run();
}
}
注意:默认的Thread.run()方法就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么更为合理。(也就是如果是通过实现Runnable接口的方式,可以使用run()方法或start()方法启动线程)
总结
- 通过实现Runnable接口来创建线程更好
解释:
- 从代码角度:具体任务(run方法)应该和"创建和运行线程的机制(Thread类)"解耦,用runnable对象可以实现解耦
- 继承Thread类以后,由于Java是单继承的,这样就无法再继承其他的类,限制了类的可扩展性
- 两种方法的本质对比
这两种方法的本质,最终都是调用了start()方法来新建线程。这两个方法的最主要区别在于run()方法的内容来源:
- 通过继承Thread类:整个run()都被重写
- 实现Runnable接口:最终调用的是target.run()
终止线程(stop)
一般来说,线程执行完毕就会关闭,无须手动关闭。但是,一些服务端的后台可能会常驻系统,它们通常是一个大大的无穷循环,它们通常不会正常关闭。
JDK线程Thread提供了一个stop()方法,如果使用stop()方法,就可以立即将一个线程终止,非常方便。
但是stop()方法过于暴力,强制把执行到一半的线程终止,可能会引起一些数据不一致的问题,所以被标注为废弃方法,不推荐使用。
stop()方法在结束线程时,会直接终止线程,并立即释放这个线程所持有的锁,而这些锁恰恰是用来维持对象一致性的。如果此时,写线程正写到一半,强行终止线程对象就会被写坏。同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章地读到了这个被写坏的对象,悲剧也就此发生。
java 终止线程的4种方式_java中如何停止一个线程-CSDN博客
线程中断(interrupt)(重要)
在Java中,线程中断是一种重要的线程协作机制。
严格来讲,线程中断并不会使线程立即关闭,而是给线程发送一个通知,告知目标线程,有人希望你关闭啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
public void Thread.interrupt() //中断线程
public boolean Thread.isInterrupted() // 判断线程是否被中断
public static boolean Thread.interrupted() // 判断线程是否被中断,并清除当前中断状态
真正的停止线程,其实是如何正确的interrupt通知需要停止的线程,以及被停止的线程如何配合的问题。
- Thread.interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。
- Thread.isInterrupted()方法也是实例方法,它判断当前线程是否被中断(通过检查中断标志位)。
- 静态方法Thread.interrupted()也可用来判断当前线程的中断状态,但同时会清除当前线程的中断状态。
等待(wait)和通知(notify)(重要)
等待方法wait()和通知方法notify()并不在Thread类中,而在Object类中,这意味着任何对象都可以调用这两个方法。
当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。这是什么意思呢?
比如,在线程A中,调用了wait()方法,那么线程A就会停止执行,转为等待状态。
等待到何时结束呢?线程A会一直等待到其他线程调用了notify()方法为止。这时,object对象俨然称为多个线程之间的有效沟通手段。
如果一个线程调用了object.wait()方法,那么它就会进入object对象的等待队列,会让线程进入WAITING状态。这个等待队列中可能会有多个线程,因为系统运行了多个线程,同时等待某一个对象。
当object.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。
需要注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。
除notify()方法外,Object对象还有一个类似的notifyAll()方法,它会唤醒在这个等待队列中等待的所有线程。
wait做的事情:
- 释放当前的锁
- 使当前执行代码的线程进入阻塞等待(把线程放到等待队列中)
- 满足一定条件时(收到通知时)被唤醒,同时重新尝试获取这个锁
wait结束等待的条件:
- 其他线程调用该对象的notify方法
- wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
- 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常
注意:wait() 必须搭配 synchronized 来使用,wait()必须写到synchronized代码块里面(notify 方法也必须在synchronized代码块中使用)。脱离 synchronized 使用 wait() 会直接抛出异常。
如果还没有获取到锁就尝试解锁,运行后就会抛出非法的锁状态异常。这里的代码抛出该异常正是这个原因:wait()方法内部有一步重要的操作:先解锁,再阻塞等待。
因此,在使用 wait() 前,必须先加锁,把wait()写到synchronized代码块内部。同时,Java也规定调用 notify() 也必须在synchronized代码块中。
并且,加锁的锁对象必须要与调用wait()的锁对象是同一个。如果加锁对象与调用wait()的对象不是同一个,也会抛出 IllegalMonitorStateException 异常。
注意:wait()方法和sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另外一个区别就是wait()方法会释放目标对象锁,而sleep()方法不会释放任何资源。
Java多线程基础-7:wait() 和 notify() 用法解析_java notify_碳基肥宅的博客-CSDN博客
- wait() 和 notify() 方法都必须搭配 synchronized 和同一个锁对象,如果wait()和notify()作用于不同的锁对象,是没有任何作用的。
- 如果一个线程调用对象的notify()方法,但该线程并不处于wait的状态中,notify()不会产生作用(也没有副作用)。
- 如果有多个线程在wait(),notify()是只随机唤醒一个,而notifyAll()则是唤醒所有。
挂起(suspend)和继续执行(resume)
略。这两个方法已经被弃用。
不推荐使用suspend()方法挂起线程是因为suspend()方法在暂停线程的同时,并不会释放任何资源。
如果resume()方法意外地在suspend()方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。而且,对于被挂起的线程,从它的线程状态上看,居然还是RUNNABLE,这也会严重影响我们对系统当前状态的判断。
等待线程结束(join)和谦让(yield)(重要)
一个线程的输入可能非常依赖于另一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。
第二个join()方法给出了一个最大等待时间,如果超过给定时间,目标线程还在执行,则当前线程也会因为"等不及了"而继续往下执行。
join通常是加入的意思,这个意思用在这里也非常贴切。因为一个线程要加入另外一个线程,最好的方法就是等着它一起走。
另一个方法就是yield()方法:
public static native void yield();
这是一个静态方法,一旦执行,它会使当前线程让出CPU。
但要注意,让出CPU并不表示当前线程不执行了,当前线程在让出CPU后,还会进行CPU资源的争夺,但是,是否能够再次分配到就不一定了。
因此,对yield()方法的调用就好像在说:“我已经完成了一些最重要的工作了,我可以休息一下了,可以给其他线程一些工作机会啦!”
如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用yield()方法,给予其他重要线程更多的工作机会。
yield()让当前线程暂停,但是不阻塞;yield()应该做的是让当前线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
volatile与Java内存模型(JMM)
Java内存模型都是围绕着原子性、有序性和可见性展开的。
Java使用了一些特殊的操作或者关键字来声明、告诉虚拟机。在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一。
当你用关键字volatile声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。
注意:关键字volatile不能代替锁,它也无法保证一些符合操作的原子性。
分门别类的管理:线程组
在一个线程中,如果线程数量很多,而且功能比较明确,就可以将相同功能的线程放在一个线程组里。
public class ThreadGroupTest implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Thread currentThread = Thread.currentThread();
System.out.println("currentThread:" + currentThread.getName()
+ "; threadGroup:" + currentThread.getThreadGroup().getName()
+ "; parentThreadGroup:" + currentThread.getThreadGroup().getParent().getName());
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadGroup rootGroup = new ThreadGroup("线程组root");
Thread thread0 = new Thread(rootGroup, new ThreadGroupTest(), "thread-0");
thread0.start();
ThreadGroup group1 = new ThreadGroup(rootGroup, "线程组1");
ThreadGroup group2 = new ThreadGroup(rootGroup, "线程组2");
Thread thread1 = new Thread(group1, new ThreadGroupTest(), "thread-1");
Thread thread2 = new Thread(group1, new ThreadGroupTest(), "thread-2");
thread1.start();
thread2.start();
Thread thread3 = new Thread(group2, new ThreadGroupTest(), "thread-3");
Thread thread4 = new Thread(group2, new ThreadGroupTest(), "thread-4");
thread3.start();
thread4.start();
}
}
驻守后台:守护线程(Daemon)
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程和JIT线程就可以理解为守护线程。与之相对应的是用户线程,我们可以认为用户线程是工作线程,它会完成程序应该要完成的业务操作。如果用户线程全部结束,则意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个程序就应该结束。因此,当一个Java程序内只有守护线程时,Java虚拟机就会自然退出。
设置守护线程:setDaemon(true)
注意:守护线程必须在线程start()之前设置,否则会报异常,告诉你守护线程设置失败。但是该线程依然可以继续执行,只是被当做用户线程而已。
先做重要的事:线程优先级
Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢到资源,当然,这只是一个概率问题。
在Java中,使用1到10表示线程优先级。
内置的三个静态标量:
1为最低优先级,5为默认值,10为最高优先级。
在Java中,可以通过setPriority(int newPriority)方法来设置线程的优先级。
数字越大则优先级越高,但有效范围在1到10之间。高优先级的线程在大部分情况下都会首先完成任务,但不能保证所有情况都这样。
本文作者:Ac_c0mpany丶
本文链接:https://www.cnblogs.com/keyongkang/p/17828739.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2022-11-13 【Vue2-04】scoped样式
2022-11-13 【Vue2-03】props属性
2022-11-13 【Vue2-02】ref属性
2022-11-13 【Vue2-01】Vue脚手架
2022-11-13 【ES6】模块化