java多线程
文章正文:
https://blog.csdn.net/beidaol/article/details/89135277
创建线程的几种方式
创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。
调用run()方法后,主线程去执行完run()方法后再执行主线程的方法;
调用start()方法后,会新建一个子线程去执行run()方法,主线程和子线程交替执行。
通过继承Thread类来创建并启动线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
通过实现Runnable接口来创建并启动线程的步骤如下:
- 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
- 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
- 调用线程对象的start()方法来启动该线程。
通过实现Callable接口来创建并启动线程的步骤如下:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
通过继承Thread类、实现Runnable接口、实现Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
-
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
-
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
-
劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
-
采用继承Thread类的方式创建多线程的优缺点:
-
劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
-
优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
线程休眠sleep
每个对象都有一个锁,sleep()不会释放锁
线程礼让yield
Thread.yield();
- 礼让线程,让当前线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功!看cpu心情
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程开始执行");
}
如果礼让成功,会让出cpu给b执行
否则cpu继续执行a
线程强制执行 join
插队执行线程
Thread.join()
观测线程状态
Thread.State state = thread.getState();
线程的优先级
thread.setPriority(4);
守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如后台记录操作日志,监控内存,垃圾回收等待
thread.setDaemon(true);//默认是false表示用户线程
线程同步机制
-
并发:同一个对象被多个线程同时操作,每个对象都有锁
-
由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制
synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题: -
一个线程持有锁会导致其他所有需要此锁的线程挂起
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
三大不安全案例
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
输出结果小于10000,因为ArrayList不安全,两个线程同一时间给同一位置赋值时覆盖掉了,所以小于10000
同步方法及同步块
- synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
- 方法里面需要修改的内容才需要锁,锁太多,浪费资源
- synchronized同步方法锁的是this,当前对像
- synchronized同步块可以锁任何东西
例如以下代码同步方法锁的是BuyTicket
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
// 外部停止
boolean flag = true;
@Override
public synchronized void run() {
// 买票
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
if (ticketNums <=0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
}
}
以下代码同步块锁的是account
class Account{
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawing extends Thread{
Account account;
int drawingMoney;
int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//所得对象是变化的量,需要增删改的
synchronized (account) {
if (account.money-drawingMoney<0) {
System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");
return;
}
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account+"余额为:"+account.money);
}
}
死锁
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种首尾相接的循环等待资源关系
上面列出的四个必要条件,只要想办法破其中一个或多个条件就可以避免死锁发生
LOCK锁
- Lock是显式锁(手动开启和关闭锁)syncronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,syncronized有代码块和方法锁
- 使用Lock锁,jvm将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体外)
@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (ticketNum > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNum--);
} else {
break;
}
} finally {
//解锁
lock.unlock();
}
}
}
管程法
执行wait()方法时,this(当前对像)线程会停这里,直到notify()唤醒才往下执行
信号灯法
线程池
线程池的优势
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
线程池的主要参数
(1)corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
(2)maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
(3)keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
(4)workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
(5)threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
(6)handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
线程池流程
1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
锁升级
无锁
->synchronized
->偏向锁(自己访问同步代码块不需要重新获取锁,省时间。对象头的MARK WORD里会保存线程ID)
->其他线程请求偏向锁(锁竞争)
->轻量级锁(为了减少用户态和内核态之间的开销,其他线程请求轻量级锁会自旋)
->自旋10次(通过虚拟机设定次数,默认10次)
->重量级锁(请求重量级锁的线程,直接转为阻塞状态,等待唤醒,这需要在用户态和内核态之间切换,耗时)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示