线程是什么
线程(Thread)是操作系统能进行运算调度的最小单位,被包含在进程中,是进程的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是独立调度和分派的基本单位。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环(registercontext),自己的线程本地存储(thread-local storage)。
线程的生命周期
新建—>就绪(可运行)—>运行—>等待/阻塞–>可运行–>死亡
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
使用线程有什么好处
计算机运行过程中,进程不断执行,假如需要不间断的交叉输出两个用户的信息,一般代码很难完成这种功能,使用线程能很好的解决并发执行问题 ,线程相当于是计算机的一个运行资源,能和代码中其他执行方法分隔开,方便,效率高。
线程创建的不止三种方法
- 继承Thread类 ,直接new类.start(),一般最好实现Runnable接口的方法,这样,本类就可以继承其他的类了,不影响代码。Thread这个类其实就是实现了Runnable接口的一个实例,进去看Thread的源码就知道其实本质还是实现的Runnable接口的,Start()是一个native方法。
// 线程类 ,调用run方法
public class Thread1 extends Thread {
@Override
public void run() {
while (true){
System.out.println("aa"+Thread.currentThread().getId());
}
}
}
//测试类
public class Test1 {
public static void main(String[] args){
Thread1 thread1 = new Thread1();
thread1.start();//start 创建一个线程,再执行run()方法
while (true){
System.out.println("bb"+Thread.currentThread().getId());
}
}
}
- 实现Runnable接口,没有Start方法,使用时要new Thread®.start() 嵌套一层
//线程类,实现Runnable接口
public class Thread2 implements Runnable {
@Override
public void run() {
while (true){
System.out.println("aaaaaaaaaaaa");
}
}
}
//测试类 使用一层Thread嵌套
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Thread2 thread2 = new Thread2();
Thread thread = new Thread(thread2);
// Thread.sleep(1000);//使线程睡眠1秒种
thread.start();//开始执行线程
while (true){
System.out.println("bbbbbbbbb");
}
}
}
3.实现Callable接口,配合上线程池(偷偷拷了一段龟兔赛跑的线程模拟代码)我们使用Callable的好处是不仅可以通过获取Future的对象来得到任务的返回值,还可以获取异常,但是Runnable就不行。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Thred {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程
ExecutorService ser = Executors.newFixedThreadPool(2);
Race tortoise = new Race("乌龟",1000);
Race rabbit = new Race("兔子",500);
Future<Integer> result1 = ser.submit(tortoise);
Future<Integer> result2 = ser.submit(rabbit);
Thread.sleep(2000);
tortoise.setFlag(false);
rabbit.setFlag(false);
// 获取值
Integer num1 = result1.get();
Integer num2 = result2.get();
System.out.println("乌龟"+num1);
System.out.println("兔子"+num2);
// 停止服务
ser.shutdownNow();
}
}
class Race implements Callable<Integer>{
private String name;//名称
private long time;//延时时间
private boolean flag = true;
private int step = 0;
public Race() {
}
public Race(String name) {
super();
this.name = name;
}
public Race(String name, long time) {
super();
this.name = name;
this.time = time;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
@Override
public Integer call() throws Exception {
while (flag){
Thread.sleep(time);
step++;
}
return step;
}
}
线程池
- 线程池的好处是:由于线程资源每次需要的时候要创建,不需要就杀掉,使用的线程越多,资源浪费就越严重,而且很占用系统调用资源,使用线程池可以大大减少资源浪费,这也是缓存策略的一种(划重点:面试官最喜欢问缓存,各大缓存策略学它个十来个,面试绝对有自信)。
- 那么线程池有看过源码就知道,最高的接口是Executor,但是它还不算是个线程池,实际上线程池接口是继承了它的儿子ExecutorService
- 使用线程池(ThreadPool)(四种创建线程池的方法)
一、 CachePool 有就用原来的,没有就创建新线程 ,创建数量最高为(Integer.MAX_VALUE)
public class CachePool {
public static void main(String[] args) throws InterruptedException {
// lombda表达式创建匿名实现类对象(必须是接口且只有一个方法)
Runnable runnable = () -> System.out.println("hahaha...."+Thread.currentThread().getId());//创建线程的简便方法
// 线程池,有四种
/*
第一种CachePool
1. 没有线程的时候,创建一个新的线程,使用完后放回
池中
2. 下次调用会重复利用该池中的线程,而不需要重新创建
新的线程
3. 如果池中没有线程, 新的任务会新建一个线程
*/
//线程池放在外面,否则循环中每次创建新的池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
//启动线程
executorService.execute(runnable);//相当于调start方法
}
//发送一个关闭指令,等待线程池中所有线程任务完成后再关闭池
executorService.shutdown();
System.out.println("end");
}
}
二、 FixedPool 有就用原来的,没有就创建新线程,但是创建线程数量固定上限,后续任务等
public class FixedPool {
public static void main(String[] args) throws InterruptedException {
// lombda表达式创建匿名实现类对象(必须是接口且只有一个方法)
Runnable runnable = () -> System.out.println("hahaha...."+Thread.currentThread().getId());//创建线程的简便方法
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
// 线程池,有四种
/*
第二种FixedPool
1. 固定线程池中多少个线程,有就用原来的,没有就创建,但不能比规定多
2. 下次调用会重复利用该池中的线程,而不需要重新创建
新的线程
3. 如果池中没有线程, 新的任务会新建一个线程,直到最大数量
*/
// 线程池放外面,否则循环中每次创建新的池
ExecutorService executorService = Executors.newFixedThreadPool(5); // 规定线程数量上限
for (int i = 0; i < 10; i++) {
//启动线程
executorService.execute(runnable);//相当于调start方法
}
//发送一个关闭指令,等待线程池中所有线程任务完成后再关闭池
executorService.shutdown();
System.out.println("end");
}
}
三、 ScheduledPool 周期性执行的线程池(类似setInterval)
public class ScheduledPool {
public static void main(String[] args) {
Runnable r = () -> System.out.println("hahaha...."+Thread.currentThread().getId());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.scheduleAtFixedRate(r,5000,1000, TimeUnit.MILLISECONDS);
//如果定时任务需要周期执行,则不能执行shutdown
// executor.shutdown();
}
}
四、SinglePool 只创建一个线程来完成任务等
补充 : run和start 的区别
- 当线程开始运行,会调用run方法。(如果手动调用该run方法,那么它和普通方法一样)
- start是告诉JVM启动一个新线程运行run方法。
线程的方法
-
join 示例:thread1.join
阻塞运行,等待thread1执行完成后,程序继续往下执行 -
stop 过时不推荐,直接停止线程,导致不安全
-
interrupt 设置一个interrupt布尔标志为true,然后线程根据这个标志
可以决定什么时候退出. 如果在线程阻塞的情况下调用了interrupt,那么线程会抛出
一个interruptException并重置interrupt为false -
sleep(ms)阻塞当前线程一段时间,并且不会释放同步块(锁)
-
yield 退让,给OS调用其他同级别线程的机会,自己重新回到就绪状态参与竞选,
可以理解为自动时间的sleep.不会释放锁. -
wait 等待,释放对象锁并且进入等待状态.释放的必须为同步块的锁对象
可以有参数设置ms多久进入就绪状态(一般不推荐,可能导致线程不安.全).
也可以不写参数,那么会一直等待下去,需要其他线程唤醒它才能继续运行 -
notify 随机唤醒一个等待状态的线程.
该方法必须和要唤醒的线程,在同一个锁对象中.而notifyAll就是唤醒所有等待中的线程。
几个面试题分享
Q:请问sleep()和wait()区别?
A:sleep()是线程Thread类的方法,一般sleep()会带一个参数,作用是使线程暂停一段时间,到时间自动恢复,而且调用sleep()不会释放对象锁。而wait()是Object的方法,调用此方法会释放对象锁,那么直到调用了Object.notify()或Object.notifyAll()方法才会重新获得对象锁进入运行状态,当然wait()也可以带时间参数,在无锁竞争情况下,在等待时间过去后就直接重新获取锁,再往后执行;但是在竞争条件下,都会等获取到锁了才可以往下执行。
Q:请问线程的休眠状态有几种?
A:有三种, 第一种是blocked(阻塞),由synchronized锁将线程释放监视器锁,线程进入阻塞状态。第二种是waiting(等待),这种方法就是调用Object.wait()或Object.join()方法让线程释放对象锁,使其休眠。第三种是timed.waiting(有时间的等待),这种方法一般是调用Thread.sleep(time),Object.wait(time),Object.join(time)等方法使其休眠,无锁竞争下,到时间也会直接重新获取锁往后执行。
Q: 线程currentThread()与interrupt()方法是做什么用的(简单说明)?
A:1. currentThread()方法是获取当前线程。2. interrupt()唤醒休眠线程,休眠线程发生InterruptedException异常
Q:JVM启动时是单线程的还是多线程的?
A:当然是多线程的,因为它不仅自己的启动main需要线程,它的回收GC线程也是需要线程启动的,这就说明了是多线程的。
以上确实是线程比较基础的知识,那么往后的多线程有许多锁,锁优化,JUC锁框架,CAS,以及上下文切换的优化,死锁等等知识敬请期待后续肝出来!