多线程
概念
是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
线程与进程的区别?
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
线程的创建方法
多线程的实现方式(1) 继承Thread类
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用是完成一定的任务,实际上就是执行一段程序流,即一段顺序执行的代码。
步骤:
- 定义一个类MyThread继承Thread类.
- 在MyThread类中重写run()方法.
- 创建MyThread类的对象.
- 启动线程.
案例:
//1、自己定义的线程类 继承了Thread类之后MyThread就具备线程的相关特性(Java虚拟机就可以识别)
public class MyThread extends Thread{
//2、创建自定义线程类目的就是去单独让线程作为载体执行一些代码(重写父类的run方法)
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyThread自定义线程类run:"+i);
}
}
}
//测试:
public class ThreadTest1 {
public static void main(String[] args) {
//3、真正想要让cpu去指定声明好的自定义线程的功能 创建自定义线程类对象
MyThread myThread = new MyThread();
//4、启动线程
myThread.start();
//myThread的确作为一个单独的线程被cpu执行的,而不是main线程去执行的
for (int i = 0; i < 100; i++) {
System.out.println("ThreadTest1的main线程:"+i);
}
}
}
多线程的实现方式(2)实现Runable接口
步骤:
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
案例:
//1、自定义类(实现Runnable:线程任务接口) 自定义的类就具备了线程任务的规范
public class MyRunnable implements Runnable{
//实现run方法(当线程启动的时候,要执行的代码功能)
@Override
public void run() {
System.out.println("线程任务的run方法执行了,对象名称:"+Thread.currentThread().getName());
}
}
//测试:
public class RunnableTest1 {
public static void main(String[] args) {
//3、创建自定义的线程任务(创建完成之后已经有了可以被基于线程作为载体去执行的线程)
MyRunnable myRunnable = new MyRunnable();
//4、创建Thread类对象,并且在创建的时候将线程任务作为参数传进去
Thread t = new Thread(myRunnable);
t.setName("线程A");
//5、启动线程 当启动线程了之后,通过Java虚拟机开辟线程栈,执行线程任务的run方法
t.start();
}
}
总结:
1.线程对象调用start方法,start方法通知JVM为当前线程对象开辟新的方法栈执行空间, 执行当前线程对象的run方法
2.每个线程的run方法执行空间属于线程私有
3.某一个线程运行中出现异常,不会影响其它线程的执行(不考虑同步: 锁的问题)
注意事项:
-
开启线程调用start()方法,而不是run方法.
-
为什么要重写run()方法?
因为run()是用来封装被线程执行的代码.
-
run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。
start():启动线程,然后由JVM调用此线程的run()方法.
线程的基本设置
方法名 | 说明 |
---|---|
String getName() | 返回此线程的名称,如果不设置名字,则线程名默认为Thread-N |
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为等于参数name |
方法名 | 说明 |
---|---|
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
某些情况下,非Thread类子类对象没办法调用getName方法获取名称,我们可以通过此方法获取当前正在执行的线程对象再获取名字.
线程安全
线程安全以卖票案例进行讲解和分析
线程安全问题的引发
- 卖票案例的重复票问题
原因:
例如有三个线程A、B、C假如线程A执行到此语句后进入等待状态,B也执行到此语句后进入到等待状态,那么C线程执行到此语句后输出的票数都是100张,导致票数重复问题,所以导致此问题的原因是线程执行的无序性导致。
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticketCount+"张票");
-
卖票案例的负数票问题
原因:
还是三个线程A、B、C假如票数就剩下一张然而线程A执行到if判断语句>0符合条件后进入等待状态,线程B也执行到if判断语句>0符合条件后进入等待状态,线程C也执行到if判断语句>0符合条件后进入等待状态。这样之后三个线程全符合条件进入到了if,A线程往下执行打印票数为1然后–票数为0,B线程往下执行打印票数为0又–票数为-1,线程C往下执行打印票数为-1,这样就出现了票数为负的情况。
解决线程安全问题的方法
-
同步代码块与同步方法
-
同步代码块:
-
锁多条语句操作共享数据**,**可以使用同步代码块实现.
-
格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
-
默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭.
当线程执行完出来了,锁才会自动打开.
-
同步的好处和弊端:
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效 率.
-
-
同步方法:
-
同步方法:就是把synchronized关键字加到方法上
-
格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
-
同步方法的锁对象是什么呢?
当前的调用者对象
-
-
-
Lock锁
-
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁。
-
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
-
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- void lock():获取锁
- void unlock():释放锁
-
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
-
注意:在线程的执行过程中,有可能出现异常,导致unlock()方法并不会被执行,不执行就不会释放锁对象.
所以要使用try…catch…finally方式将释放锁的代码写在finally代码块中进行执行.
-
线程安全问题的解决方案(卖票为例)
-
方案一同步代码块和同步方法
案例:
同步代码块:
public class TicketTask implements Runnable{ //多线程操作共享资源的方法论:将共享资源定义为所有线程都可以访问到的类的成员变量 private int ticketCount=100; private static final Object OBJECT_LOCK=new Object(); @Override public void run() { //模拟窗口一直处于卖票的状态 while (true){ //同步代码块=> 可以先选中要加锁的代码。 //()里面要声明一个锁对象:在同步代码块中可以是任意对象,虽然可以是任意对象,必须保证全局唯一 //错误的情况:new Object();思路:Object类没有任何创建对象的意义。 //推荐情况:(1)如果线程任务全局只有一个 可以使用this作为锁对象 //推荐的情况:(2)在线程任务类中声明一个私有的静态的不可变的Object类对象并完成初始化作为锁对象。 synchronized (OBJECT_LOCK) { if (ticketCount>0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+ticketCount+"张票"); ticketCount--; try { Thread.sleep(50);//模拟出票过程 } catch (InterruptedException e) { e.printStackTrace(); } }else { break; } } } } } //test public class TicketTest { public static void main(String[] args) { //创建卖票的线程任务,由于所有的线程的卖票逻辑都是一样的(创建一个线程任务就可以了) //补:Thread类除了有直接传递线程任务的构造方法之外,还有一个(Runnable target,String name)构造方法 TicketTask ticketTask = new TicketTask(); //创建线程并指定线程任务 Thread tOne = new Thread(ticketTask, "窗口A"); Thread tTwo = new Thread(ticketTask, "窗口B"); Thread tThree = new Thread(ticketTask, "窗口C"); tOne.start(); tTwo.start(); tThree.start(); } }
同步方法:
public class TicketTask implements Runnable{ //多线程操作共享资源的方法论:将共享资源定义为所有线程都可以访问到的类的成员变量 private int ticketCount=100; @Override public void run() { //模拟窗口一直处于卖票的状态 while (true){ ticketTask(); if (ticketCount==0){ break; } } } //同步方法:和同步代码块一样,可以将需要一个线程完整执行的内容抽取为一个方法,和普通的方法的区别在于,加一个synchronized关键字 //相较于同步代码块,同步方法的锁对象不需要主动声明,默认就是【this(当前方法的调用者)】,可以保证锁对象唯一 //同步方法其实在之后的实际开发中用的还是比较多的(相较于同步代码块而言) public synchronized void ticketTask(){ if (ticketCount>0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+ticketCount+"张票"); ticketCount--; try { Thread.sleep(50);//模拟出票过程 } catch (InterruptedException e) { e.printStackTrace(); } } } } //test public class TicketTest { public static void main(String[] args) { //创建卖票的线程任务,由于所有的线程的卖票逻辑都是一样的(创建一个线程任务就可以了) //补:Thread类除了有直接传递线程任务的构造方法之外,还有一个(Runnable target,String name)构造方法 TicketTask ticketTask = new TicketTask(); //创建线程并指定线程任务 Thread tOne = new Thread(ticketTask, "窗口A"); Thread tTwo = new Thread(ticketTask, "窗口B"); Thread tThree = new Thread(ticketTask, "窗口C"); tOne.start(); tTwo.start(); tThree.start(); } }
-
方案二Lock锁
案例:
public class TicketTask2 implements Runnable{ //多线程操作共享资源的方法论:将共享资源定义为所有线程都可以访问到的类的成员变量 private int ticketCount=100; //声明一个Lock锁对象作为线程任务类的成员变量 private static final Lock LOCK=new ReentrantLock(); @Override public void run() { //模拟窗口一直处于卖票的状态 while (true){ //通过创建的Lock锁对象在需要加锁的代码前进行获取锁的操作 try { LOCK.lock(); if (ticketCount>0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+ticketCount+"张票"); ticketCount--; Thread.sleep(50);//模拟出票过程 //通过创建的Lock锁对象在需要加锁的代码前进行释放锁的操作 }else { break; } } catch (InterruptedException e) { e.printStackTrace(); }finally { //无论是try执行了还是catch执行了,最终都会执行finally代码块的内容(最终LOCK锁解决方案) LOCK.unlock(); } } } } //test public class TicketTest { public static void main(String[] args) { //创建卖票的线程任务,由于所有的线程的卖票逻辑都是一样的(创建一个线程任务就可以了) TicketTask ticketTask = new TicketTask(); //创建线程并指定线程任务 Thread tOne = new Thread(ticketTask, "窗口A"); Thread tTwo = new Thread(ticketTask, "窗口B"); Thread tThree = new Thread(ticketTask, "窗口C"); tOne.start(); tTwo.start(); tThree.start(); } }
死锁
死锁案例:
public class DeadLockDemo1 {
public static void main(String[] args) {
//声明两个锁对象
final Object LOCK_A=new Object();
final Object LOCK_B=new Object();
//基于匿名内部类完成Runnable接口规范的实现并且作为一个实现类对象传递
Thread threadA= new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (LOCK_A){
//如果进入到同步代码块中,说明线程A已经获取到了LOCK_A锁,打印对应的信息,休眠500毫秒
System.out.println(Thread.currentThread().getName()+"已经获取到LOCK_A锁,休眠500毫秒");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+"休眠结束,准备拿LOCK_B锁");
//如果进入到同步代码块中,说明线程A既获取到了LOCK_A锁也获取到了LOCK_B锁
synchronized (LOCK_B){
System.out.println("获取最终资源");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程A");
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + "已经获取到LOCK_B锁,休眠500毫秒");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "休眠结束,准备拿LOCK_A锁");
synchronized (LOCK_A) {
System.out.println("获取最终资源");
}
}
} catch(InterruptedException e){
e.printStackTrace();
}
}
},"线程B");
threadA.start();
threadB.start();
}
}
死锁问题的解决方式:
- 减少竞争资源(之前两把锁,改为一把锁)
- 如果多把锁的情况下保证获取锁的顺序。
生产者消费者案例
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
注意:这几个方法在Object类中Object类的等待和唤醒方法
//将此类作为生产者与消费者线程的共享资源
public class Baozi {
private Integer number;//包子的编号
private Boolean flag;//是否有包子
public Baozi() {
}
public Baozi(Integer number, Boolean flag) {
this.number = number;
this.flag = flag;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
@Override
public String toString() {
return "Baozi{" +
"number=" + number +
", flag=" + flag +
'}';
}
}
//消费者
public class ConsumerTask implements Runnable{
private Baozi baozi;
public ConsumerTask(Baozi baozi) {
this.baozi = baozi;
}
@Override
public void run() {
while (true){
synchronized (baozi){
try {
if (!baozi.getFlag()){
//让消费者线程进入到无限等待状态(将锁释放掉:Cpu就可以选择生产者线程执行了)
baozi.wait();//唤醒之后直接往下执行,不用重新循环
}
System.out.println(Thread.currentThread().getName() + " 消费 编号为[ " + baozi.getNumber() + " ]的包子!");
baozi.setFlag(false);
Thread.sleep(500);
baozi.notifyAll();//唤醒生产者
//让消费者线程进入到无限等待状态(将锁释放掉:Cpu就可以选择生产者线程执行了)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//生产者
public class ProducerTask implements Runnable{
private Baozi baozi;
public ProducerTask(Baozi baozi) {
this.baozi = baozi;
}
@Override
public void run() {
while (true){
synchronized (baozi){
try {
if (baozi.getFlag()){
baozi.wait();//唤醒之后直接往下执行,不用重新循环
}
//当前没有包子(生产包子 + 修改标记 + 唤醒消费者[因为消费者线程可能处于无限等待状态])
baozi.setNumber(baozi.getNumber()+1);
System.out.println(Thread.currentThread().getName() + " 生产 编号为[ " + baozi.getNumber() + " ]的包子!");
baozi.setFlag(true);
Thread.sleep(500);//模拟包子生产过程
baozi.notifyAll();//唤醒消费者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Baozi baozi = new Baozi(0, false);
ProducerTask producerTask = new ProducerTask(baozi);
ConsumerTask consumerTask = new ConsumerTask(baozi);
Thread proThread = new Thread(producerTask, "生产者");
Thread conThread = new Thread(consumerTask, "消费者");
proThread.start();
conThread.start();
}
}
线程池
线程池的概念
- 线程池(Thread Pool)是一种基于池化思想管理线程的工具。
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,导致资源调度失衡,使用线程池可以进行统一的分配、调优和监控。
自定义线程池
-
ThreadPoolExecutor类就是Java中的线程池类,只因为其构造比较复杂,所以Java提供了工具类快速获取该类对象。但是实际开发中不允许使用工具类快速获取。都是根据开发的实际情况进行创建。
-
线程池构造方法的7个参数:
1.int corePoolSize : 核心线程数(不会销毁的线程)
核心线程数根据线程池的线程任务的分类去计算
-
IO密集型:(读写文件 对于Cpu的占用不大) 当前部署项目机器的核心数 * 2
-
Cpu密集型:(大量的计算 10w订单:平均客单价 占用大量Cpu资源) 当前部署项目机器的核心数 + 1
2.int maximumPoolSize : 最大线程数(临时线程数 + 核心线程数) 不是临时线程数,是需要计算得到的结果.
最大线程不能小于核心线程数,可以等于核心线程数 = 没有临时线程(可以 大部分情况都没有临时线程)
3.long keepAliveTime : 临时线程过期时间(数值单位) 10 / 20 / 30 一般与参数4搭配使用
当临时线程超过多长时间没有工作的时候,进行销毁 小贴士:没有临时线程 参数3都会传递0
4.TimeUnit unit : 临时线程过期时间(时间单位) 基于TimeUnit声明的相关枚举项来选择对应的时间单位
TimeUnit.DAYS / TimeUnit.HOURS / TimeUnit.MINUTES / TimeUnit.SECONDS / TimeUnit.MILLISECONDS
5.BlockingQueue queue : 阻塞队列,用于保存还没有执行的线程任务
new ArrayBlockingQueue(长度) : 有界阻塞队列 / new LinkedBlockingQueue() : 无界阻塞队列
6.ThreadFactory factory : 线程工厂 负责生产线程池中的线程对象
默认:Exectors.defaultThreadFactory();
7.RejectedExecutionHandler : 默认拒绝策略
AbortPolicy:丢弃任务抛异常 DiscardPolicy:丢弃任务不抛异常
DiscardOldestPolicy:丢弃等待时间最长的任务,重新提交本次任务 CallerRunsPolicy:让提交线程执行线程任务
-
-
创建线程池案例
public class ThreadPoolDemo2 { public static void main(String[] args) { //线程池:ThreadPoolExecutor类 //注意:默认拒绝策略的传递:ThreadPoolExecutor类中有4个静态内部类(实现了拒绝策略接口),传递拒绝策略对象(创建静态内部类对象new 外部类名.内部类名()) //需求(1):创建一个核心线程数为10,临时线程数5(20秒过期销毁),可以保存10个线程任务,使用默认拒绝策略的线程池 ThreadPoolExecutor poolOne = new ThreadPoolExecutor(10, 15, 20, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); //需求(2):创建一个核心线程数为5,没有临时线程,可以保存N个线程任务 //如果是完整的需求2,当给出了无界阻塞队列的时候(不传递拒绝策略) ThreadPoolExecutor poolTwo = new ThreadPoolExecutor(5, 5, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory()); //需求(3):创建一个核心线程数为10,临时线程数5(10分钟过期销毁),可以保存5个线程任务,使用(多余任务丢弃不抛出异常)拒绝策略的线程池 ThreadPoolExecutor poolThree = new ThreadPoolExecutor(10, 15, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); //需求(4):创建一个核心线程数为10,临时线程数5(20天过期销毁),可以保存10个线程任务,使用(提交线程执行线程任务)默认拒绝策略的线程池 ThreadPoolExecutor poolFour = new ThreadPoolExecutor(10, 15, 20, TimeUnit.DAYS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); } }
//需求(3):创建一个核心线程数为10,临时线程数5(10分钟过期销毁),可以保存5个线程任务,使用(多余任务丢弃不抛出异常)拒绝策略的线程池 ThreadPoolExecutor poolThree = new ThreadPoolExecutor(10, 15, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); //需求(4):创建一个核心线程数为10,临时线程数5(20天过期销毁),可以保存10个线程任务,使用(提交线程执行线程任务)默认拒绝策略的线程池 ThreadPoolExecutor poolFour = new ThreadPoolExecutor(10, 15, 20, TimeUnit.DAYS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); }
}