Java之多线程
一、什么是多线程
- 进程:是正在运行的程序
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的 并发性:任何进程都可以同其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
? 单线程:一个进程如果只有一条执行路径,则称为单线程程序
? 多线程:一个进程如果有多条执行路径,则称为多线程程序
二、并发和并行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
三、多线程的实现
①、继承Thread类的方式
1、方法介绍
- void run():在线程开启后,此方法将被调用执行
- void start():使此线程开始执行,Java虚拟机会调用run方法()
2、实现步骤
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
3、示例?
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello world");
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
4、两个小问题
- 为什么要重写run()方法?
- 因为run()是用来封装被线程执行的代码
- run()方法和start()方法的区别?
- run():封装线程执行的代码,直接调用,相当于普通方法的调用
- start():启动线程;然后由JVM调用此线程的run()方法
②、实现Runnable接口的方式
1、Thread构造方法
Thread(Runnable target):分配一个新的Thread对象
Thread(Runnable target, String name):分配一个新的Thread对象?
2、实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
3、示例?
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello world");
}
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
}
}
③、 利用Callable接口和Future接口的方式
1、方法介绍
- V call():计算结果,如果无法计算结果,则抛出一个异常
- FutureTask(Callable?callable):创建一个 FutureTask,一旦运行就执行给定的 Callable
- V get():如有必要,等待计算完成,然后获取其结果?
V:泛型,表示获取的结果的数据类型
2、实现步骤
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
3、示例
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
// 管理多线程运行的结果
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread t = new Thread(ft);
t.start();
// 获取多线程运行的结果
Integer res = ft.get();
System.out.println(res);
}
}
四、三种实现方式的对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
五、常见的成员方法?
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字 |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public static void join() | 插入线程/插队线程 |
(一)、设置和获取线程名称
- void setName(String name):将此线程的名称更改为等于参数name
- String getName():返回此线程的名称
- Thread currentThread():返回对当前正在执行的线程对象的引用
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name),用构造方法设置名字
//MyThread my1 = new MyThread("高铁");
//MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
}
}
?细节:
- 如果没有给线程设置名字,线程也是有默认名字的
- 格式:Thread-X(X序号,从0开始)
- 如果我们要给线程设置名字,可以用set方法进行设置,也可以用构造方法设置
- 当JVM虚拟机启动之后,会自动的启动多条线程,其中有一条线程就叫做main线程,它的作用就是去调用main方法,并执行里面的代码
????????????????
(二)、线程休眠?
static void sleep(long millis):使当前正在执行的线程停留(暂停执行)指定的毫秒数
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
// System.out.println("睡觉前");
// Thread.sleep(3000);
// System.out.println("睡醒了");
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
?细节:
- 哪一条线程执行到sleep()方法,那么这条线程就会在这里停留对应的时间
- 方法的参数:表示睡眠的时间,单位毫秒
- 当时间到了后,线程会自动的醒来,继续执行下面的其他代码
(三)、线程优先级?
1、线程调度
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型
2、优先级相关方法
- final int getPriority():返回此线程的优先级
- final void setPriority(int newPriority):更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10?
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
// 创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
// 创建线程对象
Thread t1 = new Thread(mr, "飞机");
Thread t2 = new Thread(mr, "坦克");
// 获取优先级
// System.out.println(t1.getPriority());
// System.out.println(t2.getPriority());
// 获取main线程的优先级
// System.out.println(Thread.currentThread().getPriority()); // 5
// 设置优先级
t1.setPriority(10);
t2.setPriority(1);
t1.start();
t2.start();
}
}
?细节:
- 线程的优先级的范围是 1~10
- 优先级越大,代表抢到CPU的概率就越高,不是一定的
- 线程的默认优先级为5,main线程也是5
(四)、守护线程
- void setDaemon(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + " --> " + i);
}
}
}
public class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " --> " + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
// 把第二个线程设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
细节:
- 当其他的非守护线程执行完毕后,守护线程会陆陆续续的结束?
(五)、礼让线程 (了解)
- ?public static void yield():出让线程/礼让线程
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " --> " + i);
// 表示出让当前CPU的执行权,也就是让所有线程都重新抢夺CPU的执行权,这样可以使结果尽可能的均匀
Thread.yield();
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("飞机");
t2.setName("坦克");
t1.start();
t2.start();
}
}
(六)、插入线程 (了解)
?public static void join():插入线程/插队线程
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " --> " + i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.setName("坦克");
t.start();
// 表示把t线程插入到当前线程只前
// t:坦克
// 当前线程:main线程
t.join();
for (int i = 0; i < 10; i++) {
System.out.println("main -->"+i);
}
}
}
?六、线程的生命周期
- 新建,是刚使用new方法,new出来的线程;
- 就绪,是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段;
- 运行,当就绪的线程被调度并获得CPU资源时,便进入运行状态;
- 阻塞,在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态;
- 销毁,线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁。
七、线程安全问题
1、安全问题出现的条件
是多线程环境
有共享数据
有多条语句操作共享数据
2、如何解决多线程安全问题呢?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
(一)、同步代码块
1、同步代码块格式:
synchronized(任意对象) {
? ? 多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
特点一、锁默认打开,有一个吸纳成进去了,锁自动关闭
特点二、里面的代码全部执行完毕,线程出来,锁自动打开
特点三、锁对象,一定要是唯一的
2、同步的好处和弊端
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
3、示例
public class MyThread extends Thread {
private static int ticket;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyThread.class) {
if (ticket < 100) {
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票!!!");
} else {
break;
}
}
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
(二)、同步方法
1、同步方法的格式
同步方法:就是把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数) {
? ? 方法体;
}
特点一、同步方法是锁住方法里面的所有代码
特点二、锁对象不能自己指定
特点三、锁对象,一定要是唯一的
同步方法的锁对象是什么呢?
? ? ? ?this
2、静态同步方法
同步静态方法:就是把synchronized关键字加到静态方法上
java 修饰符 static synchronized 返回值类型 方法名(方法参数) {
? ? 方法体;
}
同步静态方法的锁对象是什么呢?
????????类名.class
3、示例
public class MyRunnable implements Runnable {
int ticket;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (method()) break;
}
}
private synchronized boolean method() {
if (ticket == 100) {
return true;
} else {
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖" + ticket + "张票!!!");
}
return false;
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
(三)、lock锁?
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
1、ReentrantLock构造方法
- ReentrantLock():创建一个ReentrantLock的实例
2、加锁解锁方法
- void lock():获得锁
- void unlock():释放锁?
3、示例?
public class MyThread extends Thread {
static int ticket = 0;
// 创建锁的对象
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket == 100) {
break;
} else {
Thread.sleep(10);
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票!!!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
?(四)、死锁
1、概述
????????线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
2、什么情况下会产生死锁
- 资源有限
- 同步嵌套
?3、示例
public class Demo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(() -> {
while (true) {
synchronized (objA) {
//线程一
synchronized (objB) {
System.out.println("小康同学正在走路");
}
}
}
}).start();
new Thread(() -> {
while (true) {
synchronized (objB) {
//线程二
synchronized (objA) {
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
}
八、生产者和消费者(等待唤醒机制)
(一)、概述
所谓生产者消费者问题,实际上主要是包含了两类线程:
- ? 一类是生产者线程用于生产数据
- ? 一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
(二)、Object类的等待和唤醒方法
- void wait():导致当前线程等待,直到另一个线程调用该对象的notify()方法或 notifyAll()方法
- void notify():唤醒正在等待对象监视器的单个线程
- void notifyAll():唤醒正在等待对象监视器的所有线程?
(三)、生产者和消费者案例?
1、案例需求
?桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
- 判断是否有包子,决定当前线程是否执行
- 如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子
- 生产包子之后,更新桌子上包子状态,唤醒消费者消费包子
消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
- 判断是否有包子,决定当前线程是否执行
- 如果没有包子,就进入等待状态,如果有包子,就消费包子
- ?消费包子后,更新桌子上包子状态,唤醒生产者生产包子
测试类(Demo):里面有main方法,main方法中的代码步骤如下
- 创建生产者线程和消费者线程对象
- 分别开启两个线程
2、代码演示
public class Desk {
/*
作用:控制生产者和消费者的执行
*/
// 0 没有食物 , 1 有食物
public static int foodFlag = 0;
// 总个数
public static int count = 20;
//锁对象
public static Object lock = new Object();
}
public class Cooker extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
if (Desk.foodFlag == 1) {
// 桌子如果有食物,等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 桌子如果没有食物,制作
System.out.println("厨师做了一碗面条");
// 修改桌子状态
Desk.foodFlag = 1;
// 唤醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
public class Foodie extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
if (Desk.foodFlag == 1) {
// 总数-1
Desk.count--;
// 如果有,就开吃
System.out.println("正在吃面条,还能吃" + Desk.count + "碗!!!");
// 吃完,唤醒厨师
Desk.lock.notifyAll(); // 唤醒绑定在这把锁上的所有线程
// 改变桌子的状态
Desk.foodFlag = 0;
} else {
try {
Desk.lock.wait();// 让当前线程和锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
public class Demo {
public static void main(String[] args) {
// 创建线程对象
Cooker cooker = new Cooker();
Foodie foodie = new Foodie();
// 设置线程名称
cooker.setName("厨师");
foodie.setName("吃货");
//开启线程
cooker.start();
foodie.start();
}
}
(四)、阻塞队列实现等待唤醒机制
生产者和消费者必须使用同一个阻塞队列
1、阻塞队列继承结构
2、常见BlockingQueue:
- ArrayBlockingQueue: 底层是数组,有界
- LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
3、BlockingQueue的核心方法:
- put(anObject): 将参数放入队列,如果放不进去会阻塞
- take(): 取出第一个数据,取不到会阻塞
两个方法底层都自带锁,所以不需要自己再加锁
4、代码示例
public class Cooker extends Thread {
private ArrayBlockingQueue<String> queue;
public Cooker(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
// 生产者步骤:
// 1,判断桌子上是否有汉堡包
// 如果有就等待,如果没有才生产。
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while (true) {
try {
queue.put("汉堡包");
System.out.println("厨师放入一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Foodie extends Thread {
private ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
//1,判断桌子上是否有汉堡包。
//2,如果没有就等待。
//3,如果有就开吃
//4,吃完之后,桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
//汉堡包的总数量减一
while (true) {
try {
String take = queue.take();
System.out.println("吃货将" + take + "拿出来吃了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) {
// 创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
Foodie f = new Foodie(queue);
Cooker c = new Cooker(queue);
f.start();
c.start();
}
}
九、线程池
(一)、概述
线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
(二)、设计思路?
1.?准备一个任务容器
2.?一次性启动多个消费者线程
3.?刚开始任务容器是空的,所以线程都在wait
4.?直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
5.?这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来
(三)、Executors默认线程池?
1、创建线程池
- static ExecutorService newCachedThreadPool():创建一个默认的线程池
- static newFixedThreadPool(int nThreads):创建一个指定最多线程数量的线程池
2、示例
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " --> " + i);
}
}
}
public class Demo {
public static void main(String[] args) {
// 获取线程池对象
// ExecutorService pool = Executors.newCachedThreadPool();
ExecutorService pool = Executors.newFixedThreadPool(2);
// 提交任务
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// 销毁线程池
//pool.shutdown();
}
}
(四)、自定义线程池?
1、创建线程池对象
?2、示例
public class Demo {
public static void main(String[] args) {
// 参数一:核心线程数量
// 参数二:最大线程数
// 参数三:空闲线程最大存活时间
// 参数四:时间单位
// 参数五:任务队列
// 参数六:创建线程工厂
// 参数七:任务的拒绝策略
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
3, // 核心线程数量,不能小于0
6, //最大线程数量,不能小于0,最大线程数量 >= 核心线程数量
60, //空闲线程最大存活时间
TimeUnit.SECONDS, //时间单位
new ArrayBlockingQueue<>(3), //任务队列
Executors.defaultThreadFactory(), //创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
);
poolExecutor.submit(new MyRunnable());
poolExecutor.submit(new MyRunnable());
poolExecutor.shutdown();
}
}
3、非默认任务拒绝策略
RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。
- ThreadPoolExecutor.AbortPolicy:?????????????丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
- ThreadPoolExecutor.DiscardPolicy:????????????丢弃任务,但是不抛出异常 这是不推荐的做法。
- ThreadPoolExecutor.DiscardOldestPolicy:????抛弃队列中等待最久的任务 然后把当前任务加入队列中。
- ThreadPoolExecutor.CallerRunsPolicy:????????调用任务的run()方法绕过线程池直接执行。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!