系列文章目录
多线程与高并发-多线程(一)
前言
多线程和高并发在大厂面试和在大流量的系统是常涉及到的。老铁们要是不会这个,要想拿高薪,那是没戏的,再说吹牛逼也是需要资本的。
一 多线程
多线程的就是不同的执行路径。
1.1 定义
多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。
1.2 特点
- 在java内存模型,系统存在一个主内存, Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory)——工作栈,保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
- 多个线程的执行是并发的,在逻辑上“同时。如果系统只有一个CPU,那么真正的“同时”是不可能的。由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,将会带来线程调度,同步等问题。
1.3 状态
- 五大状态:新建、就绪、运行、等待/阻塞、死亡。
- 新建状态(New):当线程对象对创建后,即进入新建状态,如:Thread t = new MyThread();
- 就绪状态(Runnable):当调用线程的start(),即进入就绪状态。处于就绪状态的线程,只是说明此线程做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.waiting阻塞:运行状态中的线程执行wait()、join()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.timewaiting – 通过调用线程的sleep(time)或join(time)或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
方法解释:
- sleep(time):Thread的方法,会导致此线程暂停执行,主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,这个线程重回为就绪状态,如果当前线程进入了同步锁,sleep方法并不会释放锁,只让出了cpu,其他被同步锁挡住了的线程也无法得到执行;
- yield():主动放弃执行权,就像sleep(0);
- wait():Object类的方法,只能在同步代码块中才能执行,不然报错。放弃执行权、锁,进入对象等待队列,等待被唤醒
- this.notify();唤醒其他一个线程,notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁
- this.notifyAll();唤醒其他多个线程,
- synchronized(){};默认使用this锁,当线程执行到synchronized的方法的是,拥有了this的锁,其他线程不可以再进入这个方法,但是这个对象this下的其他未同步的方法,其他线程是可以进入的,只是不能获取this的锁
- join:A线程中调用B线程,等B线程完成后,才能继续用下运行A。
1.4 创建方式
// 1.继承Thread类,重写该类的run()方法。
class MyThread extends Thread {
}
public class ThreadTest {
public static void main(String[] args) {
Thread myThread1 = new MyThread(); // 创建一个新的线程 myThread1 此线程进入新建状态
myThread1.start(); // 调用start()方法使得线程进入就绪状态
}
}
// 2.实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
class MyRunnable implements Runnable {
}
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
thread1.start(); // 调用start()方法使得线程进入就绪状态
}
}
}
//3、线程池的方式,其实本质也是通过上面的方式。下面会专门讲线程池的方式,Executors执行器工具类,创建线城池执行器
ExecutorService pool = Executors.newFixedThreadPool(3).execute(new Runnable(){public void run(){}});
ExecutorService pool = Executors.newCachedThreadPool().execute(new Runnable(){public void run(){}});
ExecutorService pool = Executors.newSingleThreadExecutor().execute(new Runnable(){public void run(){}});
ExecutorService pool = Executors.newScheduledThreadPool(3).execute(new Runnable(){public void run(){}});
pool.execute(runnable);//提交任务,无返回值
Future future = pool.submit(callable);//提交任务,有返回值
二 线程池
出现原因: 每次都重新创建线程,1、耗费资源,2、可能会发生线程量激增,造成资源耗尽。通过线程池的方式,不用每次新建,还可以控制线程数量。
2.1 组成角色
区分于直接new thread().start()的方式,分离执行程序和执行器。通过创建执行器来执行执行程序
执行程序:
- Runnable:线程执行体
- Callable:线程执行体、可异步得到执行结果。
Future:返回线程执行的对象,通过它可得到线程的执行结果、取消线程执行,针对于执行callable来说的。
执行器:
Executor //父
——> ExecutorService //子
——> AbstractExecutorService //孙子
——> ThreadPoolExecutor //重孙子, 线程池执行器
——> ScheduledExecutorService //孙子
ps:通过创建执行器来执行执行程序
ExecutorService pool = Executors.newFixedThreadPool(3).execute(new Runnable(){public void run(){}});
ExecutorService pool = Executors.newCachedThreadPool().execute(new Runnable(){public void run(){}});
ExecutorService pool = Executors.newSingleThreadExecutor().execute(new Runnable(){public void run(){}});
ExecutorService pool = Executors.newScheduledThreadPool(3).execute(new Runnable(){public void run(){}});
pool.execute(runnable);//提交任务,无返回值
Future future = pool.submit(callable);//提交任务,有返回值
Executors四个方法创建的方式底层都是调用的new ThreadPoolExecutor
new ThreadPoolExecutor(
int corePoolSize,//核心线程数(合同工)
int maximumPoolSize,//最大线程数
long keepAliveTime,//临时工线程存活时长
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝后处理类
)
/**
** pool.submit(callable) 底层
** 把callable包装成futureTask,扔到任务队列中,等着被执行
**/
new FutureTask<T>(callable)
/**
** pool.execute(runnable);//提交任务,无返回值 ,底层
** 把runnable包装成ForkJoinTask,扔到任务队列中,等着被执行
**/
new ForkJoinTask.RunnableExecuteAction(task);
ThreadPoolExecutor 执行路径:
1、为什么需要使用多线程?
(1)多CPU多核系统中,使用线程提高CPU利用率。
(2)多个任务同步进行,提高用户体验。
(3)耗时的操作使用线程,提高应用程序响应
我向你奔赴而来,你就是星辰大海