多线程!!
使用多线程并不难.难点是对这些概念的理解.
在实际开发过程中,我们自己写多线程的机会并不多,我们会使用框架.很多框架已经封装了多线程.
一.面试官都会问多线程的概念.
- 程序:就是具有特定功能的代码编译打包后的产物!例如:钉钉.程序安装到电脑上之后会有两种状态:运行状态和非运行状态.
- 进程:正在运行的程序.进程是资源分配的最小单位.操作系统会给进程分配资源(cpu,内存).
- 线程:一个进程里面包含1个或者多个线程.线程就是真正干活的"人".一般来说,一个线程可以执行一个或者多个任务.
- 操作系统把资源分配给进程,进程会把资源分配给线程.操作系统并不会直接和线程打交道.
二.当运行一个Java程序的时候,系统其实会创建3个线程.
- 主线程:执行main函数代码的线程
- 垃圾回收线程:垃圾回收线程会执行垃圾回收机制,会定期器回收不再使用的堆区内存.
- 错误处理线程:会出现报错.报错有一个线程.
//单线程.
//如果把所有的任务都放在一个线程里面执行会出现一个问题:如果前面的任务执行不完,后面的任务不开启.
//如果想要多个任务"同时"执行,称为并发.多个进程(正在运行的程序)他们交替使用cpu进行运算.从微观上,只看一个进程,程序中包含若干个线程,这些线程会抢占cpu.多线程让多个任务同时执行.
//如何创建额外的线程:
//创建线程有两种方式:
//1.定义Thread类的子类,重写run方法.1).新建一个类继承于Thread类;2).在子类中重写run方法--run方法里面写你要执行的任务;3).创建一个线程对象;4).需要调用线程对象的start方法,用于开辟线程.5).调用run方法其实就是普通的方法调用,并不会开辟新线程,只有调用start方法才会开辟新线程,在新线程里运行run方法.
//定义成匿名类:没有名字的 局部内部类,主要就是为了重写方法.
Thread t =new Thread(){
@Override
public void run(){
//自己写的代码
}
};
t.start();
//2.创建一个Runnable接口的实现类,这个实现类作为Thread的参数.1).创建一个类,实现Runnable接口;2).实现接口中声明的run方法.3).创建一个Thread对象,构造器的参数就是实现Rannable接口的对象.4).调用Thread对象的start方法.
//匿名类的使用场景:如果只是为了重写父类的方法,或者为了实现接口中的方法.
Thread t2 =new Thread(new Runnable(){
@Override
public void run(){
//自己写的代码
}
});
t2.start();
//区别:继承Thread和实现Runnable接口的区别?
// 1.在Java中只允许单继承,不能有多个父类,如果一个类已经有父类了,那么不让使用继承Thread方式实现多线程了.例如,如果有一个Student类已经继承了Person类那么只能选择Runnable接口了.
//2.如果继承Thread,调用sleep时,可以那对象(this)通用sleep,因为继承了整个方法.但是Runnable就不行,只能用Thread.sleep(long)方式.
//3.如果使用继承的方式实现线程同步,解决线程同步的问题.共享数据定义成static的,而且所需要类名.class.如果使用同步方法,方法必须是static方法.但Runnable实现类都可以不定义成静态的.static.
//lambda表达式:
public static void main(String[] args) {
Runnable mr = () -> {
System.out.println("我是一个子线程");
for(int i = 0; i < 500000; i++) {
System.out.println(i);
}
};
Thread t = new Thread(mr);
t.start();
System.out.println("hello world");
}
Thread类的常用方法:
- Thread是一个用于描述线程的类.他是可以描述,线程要执行什么任务(Runnable),线程的优先级,线程名称,线程状态等.
- currentThread() 获取当前线程对象。 类方法. Thread.currentThread()
- setName(String name) 设置线程的名字。
- getName() 获取线程的名字。
- setPriority(int priority) 设置线程的优先级。 优先级的取值范围[1,10],默认是5.优先级高不高只是概率问题,不是绝对的.
- getPriority() 获取线程的优先级。
- getState() 获取线程的状态.线程的状态.
-
- NEW 线程处于新建状态,当你new出来一个Thread,但是还没有调用start方法.
- RUNNABLE 可运行状态,就绪状态.当start之后,尚未获取cpu之前的状态.当前cpu时间片耗尽,会从运行状态到就绪状态.
- RUNNING运行状态,正在使用cpu.
- BLOCK阻塞状态.sleep
- join() 执行该线程,会阻塞当前线程。执行调用join方法的线程.等调用join方法的线程执行完之后,当前线程才继续执行.
- sleep(long millis) 休眠指定时间(单位毫秒),会阻塞当前线程。类方法
- start() 启动线程.
- yield() 暂定该线程的执行,交出CPU的使用权。
- 运行程序的时候,系统会给你创建一个线程(main),在这个线程中执行main方法.
线程的同步
卖票问题
什么是线程的同步?
- 并行指的是线程同时执行。
- 同步不是线程同时执行,而是线程不同时执行。同步本质指的是数据的同步。一般情况下,线程之间是相互独立,如果都去访问同一个变量,极有可能让这个数据变乱。如果不想让数据变乱,应在不让他们同时访问同一个变量。这个控制过程称为线程同步。
一.线程同步问题,就是线程安全问题,如果多个线程访问同一个数据(共享数据),会产生数据紊乱问题.如果解决?
- 上锁;—同步锁synchronized
- 同步代码块:被加同步锁的代码块,称为同步代码块.
- 同步方法:被synchronized修饰的方法称为同步方法。
二.要给什么代码块加锁?
- 凡是涉及到共享变量的代码都要加锁.
如何实现线程的同步?
1.同步代码块
synchronized(对象可以用类名.class){ //必须多个线程用同一把锁,锁是什么对象都行.锁也叫做同步监视器.
共享资源//我们所谓的那个变量。
}
同步代码块synchronized (对象),多个线程要公用同一个对象,才能真正意义上加上锁。对象没有特殊要求,可以是任何继承于Object类的对象。包括this
2.同步方法
被synchronized修饰的方法称为同步方法。
3.使用锁对象实现加锁和解锁.
public class SellWindow4 implements Runnable {
private int tickets = 100;
//锁对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (tickets > 0) {
String threadName = Thread.currentThread().getName();
lock.lock(); //加锁
tickets--;
if (tickets >= 0) {
System.out.println(threadName + "卖掉1张票,剩余" + tickets); }
lock.unlock(); //释放锁
}
}
}
- 线程的生命周期(从诞生到死亡)
- 变量的生命周期:变量从定义到内存被回收
- 线程的生命周期:线程从创建到销毁.在整个过程中,不同时期在整个过程中,不同的时期线程有不同的状态。而且在程序运行期间会发生状态的转换。
官方定义的线程状态如下:
- NEW:新建状态,指的是线程已经创建,但是尚未start()。
- RUNNABLE:可运行状态(已经调用了start方法),已经准备就绪,一旦抢到CPU就立即执行。
- BLOCKED:阻塞状态,处于阻塞状态的线程正在等待进入Synchronized块(或方法)。
- WAITING:等待状态,等待其他线程执行任务。直到其他线程任务结束或者收到notify信号。
- TIMED-WAITING:等待状态,限定时间的等待状态。
- TERMINATED:终止状态。线程要运行的任务已经结束。
生活中程序员会把线程划分为如下状态:
- NEW:新建状态,指的是线程已经创建,但是尚未start()。
- RUNNABLE:可运行状态(已经调用start方法),已经准备就绪,一旦抢到CPU就立即执行。
- RUNNING:正在运行状态,已经抢到CPU,正在执行代码片段。
- BLOCKED:阻塞状态。
- DEAD:死亡状态。线程的任务已经结束
一.线程一共两种:
- 用户线程:一般情况下,我们创建的线程都是用户线程.
- 守护线程:如果把线程t.setDaemon(true);这个t就会变成守护线程.
二.程序什么时候会终止:
- 在单线程下,main方法一旦执行完毕,程序就终止
- 在多线程下,所有的用户线程全部结束以后,程序才会终止.
三.程序刚启动的时候,系统会自动创建三个进程:
- mian线程—用户线程.
- 垃圾回收线程–守护线程.
- 有一个运行System.err的线程—守护线程.
三、线程池
1、什么是线程池?
- 水池:存放水的池子。
- 线程池:存放线程的池子。
- 线程池里面包含两类线程:核心线程(长期存在的线程);非核心线程(不能长期存在的).
- 任务队列:任务队列里包含若干个任务.BlockingTask(任务代码块)—Runnable对象.
- 队列:先进先出
任务如何与线程分配?
如果有线程空闲
Java中的线程池:是一个管理线程的池子。可以在需要的时候开辟线程,可以控制最大开辟的线程个数,可以在不需要的时候关闭线程,可以让任务排队执行。这些管理过程不需要我们干预,线程池能帮我们完成。我们所要做的就是往线程池中放任务。
2、为什么要有线程池?
- 多线程解决了任务并发问题,但是开辟和关闭线程很消耗系统的性能,开辟和关闭一个线程要处理很多细节,频繁的开辟和关闭线程会给系统增加很多开销。
- 线程池使用了重用的概念,可以控制线程开辟的数量,复用这些线程执行任务。这样就不用频繁的开辟和关闭线程了。
Execute 执行 .exe
Executor 执行器
3、创建线程池各参数含义
- new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,handler)
- corePoolSize 核心线程数
- maximumPoolSize 最大的线程数(还能扩充那么大)
- keepAliveTime 保留的存活时间(多长时间不用,移除该线程)
- unit TimeUnit 指定时间单位
- workQueue new ArrayBlockingQueue<>(5); 规定排队线程个数
- handler new ThreadPoolExecutor.CallerRunsPolicy() 指定拒绝策略
线程池能帮我们管理线程(在需要的时候开辟线程,不需要的时候销毁线程),把任务交给线程池即可,线程池会给你分配线程去执行,自己不需要和线程打交道.
拒绝策略
提供了四个预定义的处理程序策略:
-
在 ThreadPoolExecutor.AbortPolicy ,不执行新任务,并抛出异常。
-
在 ThreadPoolExecutor.CallerRunsPolicy 中,执行新提交的任务.
-
在 ThreadPoolExecutor.DiscardPolicy 中 ,不执行任务,也不抛异常。
-
在 ThreadPoolExecutor.DiscardOldestPolicy 中 ,丢弃队首的任务.
pool可以指定核心线程的个数,最大允许的线程的个数,超过核心线程数以后,多久关闭线程,任务队列,以及任务拒绝的机制。
5、线程池工具类
Exectors 是线程池工具类,可以帮我们快速构建线程池。
三种常见的线程池:
-
固定线程个数的线程池
-
不限线程个数的线程池
-
单个线程的线程池(串行任务池)
public class TestExecutors {
public static void main(String[] args) {
//创建一个固定个数的线程池
ExecutorService es = Executors.newFixedThreadPool(3);
//创建一个不限容量的线程池 ,初始容量是0.闲置60s,那么这个线程会被销毁
//ExecutorService es = Executors.newCachedThreadPool();
//创建一个只有一条线程的线程池 ,任务串行执行.
// ExecutorService es = Executors.newSingleThreadExecutor();
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
es.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}