Java线程 基础知识<1>
一、相关概念的解读
1.并发和并行
- 并发:就是通过调度算法,让用户看上去程序是在同时运行。这么说能好一点,并发就是指同一时刻只能有一条指令执行,但是多个线程指令被快速的轮换执行,使得宏观上具有多个线程同时执行的效果,但是微观上并不是同时执行的,只不过是把时间分成了若干段,让多个线程快速交替的执行。并发可以看作是并行的假象。我们可以把并发理解为逻辑上的同时发生。
- 并行:就是指在同一时刻,由多条指令在多个处理器上同时执行,所以无论是从微观还是宏观的角度上来说,二者都是一起执行的。我们可以把并行理解为物理层面的同时发生。
2.同步和异步(java
中)
- 同步:就是指发送一个请求,需要等待返回,然后才能发送下一个请求,这里会有一个等待过程。
- 异步:就是指发一个请求,不需要等待返回,随时可以再发送下一个请求,不需要等待。
- 区别:同步需要等待,异步不需要等待,在部分情况下,我们的项目开发都会使用异步开发的方式来实现。
- 哪些情况必须是同步的呢?
- 银行的转账操作
- 对数据据的保存操作等
3.进程和线程:
- 进程:进程是操作系统分配资源的基本单位。
- 线程:线程是任务调度和执行的最小基本单位。
- 开销方面:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销,线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间的切换的开销小。
- 所处环境:在操作系统中能同时运行多个进程,而在同一个进程中由多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)。
- 内存分配:系统在运行的时候会为每个进程分配不同的内存空间,而对线程而言,线程只能共享内存资源。
- 包含关系:我们可以这么理解,一般来说,线程是进程的一部分,线程可以被称之为轻量级线程。
4.阻塞和非阻塞:
- 阻塞:就是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
- 非阻塞:就是指不能得到返回结果之前,改调用不会阻塞当前线程。
二、线程的相关知识
1.线程的状态图
- 新建状态:顾名思义,就是刚刚新鲜new或者通过其他方式创建出来的线程,就处于新建状态。
- 就绪状态:当线程对象调用了start()方法以后,那么线程就会进入就绪状态。该状态下线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:如果就绪状态的线程获取了CPU的资源,就可以执行run()方法,此时线程就会处于运行状态,运行状态的线程最为复杂,他可以变成 阻塞状态、就绪状态和死亡状态。
- 阻塞状态:如果一个线程执行了sleep()或者suspend(),也就是睡眠或者挂起等方法,失去了自身线程所占的资源之后,那么该线程就会从运行状态进入到阻塞状态。阻塞状态也分为三种:
- 等待阻塞:运行状态中的线执行了wait()方法,让线程进入到等待阻塞状态。
- 同步阻塞:线程在获取synchronized同步锁失败(因为同步锁被其他线程锁占用)。
- 其他阻塞:调用线程的sleep()或者join()放出了io请求时,线程就会进入到阻塞状态。当sleep()状态超时,join()等待线程终止或超时,或者io处理完毕,那么线程重新转入就绪状态。
- 死亡状态:一个运行状态的线程完成任务或者被其他终止条件发生时,该线程就会切入到死亡状态。死亡的可能条件:
- run()或者call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的异常或者错误
- 直接调用线程的stop()方法来结束该线程
2.创建线程的三种方式
2.1 继承Thread类来创建线程
public class ExThread1 extends Thread{
@Override
public void run() {
//获取线程的名称
for(int i=0;i<3;i++){
System.out.println("当前线程的名称 "+this.getName()+" i "+i);
}
}
}
public class Test1 {
public static void main(String[] args) {
ExThread1 thread1 = new ExThread1();
ExThread1 thread2 = new ExThread1();
thread1.start();
thread2.start();
}
}
//上述操作的执行结果,我们可以看出,线程是抢占式执行的
当前线程的名称 Thread-0 i 0
当前线程的名称 Thread-1 i 0
当前线程的名称 Thread-1 i 1
当前线程的名称 Thread-1 i 2
当前线程的名称 Thread-0 i 1
当前线程的名称 Thread-0 i 2
2.2 实现Runnable接口来创建线程
- 先创建一个runnable接口的实现类,然后重写runnable接口中的run方法
- 创建一个runnable接口的实现类对象
- 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
- 调用Thread类中的对象的start方法,开启线程执行任务
public class ImThread2 implements Runnable{
@Override
public void run() {
for(int i=10;i<13;i++){
System.out.println("当前线程的名字: "+Thread.currentThread().getName()+" i "+i);
}
}
}
public class Test2 {
public static void main(String[] args) {
ImThread2 imThread1 = new ImThread2();
ImThread2 imThread2 = new ImThread2();
Thread thread3 = new Thread(imThread1);
Thread thread4 = new Thread(imThread2);
thread3.start();
thread4.start();
}
}
//执行结果
当前线程的名字: Thread-1 i 10
当前线程的名字: Thread-1 i 11
当前线程的名字: Thread-1 i 12
当前线程的名字: Thread-0 i 10
当前线程的名字: Thread-0 i 11
当前线程的名字: Thread-0 i 12
2.3 实现Callable接口和Future来创建线程
public class ImThread3 implements Callable<String> {
@Override
public String call() throws Exception {
String string = "我喜欢你,就像风飞了十万里,不见归期";
for(int i=100;i<103;i++){
System.out.println("当前线程: "+Thread.currentThread().getName()+" i "+i);
}
return string;
}
}
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ImThread3 imThread5 = new ImThread3();
ImThread3 imThread6 = new ImThread3();
FutureTask<String> f1 = new FutureTask<String>(imThread5);
FutureTask<String> f2 = new FutureTask<String>(imThread6);
Thread thread5 = new Thread(f1);
Thread thread6 = new Thread(f2);
thread5.start();
thread6.start();
System.out.println(f1.get());
System.out.println(f2.get());
}
}
//执行结果
当前线程: Thread-1 i 100
当前线程: Thread-1 i 101
当前线程: Thread-1 i 102
当前线程: Thread-0 i 100
当前线程: Thread-0 i 101
当前线程: Thread-0 i 102
我喜欢你,就像风飞了十万里,不见归期
我喜欢你,就像风飞了十万里,不见归期
创建线程三种方式的对比:
- 使用Thread类
- 优点:编写简单
- 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
- 使用Runnable接口
- 优点:实现的是接口,还可以继承其他的类,比较灵活,是面向对象思想的体现
- 缺点:编程会复杂一点
- 使用Callable配合FutureTask
- 优点:callable任务执行后,可以有返回值
- 缺点:比较复杂
还有一个这里的面试题,需要注意:
-
start和run两个方法的区别?
-
start使用开启线程的,但是线程开启后不会立即执行,而是需要获取CPU的执行权才能执行。需要手动调用。
-
run方法是用来执行具体的操作的,是JUM创建完本地操作系统即线程后回调的函数,不可以手动调用。如果手动调用,那么就是手动调用一个普通方法,和线程无关。
-
-
call方法和run方法的区别
-
call可以有返回值,run方法没有返回值
-
call可以抛出异常,而run方法则是由于本身没有抛出异常,所以我们自定义的时候也不能抛出异常。
-
3.多线程中的关键函数
3.1 这里是线程对象调用的函数
序号 | 方法描述 |
---|---|
public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public void run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
public final void setPriority(int priority) | 更改线程的优先级。 |
public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
public void interrupt() | 中断线程。 |
public final boolean isAlive() | 测试线程是否处于活动状态。 |
3.2 这里是静态方法,也就是Thread对象可以调用的
序号 | 方法描述 |
---|---|
public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
public static boolean holdsLock(Object x) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
public static void dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 |