目录
一,什么叫线程? 那我们要先了解什么叫进程,线程依赖于进程而存在的。
二.多线程的创建
方式一:继承Thread类
方式二:实现Runnable接口
方式三:JDK 5.0新增:实现Callable接口
三种方式的比较
三.线程Thread的常用方法
四.线程调度
五.线程控制
六.线程的生命周期:
七.线程同步
1.同步代码块:
2.同步方法:
3.lock锁
八.线程池
1.概念
2.不使用线程池的问题
3.工作原理
4.如何得到线程池对象
5.ThreadPoolExecutor构造器的参数说明
6.线程池常见面试题:
作者有话说
一,什么叫线程?
那我们要先了解什么叫进程,线程依赖于进程而存在的。
进程:正在运行的程序
- 是系统进行资源调用和资源分配的独立单位
- 每一个进程都有它自己的内存空间和系统资源
线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径就是单线程程序
记事本程序:在调出页面设置的时候只能在关闭页面设置之后进行其他操作,否则无法进行其他操作。
- 多线程:一个进程如果只有多条执行路径就是多线程程序
扫雷程序:点击第一下时间开始计时,时间计时的同时可以玩扫雷游戏
二.多线程的创建
方式一:继承Thread类
- Java是通过java.lang.Thread 类来代表线程的。(不需要导包)
- 按照面向对象的思想,Thread类应该提供了实现多线程的方式。
方法:
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
为啥要重写run()方法:
因为在MyThread里面还有其他的代码,并不是所有的代码都需要被线程执行,为了区分哪些被线程执行,java就提供了一个run()方法
- 创建MyThread类的对象
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
优缺点:
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
/**
目标:多线程的创建方式一:继承Thread类实现。
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程(执行的还是run方法)
//run方法就是一个普通的方法,没有真正启动一个线程,就会把run方法执行完毕,才向下执行,
//就是会先执行run方法 只有当run执行完毕才会执行其他线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程类继承Thread类
*/
class MyThread extends Thread{
/**
2、重写run方法,里面是定义线程以后要干啥
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
小问题:
为什么不直接调用了run方法,而是调用start启动线程。
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。 只有调用start方法才是启动一个新的线程执行。
方式二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
构造器 | 说明 |
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) | 封装Runnable对象成为线程对象,并指定线程名称 |
优缺点:
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
/**
目标:学会线程的创建方式二,理解它的优缺点。
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// Thread t = new Thread(target, "1号");
// 5、启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程任务类 实现Runnable接口
*/
class MyRunnable implements Runnable {
/**
2、重写run方法,定义线程的执行任务的
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
方式三:JDK 5.0新增:实现Callable接口
1、前2种线程创建方式都存在一个问题:
他们重写的run方法均不能直接返回结果。 不适合需要返回线程执行结果的业务场景。
2、怎么解决这个问题呢?
JDK 5.0提供了Callable和FutureTask来实现。 这种方式的优点是:可以得到线程执行的结果。
3.多线程的实现方案三:利用Callable、FutureTask接口实现。
(1)、得到任务对象
定义类实现Callable接口,重写call方法,封装要做的事情。
用FutureTask把Callable对象封装成线程任务对象。
(2)、把线程任务对象交给Thread处理。
(3)、调用Thread的start方法启动线程,执行任务
(4)、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
优缺点:
优点:
线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
缺点:
编码复杂一点。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
目标:学会线程的创建方式三:实现Callable接口,结合FutureTask完成。
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable(100);
// 4、把Callable任务对象 交给 FutureTask 对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
*/
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
/**
2、重写call方法(任务方法)
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
三种方式的比较
方式 | 优点 | 缺点 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
三.线程Thread的常用方法
1. 当有很多线程在执行的时候,我们怎么去区分这些线程呢?
此时需要使用Thread的常用方法:getName()、setName()、currentThread()等。
Thread常用方法、构造器
方法名称 | 说明 |
String getName() | 获取当前线程的名称,默认线程名称是Thread-索引 |
void setName(String name) | 设置线程名称 |
public static Thread currentThread(): | 返回对当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒。 |
public void run() | 线程任务方法 |
public void start() | 线程启动方法 |
构造器 | 说明 |
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 把Runnable对象交给线程对象 |
public Thread(Runnable target ,String name ) | 把Runnable对象交给线程对象,并指定线程名称 |
public class MyThread extends Thread{
public MyThread(){}
public MyThread(String name){
super(name);
}
public void run(){
for(int i=0;i<50;i++){
System.out.println(getName()+"追逐王二的速度"+i+"km/h");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t=new MyThread("张三");
Thread t1=new MyThread("李四");
t.start();
t1.start();
}
}
四.线程调度
线程有两种调度模型
●分时调度模型: 所有线程轮流使用CPU的使用权,平均分配每个钱程占用CPU的时间片
●抢占式调度模型: 抢占式调度模型:优先让优先级高的线程使用cpu,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
Java使用的是抢占式调度模型
假如计算机只有一个CPU, 那么CPU在某一个时刻只能执行一 条指令, 线程只有得到CPU时间片,也就是使用权, 才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
Thread类中设置和获取线程优先级的方法
●public final int getPriority0:返回此线程的优先级
public final void setPriority(int newPriority):更改此线程的优先级
五.线程控制
方法名 | 说明 |
static void sleep(long millis) | 使当前正在执行的线程停留 (暂停执行) 指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(booleanon) | 将此线程标记为守护线程, 当运行的线程都是守护线程时,Java虚拟机将退出 |
static void sleep(long millis) :可以让线程1秒内同时开始,然后停止。只有前后的争夺不会连续;
public void run() {
for (int i=0;i<50;i++){
System.out.println(getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void join() :“李渊”死了之后李世民和李建成才能开始夺位,所以join()是等设置的线程死亡其他线程才能工作。
public static void main(String[] args) {
Thread t=new zh01("李渊");
Thread t1=new zh01("李世民");
Thread t2=new zh01("李建成");
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
t2.start();
}
void setDaemon(booleanon) :设置一个主线程,其他守护线程等主线程死亡后,也慢慢停止运行
public class Zh04 {
public static void main(String[] args) {
zh01 td1 = new zh01 () ;
zh01 td2 = new zh01 ( ) ;
td1. setName( "关羽");
td2. setName("张飞");
//设置主线程为刘备
Thread . currentThread(). setName("刘备");
//设置守护线程
td1. setDaemon(true);
td2. setDaemon(true);
td1.start();
td2. start();
for(int i=0; i<10; i++) {
System. out . println(Thread. currentThread() . getName()+":"+i);
}
}
}
六.线程的生命周期:
NEW(新建) | 线程刚被创建,但是并未启动。 |
Runnable(可运行) | 线程已经调用了start()等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
七.线程同步
需求:某电影院正在上演一部大片,现有100张票在三个窗口销售,用线程模拟三个窗口的售票速度和票数情况,所以我们用sleep()方法模拟售票等待时机
package 多线程;
public class Buypiao extends Thread{
public Buypiao(){
}
public Buypiao(String name){
super(name);
}
private int piao=100;
@Override
public void run() {
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(piao>0){
System.out.println(Thread.currentThread().getName()+"销售还有"+piao+"张");
piao--;
}
}
}
package 多线程;
public class Buypiaodomn {
public static void main(String[] args) {
Buypiao buy=new Buypiao("窗口1");
Buypiao buy1=new Buypiao("窗口2");
Buypiao buy2=new Buypiao("窗口3");
buy.start();
buy1.start();
buy2.start();
}
}
我们发现在运行的时候出现了票数重复 :假设t1线程先抢到cpu的执行权,但是需要休息,这个时候t2抢到cpu的执行权,故此t2也开始执行,t3也是如此;最后才能减少票数
问题分析:
为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准
●是否是多线程环境
●
是否有共享数据
●是否有多条语句操作共享数据
如何解决多线程安全问题呢?
●基本思想: 让程序没有安全问题的环境
怎么实现呢? .
●把多条语句操作共享数据的代码给锁起来, 让任意时刻只能有一个线程执行即可
●Java提供 了同步代码块的方式来解决
1.同步代码块:
格式:
synchronized (任意对象:相当于一把锁) {
多条语句操作共享语句的代码
}
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
锁对象的规范要求
规范上:建议使用共享资源作为锁对象。
对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
同步代码块是如何实现线程安全:
对出现问题的核心代码使用synchronized进行加锁
每次只能一个线程占锁进入访问
public void run() {
while (true) {
synchronized (obj) {
if (piao > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "销售还有" + piao + "张");
piao--;
}
}
}
}
问题解决思路:
假设 t1抢到了cpu的执行权,然后t1开始运行,t1休息的时候t2抢到cpu的执行权,因为代码上锁所以只能等待,等t1休息好,这段代码的锁就被释放了,运行完t2才开始执行,这个时候代码也会一步步递减
同步的好处和弊端
●好处:解决了多线程的数据安全问题
●弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
2.同步方法:
就是把synchronized关键词加到方法上
格式:
修饰符synchronized返回值类型 方法名(方法参数){ }
同步方法的锁对象:
this
同步静态方法:就是把synchronized关键词加到静态方法上
格式:
修饰符static synchronized返回值类型 方法名(方法参数){ }
同步方法的锁对象:
类名.class
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
底层原理:
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法:同步方法默认用this作为的锁对象。
但是代码要高度面向对象! 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
3.lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象
方法名称 | 说明 |
public ReentrantLock() | 获得Lock锁的实现类对象 |
方法名称 | 说明 |
void lock() | 获得锁 |
void unlock() | 释放锁 |
while (true) {
lock.lock();
if (piao > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "销售还有" + piao + "张");
piao--;
}
lock.unlock();
}
}
八.线程池
1.概念
线程池就是一个可以复用线程的技术。
2.不使用线程池的问题
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
3.工作原理
4.如何得到线程池对象
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
ExecutorService-->ThreadPoolExecutor
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
5.ThreadPoolExecutor构造器的参数说明
参数一:指定线程池的线程数量(核心线程): corePoolSize--->不能小于0
参数二:指定线程池可支持的最大线程数: maximumPoolSize--->最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime--->不能小于0
参数四:指定存活时间的单位(秒、分、时、天): unit--->时间单位
参数五:指定任务队列: workQueue--->不能为null
参数六:指定用哪个线程工厂创建线程: threadFactory--->不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler --->不能为null
6.线程池常见面试题:
临时线程什么时候创建啊?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
作者有话说
咋就是说本来只想写点的结果越写发现线程越神秘,要不是实力受限咋能写本书出来,线程真的有点强了,还有一些写了怕误导大家就不进行反面教材了!!! 谢谢大家支持!!!