一、多线程相关概念
1、进程与线程
-
程序:是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
-
进程:是程序的一次执行过程,他是操作系统进行资源分配与调度的基本单位。每个进程都有拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要进行进程间通信(管道、文件等)
-
线程:线程是进程的一个执行单元,他是操作系统运算调度的最小单位。一个进程中可以有多个线程,每条线程并行执行不同的任务。每个线程在JVM中都有独立的栈空间。
2、主线程与子线程
-
主线程:JVM 启动时会创建一个主线程,该主线程负责执行 main 方法 . 主线程就是运行 main 方法的线程。
-
子线程:Java 中的线程不孤立的,线程之间存在一些联系. 如果在 A 线程中创建了 B 线程, 称 B 线程为 A 线程的子线程, 相应的 A 线程就是 B 线程的父线程。
3、用户线程和守护线程
3.1 概念
-
用户线程(User Thread):是系统的工作线程,它会完成这个程序需要完成的业务操作
-
守护线程(Daemon Thread):是一种特殊的线程,主要被用做程序中后台调度以及支持性的工作。
3.2 区别
两者的主要区别在于离开JVM的顺序,守护线程随着JVM一同结束工作,当JVM中的用户线程全部退出,只剩下守护线程,JVM也就退出了。
3.3 创建、使用守护线程
Thread.setDaemon(true) 可以将一个线程设置为守护线程。
守护线程相当于后台管理者,比如:GC垃圾回收等。
4、串行、并行、并发
串行:每次只能执行一个任务,一个任务完成后,另一个任务才能执行;
并行:多个任务同时执行(并发的理想状态)。从硬件角度来说,单核CPU无法实现真正意义上的并行(一段时间上的并行)。
并发:在一段时间内,多个任务交替进行。
二、多线程的优缺点
1、优势
-
提高吞吐率
-
提高响应性:Web 服务器会采用一些专门的线 程负责用户的请求处理,缩短了用户的等待时间
-
充分利用多核处理器资源
2、存在的问题和风险
2.1 线程安全问题
多线程共享数据时,如果没有采取正确的并发访问控制措施,就有可能产生数据一致性问题,如脏堵、数据丢失等问题。
诱因:
-
存在共享数据;
-
存在多个线程共同操作这些共享数据;
解决方法:
同一时刻仅有一个线程在操作共享数据,即加锁。
2.2 线程活性问题
-
死锁:两个或多个线程因竞争共享资源造成的相互等待现象。
-
活锁:一个线程通常会有会响应其他线程的活动。如果其他线程也会响应另一个线程的活动,那么就有 可能发生活锁。发生活锁的线程无法继续执行,但是他们并没有阻塞,只是一直忙着响应对方无法恢复的工作。就比如:两个人在楼道相遇,两个人相互让路,当时每次都会让到相同的位置,导致两人都无法通过。
-
饥饿 :一个线程的处理器资源被其它线程抢占而一直无法获得资源,这种状态被称之为饥饿。一般是由优先级高的线程抢占优先级低的线程处理器资源而造成的;
2.3 上下文切换
处理器从执行一个线程切换到另一个线程,有一定的性能开销。
2.4 可靠性
可能会由一个线程导致JVM意外中止,其他的线程也无法执行。
三、线程的创建与启动
1、多线程的实现方式
1.1 继承Thread类
public class ThreadDemo1 extends Thread {
public ThreadDemo1(String name) {
super(name);
}
@Override
public void run() {
super.run();
}
}
1.2 实现Runnable接口
public class ThreadDemo2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Thread(new ThreadDemo2());
thread.start();
}
}
1.3 实现Callable接口(有返回值)
# 通过FutureTask 可以获取线程执行结果
package cn.interview.base.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
public class ThreadDemo3 implements Callable<Object> {
@Override
public Object call() throws Exception {
int result = 1;
for (int i = 0; i < 5; i++) {
result += Math.random();
TimeUnit.SECONDS.sleep(2);
}
return result;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadDemo3 threadDemo3 = new ThreadDemo3();
FutureTask<Object> task = new FutureTask<>(threadDemo3);
Thread thread = new Thread(task);
thread.start();
System.out.println("线程:"+thread.getName()+",启动,可以进行其它业务流程");
Object result = task.get();
System.out.println("线程任务执行结果:"+result);
}
}
1.4 定时器 Timer
package cn.interview.base.thread;
import java.util.Timer;
import java.util.TimerTask;
/**
* 通过定时器实现
*/
public class ThreadDemo4 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器线程执行了");
}
},0,1000);
//延迟 0,周期 1s
}
}
1.5 线程池创建
package cn.interview.base.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo5 {
public static void main(String[] args) {
//创建一个具有10个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
long threadPoolUseTime = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-Run");
}
});
}
long threadPoolEnd = System.currentTimeMillis();
System.out.println("多线程用时"+(threadPoolEnd-threadPoolUseTime));
threadPool.shutdown();
}
}
2、启动线程
通过start()方法启动线程。
四、线程类的常用方法
1、Thread的方法
1.1 Thread.currentThread()
获取当前线程。
1.2 setName("") getName()
设置 获取线程名称。
1.3 isAlive()
判断线程是否存活。
1.4 Thread.sleep(1000)
使当前线程休眠,单位毫秒
1.5 getId()
获取线程唯一ID
1.6 yield()
放弃CPU资源
1.7 setPriority(1)
设置线程优先级,1-10,数字越大,优先级越高
1.8 Daemon 守护线程
isDaemon() 判断当前线程是否是守护线程。setDaemon(true) 设置当前线程为守护线程。
1.9 interrupt()
设置中断标志 。调用 interrupt() 方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。
1.10 判断线程是否中断状态
isInterrupted() 判断当前线程是否已经中断,
不清除状态。interrupted() 判断当前线程是否已经中断,
清除状态。
1.11 join
把指定线程加入当前线程,例如,将线程a 加入当前线程b,会让当前线程b进入等待状态,知道线程a生命周期结束,在此期间当前线程b处于blocked状态。
问:有三个线程 T1 T2 T3,如何保证顺序执行?
- 可以使用线程类中的join()方法在一个线程中启动另一个线程,另一个线程执行完成后该线程继续执行。在T2线程中 join T1 线程,T3线程中join T2 线程,然后启动三个线程,T1 T2 T3 三个线程会依次执行。
2、Object类中的方法
2.1 wait()
wait()方法的作用是使当前线程进行等待,它是Object类的通用方法。也可以设置等待时间。此方法用来将当前线程置入到 “预执行队列”中,并在wait 代码处停止执行,直到接到通知(notify()方法或notifyAll()方法)或中断为止。
在调用 wait方法之前线程需要获得该对象的对象级别的锁。即,只能在同步方法或者同步代码块内调用,调用wait后当前线程释放锁。
2.2 notify
唤醒正在此对象的监视器上等待的单个线程。如果有多个线程正在等待该对象,则随机选择其中一个被唤醒()。
也需要在同步方法或者同步代码块内调用。
2.3 notifyAll
notify 是将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。
五、线程的生命周期
1、获取线程的生命周期
getState()
2、生命周期过程
2.1 New
新建状态,创建了线程对象,但是未调用start()启动线程之前的状态;
2.2 Runnable
可运行状态,它包括READY 和RUNNING 两个状态。
(1) READY
被线程调度器选中之前的状态。
(2) RUNNING
如果线程被线程调度器选中,它就是RUNNING状态,代表现在正在执行。如果调用线程的yeild方法,则会使线程状态转换为READY状态。
2.3 BLOCKED
阻塞状态。线程发起阻塞的 I/O 操作,或者申请由其他线程占用的独占资源,线程会转换为 BLOCKED 阻塞状态. 处于阻塞状态的线程不会占用CPU 资源. 当阻塞I/O 操作执行完,或者线程获得了其申请的资源,线程可以转换为 RUNNABLE.
2.4 WAITING
等待状态:线程执行了 object.wait(), thread.join()方法会把线程转换为 WAITING 等待状态
2.5 TIMED_WAITING
超时等待状态:与 WAITING 状态类似,都是等待状态.区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为 RUNNABLE状态。object.wait(1000);sleep(1000)
2.6 TERMINATED
终止状态,线程运行结束。