多线程
- 多线程
- 1. 关于多线程的理解
- 1.1 进程和线程
- 1.2 并行和并发
- 1.3 线程调度
- 2. 创建多线程的方式
- 创建线程有哪几种方式?
- 2.1 通过继承`Thread`类来创建并启动线程的步骤如下:
- 2.2 通过实现`Runnable`接口来创建并启动线程的步骤如下:
- 2.3 通过实现`Callable`接口来创建并启动线程的步骤如下:
- 3 run()和start()有什么区别?
- 4 线程是否可以重复启动,会有什么后果?
- 5. 说一说sleep()和wait()的区别
- 6. 说一说notify()、notifyAll()的区别
- 7. 如何实现子线程先执行,主线程再执行?
- 8. Thread 类的方法
- 8.1 构造方法
- 8.2 Thread 类的常用方法
- 9. 线程的状态
多线程
1. 关于多线程的理解
1.1 进程和线程
每个进程有多个线程
- 进程是系统运行程序的基本单元
- 每个进程的内部数据和状态是完全独立的
- 每一个应用程序运行的时候会产生一个进程
- 线程:就是一个进程中的执行单元。一个进程可以启动多个线程。cpu 调度和分配的最小单元。
- 线程必须在某个进程内执行。
所以多线程就是:在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程。
- 进程与线程的区别
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少
有一个线程。 - 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少
1.2 并行和并发
-
并发(Concurrency):
- 并发是指多个任务在相同的时间段内交替执行,每个任务可能只执行一小部分,然后切换到另一个任务。
- 并发并不一定意味着多个任务同时在不同的处理器核心上执行。它可以在单个处理器上通过时间片轮转来实现,也可以在多个处理器核心上并行执行。
- 通常,多线程程序是并发的,因为它们可以在单个处理器上通过线程切换实现并发执行。
示例:多个线程在单个处理器上轮流执行,共享CPU时间。
-
并行(Parallelism):
- 并行是指多个任务在相同的时间点上同时执行,每个任务都在不同的处理器核心上运行。
- 并行通常需要多核处理器或多个计算资源,并且可以实现更高的性能,因为多个任务可以在不互相干扰的情况下并行执行。
- 并行通常用于解决需要高性能的问题,如大规模数据处理或计算密集型任务。
示例:多个线程在不同的处理器核心上同时执行,各自独立工作。
总结:
- 并发是任务在时间上交替执行,可能在同一处理器核心上通过线程切换实现。
- 并行是任务在同一时刻同时执行,通常需要多个处理器核心或多个计算资源来实现。
- 并发可以提高系统的响应性和资源利用率,但并不一定提高吞吐量。
- 并行通常用于提高性能,特别是在多核处理器上,可以实现更高的吞吐量
1.3 线程调度
每个程序至少自动拥有一个线程,称为主线程
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 -
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java 使用的为抢占式调度。
2. 创建多线程的方式
创建线程有哪几种方式?
创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。
2.1 通过继承Thread
类来创建并启动线程的步骤如下:
-
定义
Thread
类的子类,并重写该类的run()
方法,该run()方法
将作为线程执行体。 -
创建
Thread
子类的实例,即创建了线程对象。 -
调用线程对象的
start()
方法来启动该线程。
/**
* 实现多线程的第一种方式
* 第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法
*/
public class ThreadDemo01 {
public static void main(String[] args) {
System.out.println("main start!");
MyThread myThread = new MyThread();
myThread.test(); // 不会启动线程
// 只有调用s'tart
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程");
}
}
}
// 自定义线程类
class MyThread extends Thread{
@Override
public void run(){
Thread thread = Thread.currentThread(); // 获取当前线程
thread.setName("自定义线程"); // 设置当前线程的名字
String name = thread.getName(); // 获取当前线程的名字
for (int i = 0; i < 10; i++) {
System.out.println(name + i);
}
}
public void test(){
System.out.println("test");
}
}
2.2 通过实现Runnable
接口来创建并启动线程的步骤如下:
- 定义
Runnable
接口的实现类,并实现该接口的run()
方法,该run()
方法将作为线程执行体。 - 创建
Runnable
实现类的实例,并将其作为Thread
的target
来创建Thread
对象,Thread
对象为线程对象。 - 调用线程对象的
start()
方法来启动该线程。
/**
* 实现多线程的第二种方式
* 第二种方式:编写一个类,实现java.lang.Runnable接口
*/
public class ThreadDemo02 {
public static void main(String[] args) {
// 创建任务对象
MyRunnable myRunnable = new MyRunnable();
// 创建线程
Thread tr = new Thread(myRunnable,"线程1");
// 启动线程
tr.start();
System.out.println("主线程");
// 除了上面使用的自定义类实现Runnable接口之外,还可以使用匿名内部类、lambda表达式来实现Runnable接口
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("使用匿名内部类实现Runnable接口。。");
}
};
new Thread(runnable1).start();
Runnable runnable2 = ()->{
String name = Thread.currentThread().getName();
System.out.println(name + ":使用匿名内部类实现Runnable接口。。");
};
new Thread(runnable2).start();
}
}
// 自定义类实现 Runnable 接口
class MyRunnable implements Runnable{
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "实现线程的第二种方式.");
}
}
2.3 通过实现Callable
接口来创建并启动线程的步骤如下:
- 创建
Callable
接口的实现类,并实现call()
方法,该call()
方法将作为线程执行体,且该call()
方法有返回值。然后再创建Callable
实现类的实例。 - 使用
FutureTask
类来包装Callable
对象,该FutureTask
对象封装了该Callable
对象的call()
方法的返回值。 - 使用
FutureTask
对象作为Thread
对象的target
创建并启动新线程。 - 调用
FutureTask
对象的get()
方法来获得子线程执行结束后的返回值。
/**
* 创建线程的第三种方式:
* 实现Callable接口
*/
public class ThreadDemo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 使用匿名内部类实现Callable接口
Callable<Integer> callable = new Callable<>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < 10; i++) {
sum += i;
}
// 返回一个结果
return sum;
}
};
// 创建一个FutureTask 对象,来接受异步计算的结果
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 创建线程
Thread thread = new Thread(futureTask);
thread.start();
// futureTask.get() 是在等待执行完毕获取结果,所以是在线程启动之后,才获取。
System.out.println(futureTask.get());
}
}
采用继承Thread类的方式、实现Runnable、Callable接口的方式创建多线程的优缺点:
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。而如果是已经继承Thread 类,则不能再继承其它父类了。(所以实现接口可以避免 java 中的单继承的局限性)
- 在实现Runnable、Callable接口的这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同的程序代码的线程去共享同一个资源的情况,实现解耦操作,较好地体现了面向对象的思想。
- 线程池只能放入实现 Runnable 或 Callable 类线程,不能直接放入继承 Thread 的类
- 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
- 采用继承Thread类的方式创建多线程的优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
3 run()和start()有什么区别?
run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
4 线程是否可以重复启动,会有什么后果?
只能对处于新建状态的线程调用start()
方法,否则将引发IllegalThreadStateException
异常。
扩展阅读
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()
方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
5. 说一说sleep()和wait()的区别
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
- sleep()不会释放锁,而wait()会释放锁,并需要通过
notify()
/notifyAll()
重新获取锁。
6. 说一说notify()、notifyAll()的区别
-
notify()
用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
-
notifyAll()
用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
7. 如何实现子线程先执行,主线程再执行?
启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行。
扩展阅读
Thread类提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。
8. Thread 类的方法
8.1 构造方法
Thread
中常用的构造方法有:
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
8.2 Thread 类的常用方法
-
public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。Thread.currentThread(); // 执行这句代码的线程
-
public void run()
: 表示线程的任务。- 所有
Thread
的子类应该覆盖(重写)此方法
- 所有
-
public synchronized void start()
: 线程开始执行;- 多次调用同一个线程的此方法是不合法的。
-
public static native void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 线程不会丢失任何CPU的所有权。//线程休眠5S Thread.sleep(5000);
-
public static void sleep(long millis, int nanos)
:使当前正在执行的线程以指定的毫秒数加上指定的纳秒数暂停(暂时停止执行) -
public final String getName()
:返回此线程的名称Thread.currentThread().getName();//获取当前线程名称
-
public final synchronized void setName(String name)
:将此线程的名称更改为等于参数name -
public final int getPriority()
:返回此线程的优先级 -
public final void setPriority(int newPriority)
:更改此线程的优先级,1~10之间 -
public final native boolean isAlive()
:测试这个线程是否活着。 如果一个线程已经启动并且尚未死亡,那么线程是活着的
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread thread = new Thread(runnable);
// isAlive():测试这个线程是否活着
System.out.println(thread.isAlive()); // false
thread.start();
// 线程启动后尚未结束时返回 true
System.out.println(thread.isAlive()); // true
}
public final void join()
:等待线程死亡,等同于join(0)
public final synchronized void join(long millis)
:等待这个线程死亡的时间最多为millis毫秒。 0的超时意味着永远等待join
可以理解为当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。(让父线程等待子线程结束之后才能继续运行)
public static void main(String[] args){
Thread th= new Thread(runnable);
th.start();
try {
// 在main执行, main线程等待 th 执行完毕后再执行
th.join();
// main线程等待 th 执行 1s 后再执行
// th.join(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("main end");
}
-
public void interrupt()
:中断这个线程,线程的中断状态标记为true
-
public static boolean interrupted()
:测试当前线程是否中断。 该方法可以清除线程的中断状态(设置中断状态为False
) 。 换句话说,如果这个方法被连续调用两次,那么第二个调用将返回false(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)。 -
public boolean isInterrupted()
:测试这个线程是否被中断。 线程的中断状态不受此方法的影响。不清除中断状态。
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
for (int i = 0; i < 10; i++) {
if (i % 3 == 0) {
// 中断线程, 标记线程中断状态
thread.interrupt();
}
// 测试当前线程是否中断
System.out.println(Thread.interrupted());//true false false true false false true false false true
}
int count = 0;
while (!thread.isInterrupted()) {
if (count == 5) {
// 当count == 5 时,线程就会被中断
thread.interrupt(); // 我想停
}
System.out.println("执行" + count ++); // 执行0 执行1 执行2 执行3 执行4 执行5
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
-
public static native void yield()
:导致当前执行线程处于让步状态。如果有其他可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。并不一定会让出去。
-
public State getState()
:返回此线程的状态,返回值是Thread
的一个内部类,枚举类State
。线程状态可以是:- NEW: 尚未启动的线程处于此状态。
- RUNNABLE: 在Java虚拟机中执行的线程(可以运行的线程)处于此状态。
- BLOCKED: 被阻塞等待监视器锁定的线程处于此状态。
- WAITING: 正在等待另一个线程执行特定动作的线程处于此状态。
- TIMED_WAITING: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 (
sleep(1000)
,join(1000)
) - TERMINATED: 已退出的线程处于此状态。
9. 线程的状态
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的start()方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有六种状态。在API中 java.lang.Thread.State
这个枚举中给出了六种线程状态。
线程从 Runnable (可运行)状态与非运行状态之间的转换问题:
多线程安全问题
(重点)关于多线程并发环境下,数据的安全问题
线程安全问题的出现主要是因为多线程并发访问共享资源时可能导致的竞态条件(Race Condition)和并发访问冲突。
具体内容点击这里:Java线程安全——关于多线程并发环境下,数据的安全问题