目录
四、多线程
1,线程与进程
1.1 线程与进程
进程:
- 是指一个内存中运行的应用程序,每个进程都有一个独立
的内存空间
线程:
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进 程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
1.2 线程调度
目的是为了更合理的利用CPU
分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。
2,同步与异步&并发与并行
2.1 同步与异步
同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全
2.2 并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
3,继承Thread
3.1 程序实例
3.2 时序图
3.3 补充
- 每个线程都有自己的栈空间,共用一份堆内存
- 子线程调用的run方法 都在子线程里面与运行 压栈 弹栈
4,实现Runnable
4.1 使用方法
另一种实现多线程的方法
- 创建自定义类实现Runnable接口,并重写run方法;
- 用自定义类创建一个对象r;
- 用Thread类创建一个对象t,并将r作为t构造方法的参数;
4.2 实现Runnable与继承Thread
1)实现Runnable与继承Thread相比有如下优势
- 1,通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况;
- 2,可以避免单继承所带来的局限性(Java允许实现多个接口,但不允许继承多个父类);
- 3,任务与线程是分离的,提高了程序的健壮性;
- 4,后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程;
2)Thread也有一定的好处
5,Thread类
5.1 常用构造方法
5.2 常用其他方法
停止线程的方法:声明一个变量,线程不断监控这个变量,一旦变量达到某种条件调用return即可
所有的用户线程结束,程序才能结束。守护线程是为了守护用户线程,用户线程可以自动结束,所有用户线程结束后,守护线程便会像没有一样。
6, 设置和获取线程的名称
currentThread获取当前线程名称
getName/setName 获取/设置线程的名称
7, 线程休眠sleep
8, 线程的中断阻塞
过时的stop方法可以直接中断线程,但是如果线程来不及释放资源,会造成一部分垃圾无法回收;
这里采用添加中断标记的方法:调用interrupt方法,子线程执行时捕获中断异常,并在catch块中,添加处理释放资源的代码;
9, 守护线程
9.1 概述
线程分为守护线程和用户线程;
- 用户线程:当一个进程不包含任何存活的用户线程时,进程结束;
- 守护线程:守护用户线程,当最后一个用户线程结束后,所有守护线程自动死亡;
直接创建的都是用户线程;
设置守护线程:线程对象.setDaemon(true);
9.2 实例
1)不设置守护线程
2)设置为守护线程
10, 线程安全1-同步代码块
10.1 线程不安全的原因
多个线程争抢同一个数据,使得数据在判断和使用时出现不一致的情况。解决方法,保证一段数据同时只能被一个线程使用(排队使用)。
解决方案一:同步代码块
格式:
synchronize(锁对象){
}
10.2 代码实例
1, 不加锁
2, 加锁后
package com.kaikeba;
import javax.xml.namespace.QName;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.Scanner;
import java.util.SimpleTimeZone;
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
//线程不安全
//解决方案1 同步代码块
//格式:synchronized(锁对象){
//
//
// }
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
}
}
}
}
}
11, 线程安全2-同步方法
同步代码块粒度较细,可以给一行代码单独加锁,同步方法顾名思义,是给方法加锁;
package com.kaikeba;
import javax.xml.namespace.QName;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.Scanner;
import java.util.SimpleTimeZone;
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
//线程不安全
//解决方案2 同步方法
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
}
给方法上锁,对应的锁对象就是this, 如果是静态修饰方法的话,锁对象为类名.class(比如这里sale方法若被修饰为静态方法的话,锁对象为Ticket.class,也就是字节码文件对象)
针对以上代码来说 ,锁对象如下:
12, 线程安全3-显示锁Lock
同步方法和同步代码块都属于隐式锁,显式锁则是程序员手动加锁、解锁;
lock(); //上锁
unlock(); //解锁
package Third5.java;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
public static void main(String[] args) {
//显示锁
Runnable runnable = new Ticket();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable{
//声明票数
private int count = 10;
//声明显示锁
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if(count>0){
System.out.println("开始卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"正在卖票,剩余票数"+count);
}else {
break;
}
lock.unlock();
}
}
}
}
13, 公平锁与非公平锁
13.1 区别
公平锁:先来先得,遵循排队;
非公平锁:大家一起抢(同步代码块,同步方法,显式锁都属于非公平锁);
13.2 实现方法
在显式锁实例化时,传入参数true()
14, 多线程通信问题
主要借助于wait和notify函数实现
15, 生产者与消费者
15.1 前提条件
厨师cook为生产者线程,服务员waiter为消费者线程,食物为生产与消费的物品;
假设目前只有一个厨师,一个服务员,一个盘子。理想状态是:厨师生产一份饭菜,服务员端走一份,且饭菜的属性未发生错乱;
厨师可以制作两种口味的饭菜,制作100次;
服务员可以端走饭菜100次;
15.2 问题一: 饭菜的属性错乱
1)实验代码
package Third5.java;
import java.security.PrivateKey;
import java.util.PrimitiveIterator;
public class CookDemo {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Writer(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
for (int i =0;i<100;i++){
if (i % 2 ==0){
f.setNameAndSaste("老干妈小米粥","香辣味");
}else {
f.setNameAndSaste("煎饼果子","甜辣味");
}
}
}
}
//服务员
static class Writer extends Thread{
private Food f;
public Writer(Food f){
this.f = f;
}
@Override
public void run() {
for (int i =0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
//菜品名称
private String name;
//菜品味道
private String taste;
//为true 厨师做饭
private boolean flag = true;
public synchronized void setNameAndSaste(String name,String taste){
if (flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if (!flag){
System.out.println("菜品名称"+name+",菜品味道"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2)错误现象
3)错误原因
15.3, 问题二:一次性消费/生产多个菜品
1)实验代码
为了防止在生产过程中setNameAndTaste出现时间片切换,可以用synchronized修饰此方法;
public synchronized void setNameAndTaste(String name,String taste){// 生产
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public synchronized void get(){ // 消费
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
}
2)运行效果
3)原因分析
synchronized只是确保了方法内部不会发生线程切换,但并不能保证生产一个消费一个的逻辑关系;
15.4 解决方法
厨师做完饭后喊醒服务员,自己睡着。服务员送完饭后喊醒厨师,自己睡着;
主要修改的部分为setNameAndTaste与get方法:
public synchronized void setNameAndTaste(String name,String taste){// 生产
if(flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false; // 表示饭菜生产完毕
this.notifyAll(); // 叫醒服务员
try {
this.wait(); // 睡着
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){ // 消费
if(!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
可以看出饭菜是交替产生并消费的;
16,线程的六种状态
new 线程创建 未启动
Runnable 执行中
Blocked 被阻塞
Waiting 无限期等待
Timed_Waiting 指定时间等待
TermInated 退出线程
17, 带返回值的线程Callable
新的创建线程的方式。之前的创建线程方式:实现Thread的子类、实现Runnable接口,可以看成是和主线程并发执行的;
这里要讲的线程更像是主线程指派的一个任务,主线程可以获得其返回值;
17.1 Runnable与Callable
1)接口定义
接口定义
//Callable接口
public interface Callable<V> {
V call() throws Exception;
}
//Runnable接口
public interface Runnable {
public abstract void run();
}
2)相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
3)不同点
Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
17.2 Callable使用步骤
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
- 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
- 通过Thread,启动线程
new Thread(future).start();
17.3 常用方法
1)方法介绍
2)代码示例
package com.kaikeba;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Demo1 {
public static void main(String[] args) {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> f = new FutureTask<>(c);
new Thread(f).start();
for(int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + i);
}
}
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// Thread.sleep(100);// 睡眠过后 给出结果
for(int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + i);
}
return 100;
}
}
}
未使用get方法时,主线程和另一个线程交替执行
使用get方法
18, 线程池概述
18.1 为什么需要线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间.
线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
Java中的四种线程池(对象均为ExecutorService)
18.2 缓存线程池
1)概述
/*** 缓存线程池.
* (长度无限制)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2)代码示例
package com.kaikeba;
import java.util.concurrent.*;
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*** 缓存线程池.
* (长度无限制)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
// 指挥线程池执行新的任务
service.execute(new Runnable() {// 匿名内部类
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "滴滴滴");
}
});
service.execute(new Runnable() {// 匿名内部类
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "滴滴滴");
}
});
service.execute(new Runnable() {// 匿名内部类
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "滴滴滴");
}
});
}
}
睡眠一段时间后,添加新的任务,查看是否能利用线程池中已存在的线程:
18.3 定长线程池
1)概述
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2)代码示例
设定线程池大小为2,即线程池中最多只允许存在两个线程;
前两个线程执行时,均sleep三秒钟;
第三个任务由于线程池已满,不能开辟新的线程,所以必须等线程池中有空闲线程出现才可以执行;
package com.kaikeba;
import java.util.concurrent.*;
public class Demo1 {
/*定长线程池
长度是指定的线程池
加入任务后的执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
**/
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
18.4 单线程线程池
1)概述
效果与定长线程池 创建时传入数值1 效果一致.
/**
* 单线程线程池.
* 执行流程:
* 1. 判断线程池 的那个线程 是否空闲
* 2. 空闲则使用
* 3. 不空闲,则等待 池中的单个线程空闲后 使用
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2)代码示例
package com.kaikeba;
import java.util.concurrent.*;
public class Demo1 {
/*单线程线程池
执行流程
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用
**/
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
18.5 周期定长线程池
1)概述
public static void main(String[] args) {
/**
* 周期任务 定长线程池.
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务 .
*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
/*
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
}
2)代码示例
package com.kaikeba;
import java.util.concurrent.*;
public class Demo1 {
/*周期任务 定长线程池
执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
周期性任务执行时
定时执行 当某个任务触发时 自动执行某任务
**/
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//定时执行一次
//参数1:定时执行的任务
//参数2:时长数字
//参数3:2的时间单位 Timeunit的常量指定
/* scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5, TimeUnit.SECONDS); //5秒钟后执行*/
/*
周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次执行延迟的时间)
参数3:周期时长数字(每隔多久执行一次)
参数4:时长数字的单位
* **/
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5,1,TimeUnit.SECONDS);
}
}
19, Lanmbda表达式
19.1 为什么要用lambda表达式
对于某些应用场景,我们更注重于结果,如果能用一个方法解决,那么通过创建对象、调用方法的方式可能会更加繁琐;
1)冗余的Runnable方法
public class Demo1 {
/**
* lambda表达式
* 函数式编程思想(注重结果,而面向对象则是通过创建对象,解决问题)
* @param args
*/
public static void main(String[] args) {
// 冗余的Runnable代码
Runnable r = new MyRunnable();
Thread t = new Thread(r);
t.start(); // 写了这么多 只为了完成一个简单的任务
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("任务完成!");
}
}
}
2)通过匿名内部类简化代码
public class Demo1 {
/**
* lambda表达式
* 函数式编程思想(注重结果,而面向对象则是通过创建对象,解决问题)
* @param args
*/
public static void main(String[] args) {
// 冗余的Runnable代码
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("任务完成!");
}
});
t.start(); // 写了这么多 只为了完成一个简单的任务
}
}
19.2 使用实例
1)不使用lambda
package com.kaikeba;
import java.util.concurrent.*;
public class Demo1 {
/**
* lambda表达式
* 函数式编程思想(注重结果,而面向对象则是通过创建对象,解决问题)
* @param args
*/
public static void main(String[] args) {
print(new MyMath() {
@Override
public int sum(int x, int y) {
return x + y;
}
}, 100, 200);
}
public static void print(MyMath m, int x, int y){
int num = m.sum(x, y);
System.out.println(num);
}
static interface MyMath{
int sum(int x, int y);
}
}
2)使用lambda
不需要实现接口、实例化对象
package com.kaikeba;
import java.util.concurrent.*;
public class Demo1 {
/**
* lambda表达式
* 函数式编程思想(注重结果,而面向对象则是通过创建对象,解决问题)
* @param args
*/
public static void main(String[] args) {
print((int x, int y) -> {
return x + y;
}, 100, 200);
}
public static void print(MyMath m, int x, int y){
int num = m.sum(x, y);
System.out.println(num);
}
static interface MyMath{
int sum(int x, int y);
}
}