前言
学习多线程之前,我们先要了解几个关于多线程有关的概念。
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
单线程程序:若有多个任务只能依次执行。
当上一个任务执行结束后,下一个任务开始执行。如:某地只有一辆共享单车,只能让一个人骑行,当这个人骑行完毕后,下一个人才能重新扫码骑行。
多线程程序:若有多个任务可以同时执行。如:某地有一排共享单车,这些单车能够满足同行的人同时找到车并扫码骑行。
最近迎来了金三银四的面试季,刚好整理了一些面试专题(见文末)分享给大家。
一、多线程创建方式一:继承Thread类
创建:继承Thread类,重写里面的Run方法
启动:创建子类对象,调用start方法
public class StartThread extends java.lang.Thread {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println("一边吃饭");
}
}
public static void main(String[] args) {
StartThread thread=new StartThread();
thread.start();//开启一个新的线程 下面的代码不受这句代码的影响不需要等待执行完成 继续往下走
for (int i = 0; i <10 ; i++) {
System.out.println("一边code");
}
}
}
运行结果
首先进入Main方法,然后调用子类对象的Start方法,会启动run方法,此时不需要等待run方法执行完毕,直接向继续执行”一边code“,start方法相当于开启了一个新的线程,start方法但不保证立即运行。
如果把调用子类的方法改成run,就变成了普通方法,需要等待执行完成再进入下一步
public class StartThread extends java.lang.Thread {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println("一边吃饭");
}
}
public static void main(String[] args) {
StartThread thread=new StartThread();
thread.run();//开启一个新的线程 下面的代码不受这句代码的影响不需要等待执行完成 继续往下走
for (int i = 0; i <10 ; i++) {
System.out.println("一边code");
}
}
}
运行结果,先吃饭后code
不建议使用,继承了一个类,就不能继承其他父类了
二、多线程创建方式二:实现Runnable接口
创建:实现Runnable接口 重写Run方法
启动:创建实现类对象、Thread对象 调用Start方法
public class RunnableThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println("一边吃饭");
}
}
public static void main(String[] args) {
RunnableThread runnableThread=new RunnableThread();
Thread thread=new Thread(runnableThread);
thread.start();
for (int i = 0; i <20 ; i++) {
System.out.println("一边打游戏");
}
}
}
推荐:避免单继承的局限性,优先使用接口
三、实现Runnable接口模拟简单抢票
建立三个用户模仿抢票
public class RabbitClass extends RunnableThread {
private int num=99;
@Override
public void run() {
while (true) {
if(num<0)
{
break;
}
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+num--);
}
}
public static void main(String[] args) {
RabbitClass rabbitClass=new RabbitClass();
new Thread(rabbitClass,"one").start();//用户一
new Thread(rabbitClass,"two").start();//用户二
new Thread(rabbitClass,"three").start();//用户三
}
}
四、多线程方式三:实现Callable接口
import java.util.concurrent.*;
public class Excallable implements Callable<Boolean> {
private int num=99;
@Override
public Boolean call() throws Exception {//模拟抢票
while (true) {
if(num<0)
{
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Excallable ccallable=new Excallable();
//创建执行服务
ExecutorService service= Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> retult=service.submit(ccallable);
Future<Boolean> retult2=service.submit(ccallable);
Future<Boolean> retult3=service.submit(ccallable);
//获取结果
boolean r=retult.get();
boolean r2=retult2.get();
boolean r3=retult3.get();
//关闭服务
service.shutdownNow();
}
}
五、线程的常用方法
1.1 Thread.currentThread()
Thread.currentThread()可以获得当前线程,同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()返回的是代码实际运行时候的线程对象。示例如下
public class SubThread extends Thread {
public SubThread()
{
System.out.println("main里面调用的线程"+Thread.currentThread().getName());
}
@Override
public void run() {
System.out.println("run里面调用的线程"+Thread.currentThread().getName());
}
public static void main(String[] args) {
System.out.println("main里面调用的线程"+Thread.currentThread().getName());
SubThread subThread=new SubThread();
subThread.start();//子线程
}
}
在main方法里面。调用线程所以是main线程,main里面调用构造方法,所以构造方法也是调用main线程,当启动子线程相当于开启了一个新的线程。
1.2 Thread.setName()/getName()
setName可以设置线程名称,getName可以获取线程名称,通过设置线程名称有助于程序调试,提高可读性,建议为每一个线程设置一个可以体现线程功能的名称。
1.3 isAlive()
isAlive可以判断线程是否处于活动状态,
public class SubThread extends Thread {
@Override
public void run() {
System.out.println("run方法-->"+isAlive());
}
public static void main(String[] args) {java
SubThread subThread=new SubThread();
System.out.println("begin-->"+subThread.isAlive());
subThread.start();
System.out.println("end-->"+subThread.isAlive());//此时线程结束有可能返回false,不定性
}
}
1.4 Sleep()
sleep方法让当前线程休眠指定毫秒数
1.5 getId()
Thread.getId()可以获得线程的唯一标识
某个编号的线程运行结束之后可能又被其他线程使用,重启JVM之后,同一个线程的id可能不一样。
1.6 yieId()
Thread.yieId()方法作用是放弃当前的CPU资源
1.7 setPripority()
Thread.setpropority(num)设置线程优先级
java线程优先级取值范围:1~10,超过这个范围会异常
操作系统中,优先级较高的线程获得CPU的资源比较多
线程的优先级本质上是给线程调度器一个提示,用于决定先调度那些线程,并不能保证线程先运行
优先级如果设置不当可能导致某些线程永远无法运行,即产生了线程饥饿。
线程优先级并不是设置的越高越好,一般设置普通优先级就好。线程的优先级具有继承性,在A线程中创建B线程,则B线程的优先级与A线程一样。
1.8 interrupt()
中断线程,该方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程
public class SubThread extends Thread {
@Override
public void run() {
for (int i = 0; i <10000 ; i++) {
System.out.println("run-->"+i);
if(this.isInterrupted())
{
System.out.println("线程中断退出");
return;//直接结束run方法
}
}
}
public static void main(String[] args) {
SubThread subThread=new SubThread();
subThread.start();//子线程
for (int i = 0; i <100 ; i++) {
System.out.println("main-->"+i);
}
subThread.interrupt();//标记线程中断此时isInterrupted=true 线程并没有中断
}
}
1.9 setDaemon()
java 中线程分为用户线程和守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个守护线程
守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程,守护线程会自动销毁。
public class SubThread extends Thread {
@Override
public void run() {
for (int i = 0; i <10000 ; i++) {
System.out.println("run-->"+i);
}
}
public static void main(String[] args) {
SubThread subThread=new SubThread();
subThread.setDaemon(true);//设置线程守护
subThread.start();//子线程
for (int i = 0; i <100 ; i++) {
System.out.println("main-->"+i);
}
}
}
设置线程守护以后,子线程run运行了一段才停止,因为设置线程守护以后销毁需要时间。
六、线程的生命周期
线程的生命周期可以通过getstate()获得,Thread.state类型分为
New:新建状态,创建了线程对象,在Start启动前的状态
Runnable可运行状态:包含READY,表示该线程可以被资源调度器进行调度。使它处于RUNNING状态,RUNNING状态表示该线程正在执行,如果用yieid方法可以把RUNNING状态转化为READY状态
Waiting等待状态:线程执行了wait()、thread.join 方法会把线程转化为Waiting等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转化为RUNNABLE状态。
TimeD_WAITING状态:跟Waiting类似,但是如果没有在指定范围实际完成期望操作,会自动转化为RUNNABLE状态。
TERMINARED状态:终止,线程结束
七、多线程状态图
八、多线程的优势和缺点
优势
1.提高系统的吞吐率,多线程可以使一个进程有多个并发的操作
2.提高响应性,WEB服务器会采用一些专门的线程负责处理请求操作,缩短用户等待时间
3.充分利用多核处理器资源,通过多线程可以充分的利用CPU资源避免浪费
劣势
线程安全问题,多线程共享数据时,如果没有采取正确的并发控制措施,就可能产生数据一致性的问题,如读取过期的数据,丢失数据更新。
线程活性问题,由于程序自身的缺陷导致哦线程一直处于非RUNNABLE状态,常见的活性故障有:
1.死锁(DEADLOOK):类似与鹬蚌相争
2.锁死(LockOut):类似于睡美人故事的王子挂了,一直处于一种状态没有唤醒
3.活锁(Livelock):类似于小猫咬自己尾巴
4.上下文切换(Context Switch)问题,处理器从一个线程切换到另一个线程
可靠性问题,可能会由一个线程导致JVM意外终止,其他线程无法执行
九、多线程的线程安全问题
非线程安全就是指多个线程对同一个实例对象进行操作的时候有只被更改或者值不同步的问题。
线程安全问题表现在三个方面:
1.原子性
原子就是不可分割的意思,有两层含义:
(1)访问共享变量的操作,其他线程来看,要么已经关闭,要么执行完成,其他线程看不到这个操作的中间结果
(2)访问同一种共享变量的原子操作是不能交错的
用户ATM取钱,要么成功取到钱了余额发生变更,要么失败什么都没有变
java有两种方法实现原子性:
(1)使用锁(锁具有排它性,一时刻只能被一个线程访问)
(2)使用处理器的CAS指令(硬件锁)
2.可见性
在多线程中,一个线程对某个共享变量进行修改,其他线程可能不能立即获取到这个更新的结果
如果更新之后能获取到则这个线程具有可见性,否则不具有可见性。可能会导致其他线程读取到脏数据。
3.有序性
有序性是指在某些情况,下一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器的其他线程看来是乱序的。
在多核处理器的环境下,编写代码的顺序可能不会是执行的顺序,在一个处理器上执行的顺序,在其他处理器上看起来和代码不一样,这种现象称为重排序。重排序是对内存访问操作的优化,前提是单线程,但是对多线程的正确性能可能会有影响。
操作顺序概念
1.1源代码顺序,指源码中指定的内存访问顺序
1.2程序顺序,处理器上运行的目标代码所指向的内存访问顺序
1.3感知顺序,给定处理器所感知到的该处理器以及其他处理器内存访问的操作顺序
1.4执行顺序,内存访问操作在处理器上的执行顺序
可以把重排序分为指令重排序和存储子系统重排序:
指令重排序主要有JIT编译器处理器引起的,指程序顺序和执行顺序不一样
指令重排序是一种动作,确实对指令的顺序做了调整,Javac编译器一般不会执行指令重排序,而JIT编译器可能执行。CPU处理器可能执行指令重排序,使得执行顺序与程序顺序不一致。
存储子系统重排序是由高速缓存,写缓冲器引起的,感知顺序和执行顺序不一致。
高速缓存是cpu为了匹配与主内存处理速度不匹配而设计的高速缓存,写缓存器用来提高写高速缓存的效率,即使处理器严格执行两个内存的访问操作,在存储子系统的作用下其他处理器对操作的操作顺序和感知顺序可能不一致。
存储子系统排序并没有对指令顺序进行排序,而是造成指令执行顺序被调整的假象。存储子系统重排序对象是内存操作的结果。
从处理器角度来看, 读内存就是从指定的 RAM 地址中加载数据到 寄存器,称为Load操作; 写内存就是把数据存储到指定的地址表示的RAM 存储单元中,称为Store操作.
内存重排序有以下四种可能:
1.1LoadLoad重排序,一个处理器先后执行两个读操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2->L1
1.2toreStore重排序一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是W2->W1
1.3LoadStore重排序,一个处理器先执行读内存操作L1再执行写内存操作W1,其他处理器对两个内存操作的感知顺序可能是W1->L1
1.4StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存操作L1,其他处理器对两个内存操作的感知顺序可能是L1->W1
内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重序不同
十、貌似串行语义
JIT编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序, 给单线程程序造成一种假象----指令是按照源码的顺序执行的.这种假象称为貌似串行语义. 并不能保证多线程环境程序的正确性
为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只 有不存在数据依赖关系的语句才会被重排序.如果两个操作(指令)访 问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间 就存在数据依赖关系(Data dependency).
x=1; y=x+1;后一条语句的操作数包含前一条语句的执行结果 如果不存在数据依赖关系则可能重排序,如:
double price=45.8; int quantity=10; double sum=price*quantity;
十一、保证内存访问的顺序性
可以使用volatile关键字,synchronized关键字实现有序性
最后
最近很多人都在面试,我这边也整理了相当多的面试专题资料,希望可以帮助到大家。 我这边整理了一份:多线程相关资料文档、Spring系列全家桶、Java的系统化资料:(包括2021年最新Java核心知识点、面试专题和20年总结的互联网真题、电子书等),有需要的朋友可以关注公众号【程序媛小琬】即可获取。