Java多线程基础

一、基本概念

程序:

为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象

进程(process):

程序的依次执行过程,或是正在内存中运行的应用程序。如:运行的微信,运行的有道云笔记,运行的360安全软件等等。

  • 每个进程都有一个独立的内存空间(所以多进程下全局变量不能共享),系统运行一个程序即是一个从进程从创建,就绪,运行到消亡的过程(声明周期)
  • 程序是静态的,进程是动态的。
  • 进程作为操作系统调度和分配资源的最小单位(也是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
  • 现代的操作系统,大都支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板
  • 进程之间不共享内存,即全局变量不能共享。

线程(thread):

进程可进一步细化线程,是程序内部的一条执行路径。一个进程至少有一个线程。比如360安全软件可以一边杀毒,一边清理垃圾文件,一边进行启动项优化;像QQ开多个窗口等等。

  • 一个进程同一时间若并行执行多个线程,就是支持多线程的。就是操作系统的任务调度器能在不同的时间切换不同的线程,如下是简单的进程线程图

  • 线程作为CPU调度和执行的最小单位
  • 一个进程中的多个线程共享相同的内存单元,他们从同一个内存堆中分配对象,可以访问相同的变量和对象。这使得线程间通信更简便、高效。但多线程操作共享的系统资源可能会带来安全的隐患
  • 下图是多线程的JVM实例图:

JVM结构图.png

线程和进程的区别:

  • 进程一个运行中的程序。
  • 运行中的进程的一条或多条执行路径

线程的应用场景:

  • 手机app应用的图片下载
  • 迅雷下载
  • Tomcat的web应用,多个客户端发起的请求,Tomcat针对多个请求开辟多个线程进行处理

并行和并发的概念

  • 并行:指两个或多个事件在同一个时间点发生(同时发生)。指在同一个时间点,多条指令在多个CPU上同时执行。比如,多个进程同时做不同的任务。比如一套采集系统,放在不同的服务器上爬取优秀的Java学习资料,其中A服务器只负责的是CSDN,B服务器只负责的是51cto,C服务器只负责的是稀土掘金等等。
  • 并发:指两个或多个事件在同一个时间段内发生。即在同一个时间段内,有多条指令在单个CPU上``快速轮换,交替执行,使得宏观上具有多个进程同时执行的效果。

二、线程的启动和创建

概述

  • Java语言的JVM运行程序运行多个线程,使用java.lang.Thread类代表线程,所有的线程对象都必须Thread类或其子类的实例。
  • Thread类的特性
    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为线程执行体
    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
    • 要想实现多线程,必须在主线程中创建新的线程对象。

实现方式1:继承Thread类

java通过继承Thread类来创建并启动多线程的步骤如下:

  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run()方法—>将此线程需要执行的操作,声明在此方法体中
  3. 创建Thread子类的实例,即创建了线程对象
  4. 调用线程对象的start()方法来启动该线程

实现方式2:实现Runnable接口

由于Java是单继承模式,如果一个类在继承了Thread类之后就不能再去继承其他类了,这样子处理业务有局限性,所以就通过实现Runnable接口的方式来实现多线程,同时又能继承其他类。

  1. 创建一个实现Runnable接口的类
  2. 实现接口中的run()方法—>将此线程需要执行的操作,声明在此方法体中
  3. 创建当前实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例,(其实此对象其实传递的是共享数据)
  5. Thread类的实例调用start();1.启动线程,2.调用当前线程的run()

两种方式的对比

  • 共同点:
    • 1、启动线程,使用的都是Thread类中定义的start()
    • 2、创建线程对象,都是Thread类或其子类的实例。
  • **不同点:**Thread是类的继承,Runnable是接口的实现。
    • Runnable接口实现可以当作是一个任务处理,继承Thread就是当作是一个子线程
    • 建议:使用实现Runnable接口的方式
    • Runnable方式的好处:
      • 1、实现的方式,避免类的单继承的局限性
      • 2、更适合处理有共享数据的问题。
      • 3、实现了代码和数据的分离。
  • 代码格式不同:
    • 继承自Thread类:
public class MyThreadTest {
    public static void main(String args) {
        MyThread t = new MyThread();
        t.start();	//①启动线程,②调用t1.run()
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        //子线程的逻辑处理
        ...
    }       
}
  • 实现Runnable接口:
public class MyTheadTest2 {
    public static void main(String args) {
        //创建一个MyRunnable对象作为一个任务传给线程
        MyRunnable task = new MyRunnable();
        MyThread2 t = new Thread(task);
        t.start
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        //子线程的逻辑处理
        ...
    }
}
  • 两者关系:其实Thread类也是基于继承字Runnable接口来实现的
    • public class Thread implements Runable

三、线程的常用结构

线程中的构造器

  • public Thead():分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象,并指定名字

线程中的常用方法

  • start():①启动线程,②调用线程的run()方法
  • run():将线程中要执行的操作,声明在run()中。
  • currentThread():静态方法,获得当前执行代码对应的线程名
  • setName(String name):给当前线程设置名字
  • getName():获得当前线程的名字
  • sleep(long millSecond):静态方法,当前线程休眠的指定毫秒数
  • yield():静态方法,一旦执行此方法,当前线程就先暂停运行,释放CPU的执行权,切换给其他线程。等重新获得执行权后,接着上一个位置继续往下执行
  • join():等待某个线程结束,在线程a通过线程b调用join(),意味着线程a进入阻塞状态,直到b线程执行结束,线程a才结束阻塞状态,继续往下执行。使用场景:a线程需要通过b线程获得的数据共享,才能处理下一步逻辑。像b线程在做网络请求获取数据,需要一定的时间,可以用这种方式。
  • isAlive():线程是否存活。
  • stop():不建议使用,已过时。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理工作无法完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有锁,导致数据得不到同步处理,出现数据不一致的问题。
  • suspend() / resume():不建议使用,已过时。这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则容易死锁,因为suspend()调用会导致线程暂停,但不会释放锁,导致其他线程都无法访问被它占用的锁,直到调用resume()。

线程的优先级

每个线程都有一定的优先级,同时优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
在单核CPU下的运行效果明显,在多核CPU下,不怎么明显。

  • Thread类的三个优先级常量
    • MAX_PRIORITY(10):最高优先级
    • MIN_PRIORITY(1):最低优先级
    • NORM_PRIORITY(5):普通优先级,默认情况下main线程具有普通优先级,即使main优先级最高
  • public final int getPriority():返回线程优先级
  • public final void setPriority(int newPriority):改变线程的优先级,范围在[1,10]之间

4 线程的生命周期

Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:

  • JDK1.5之前5种状态:

image.png

  • JDK1.5之后的6种状态:

JDK1.5之后的线程状态.png
或借鉴网上找的图:
image.png

四、线程安全

场景:库存超卖

  1. 由于秒杀业务是属于高并发的,需要使用多线程。当一个线程在处理并发请求时,遇到了耗时业务时可能会阻塞,其实流程并未结束,还没减库存,CPU把执行权让给其他线程;而其他线程进来还是认为库存>0的,继续往下执行,遇到耗时业务的时候又阻塞,同样也是流程没走完;
  2. 在耗时业务结束后,线程拿到CPU执行权,继续下一个流程即对库存进行减操作,当最后一个库存=1时,同时又存在多个线程来完成还未结束的流程进行减库存时,超卖现象就出现了。
  3. 因为库存是一份共享数据,所以那些未执行完流程的线程继续来对同一个库存数据进行减操作,导致库存变成负数

解决方案:同步机制

采用线程同步机制来处理,即必须保证当前线程A在减库存时,其他线程必须等待,直到线程A操作库存结束以后,其他线程才可以进来继续操作库。

方式1:同步代码块
  • 代码格式:
...
synchronized(同步监视器) {
    //需要被同步的代码
}
...
  • 说明:
    • 需要被同步(上锁)的代码即为操作共享数据的代码。
    • 共享数据:即多个线程需要操作的数据,比如:库存
    • 需要被同步的代码,在被synchronized包裹以后,就是的一个线程在操作这些代码过程中,其他线程必须等待。
    • 同步监视器,俗称锁。哪个线程获取了锁,那个线程就能执行需要被同步的代码。
    • 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个监视器
  • 注意:
    • 在实现Runnable接口的方式中,因对象实例共享的是同一份,所以同步监视器可以考虑使用:this
    • 在继承Thread类的方式中,因为多个线程是多个实例对象,同步监视器要慎用this,可以使用当前类.class或者使用一个静态的对象来表示
方式2:同步方法
  • Runnable接口实现的同步监视器:
// 此时的同步监视器是:this。外部线程调用的对象实例 xxx 是一个共享数据,是唯一的
public synchronized void showInfo() {
    ....
}
  • 继承Thread类的同步监视:
// 此时同步监视器为当前类本身,即为ThreadSyncMethod.class
public synchronized void rushToBuy() {
    ...
}
  • 说明:
    • 如果操作共享数据的代码完整的声明在一个方法中,那么我们就可以将此方法声明为同步方法即刻
    • 非静态的同步方法,默认同步监视器为this
    • 静态的同步方法,默认同步监视是当前类.class
同步机制的利弊:
  • 好处:解决了线程的安全问题
  • 弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。

同步机制带来的死锁

  • 概念:

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。

  • 如何看待死锁?

    • 我们编写程序时,要避免死锁
  • 诱发死锁的原因?

不管是同步代码块还是同步方法,两个线程s1,s2同时相互等待对方释放锁的时候,就会产生死锁;即线程s1的程序调用了s2的程序,s2里面上了同步锁,同时线程s2的程序也调用了s1的程序,s1里面也上了同步锁。这就会造成同时等待锁的释放,但都因为阻塞了,无法释放导致死锁了。

  • 互斥条件
  • 占用且等待
  • 不可抢夺
  • 循环等待

以上4个条件,同时出现就会触发死锁。

  • 如何解决死锁?

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

  • 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
  • 针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
  • 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
  • 针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

jdk1.5的Lock锁

除了使用synchronized同步机制处理线程安全问题处理外,还可以使用jdk5.0提供的ReentrantLock锁的方式,实际于JUC的。

  • 创建Lock锁的步骤:
    • 1、创建Lock的实例,需要确保多个线程共用同一个Lock实例,需要考虑将此对象声明为static final
    • 2、执行lock()方法,锁定共享资源的调用
    • 3、unlock()的调用,释放对共享数据的锁定
  • synchronized的同步方式与Lock的对比?
    • synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后释放对 同步监视器的调用。
    • Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。

五、线程间通信

线程之间通信

为什么要处理线程间通信:
当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一个资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与B之间就需要线程通信,就是–等待唤醒机制,以此实现多线程共同操作一份数据。

等待唤醒机制

  • 线程1在进入wait状态后,就会阻塞,并释放同步锁(同步监视器),方便线程2进来;
  • 线程2拿到同步锁之后,就会将等待状态的线程1notify(多个则按优先级高低,相同则随机),线程1状态此时进入了就绪状态;
  • 线程2执行到了wait状态,并释放了同步锁,线程1在得到了同步锁时,就会接着上一次wait的位置继续往下执行到结束。
  • 如下序列图所示:

image.png

涉及到三个方法的使用:

  • wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步锁的调用。
  • notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

注意点:

  • 上面三个方法的使用,必须是在同步代码块或同步方法中使用。但不能在JUC中的Lock里面使用。
  • 因为这三个方法是和同步监视器绑定的。否则就会报错。
  • 因同步监视器对象继承自Object,所以这三个方法,所以这3个方法是在Object中声明。

wait()和sleep()的区别:

**相同点:**一旦执行,当前线程就会进入阻塞状态。
不同点:

  • 声明的位置:
    • wait():声明在Object类中
    • sleep():声明在Thread类中,属于静态方法
  • 使用的场景不同:
    • wait():只能使用在同步代码块或同步方法中
    • sleep():可以在任何需要使用的场景
  • 使用在同步代码块或同步方法中:
    • wait():一旦执行,会执行同步监视器
    • sleep():一旦执行,不会释放同步监视
  • 结束阻塞的方式:
    • wait():到达指定时间自动结束阻塞或通过被notify唤醒,结束阻塞
    • sleep():到达指定时间自动结束阻塞

六、线程池

待续

七、总结:

  • 在多线程模式下,如果存在共享数据,就会出现因并发引起的资源抢占的情况,需要加同步机制,尽量避免出现thread.sleep()等线程阻塞的情况。
  • 39
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值