avatar

java并发编程

简单介绍

java是一个支持多线程的开发语言,多线程可以在包含多个cpu核心的机器上同时处理多个不同的任务,优化资源的使用率,提升程序的效率。在一些对性能要求比较高的场合,多线程是java程序调优的重要方面。
java并发编程主要涉及以下几个部分:

  • 并发编程三要素:
    • 原子性:即一个不可再被分割的颗粒。在java种原子性指的是一个或多个操作要么全部执行成功要么全部执行失败
    • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
    • 可见性:当多个线程访问同一个变量时,如果其中一个线程对其做了修改,其他线程能立即获取到最新的值
  • 线程的五大状态:
    • 创建状态:当用new操作符创建一个线程的时候
    • 就绪状态:调用start方法,处于就绪状态的线程并不一定马上就会执行run方法,还需要等待cpu的调度
    • 运行状态:cpu开始调度线程,并开始执行run方法
    • 阻塞状态:线程的执行过程中由于一些原因进入阻塞状态比如:调用sleep方法,尝试去得到一个锁等等
    • 死亡状态:run方法执行完成或者执行过程中遇到了一个异常
  • 悲观锁和乐观锁
    • 悲观锁:每次操作都会加锁,会造成线程阻塞
    • 乐观锁:每次操作不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞
  • 线程之间的协作:
    • 线程之间的协作有:wait/notify/notifyAll等
  • synchronized关键字
    synchronized是java中的关键字,是一个同步锁,它修饰的对象有以下几种:
    • 修饰一个代码块:被修饰的代码块被称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
    • 修饰一个方法:被修饰的方法被称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
    • 修饰一个静态的方法:起作用的范围是整个静态方法,作用的对象是这个类的所有对象
    • 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
  • CAS:
    CAS全称是Compare And Swap,即比较替换,是实现并发应用的一种技术,操作包含三个操作数–内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则处理器不做任何操作。CAS存在三大问题:ABA问题,循环时间开销大,以及只能保证一个共享变量的原子操作
  • 线程池:
    如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题,如果并发的线程数量很大,并且很多线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。

java中的线程

java中创建线程一般有两种方式

  • 继承Thread类,重写run方法实现
  • 实现Runnable方法实现

    java线程中的特征与状态

    1、所有的java程序,无论并发与否,都有一个名为主线程的Thread对象。执行该程序时,java虚拟机(JVM)将创建一个新Thread并在该线程中执行main()方法,这是非并发应用程序中唯一的线程,也是并发应用程序中第一个线程
    2、java中的线程共享应用程序中的所有资源,包括内存和打开的文件,快速而简单地共享信息,但是必须使用同步避免数据竞争
    3、java中的所有线程都有一个优先级,这个整数值介于Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之间,默认优先级是Thread.NORM_PRIORITY(5)。线程的执行顺序并没有保证,通常较高优先级的线程将在较低优先级的线程之前执行
    4、在java中,可以创建两种线程:
  • 守护线程
  • 非守护线程
    区别在于它们如何影响程序的结束。
    java程序结束执行过程的情形:
  • 程序执行Runtime的exit()方法,而且用户有权执行该方法。
  • 应用程序的所有非守护线程已结束执行,无论是否有正在运行的守护线程。
    守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。在线程start之前调用isDaemon()方法检查线程是否为守护线程,也可以使用setDaemon()方法将某个线程确立为守护线程
    5、Thread.States类中定义线程的状态如下:
  • NEW: Thread对象已经创建,但是还没有开始执行
  • RUNANABLE: Thread对象正在java虚拟机中运行
  • BLOCKED: Thread对象正在等待锁定
  • WAITING:Thread对象正在等待另一个线程的动作
  • TIME_WAITING: Thread对象正在等待另一个线程的操作,但是有时间限制
  • TERMINATED:Thread对象已经完成了执行
    getState()方法获取Thread对象的状态,可以直接更改线程的状态。
    在给定时间内,线程只能处于一个状态,这些状态是JVM使用的状态,不能映射到操作系统的线程状态。

Thread类和Runable接口

Runable接口只定义了一种方法:run()方法。这是每一个线程的主方法。当执行start()方法启动新线程时,它将调用run()方法
Thread类其他常用方法:

  • 获取和设置Tnreadd对象信息的方法:
    • getId(): 该方法返回Thread对象的标识符,该标识符是在线程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的
    • gatName()/setName(): 这两个方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造函数中建立
    • getPriority()/setPriority(): 你可以使用这两种方法来获取或设置Thread对象的优先级
    • isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件
    • getState(): 该方法返回Thread对象的状态
  • interrupt(): 中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记。中断(wait/sleep/join时线程不工作就是中断)
  • interrupted(): 判断目标线程是否被中断,并且将清除线程的中断标记
  • isInterrupted(): 判断目标线程是否被中断,不会清除中断标记
  • sleep(long ms): 该方法将线程的执行暂停ms时间
  • join(): 暂停线程的执行,直到调用该方法的线程执行结束为止,可以使用该方法等待另一个Thread对象结束
  • setUncaughtExceptionHandler(): 当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器
  • currentThread(): Thread类的静态方法,返回实际执行该代码的Thread对象

Callable接口

Callable接口是一个与Runnable接口非常相似的接口,Callable接口的主要特征如下:

  • 接口。有简单类型参数,与call()方法的返回类型对应
  • 声明了call()方法,执行器运行任务时,该方法会被执行器执行。它必须返回声明中指定类型的对象
  • call()方法可以抛出任何一种校验异常,可以实现自己的执行器并重载afterExecute()方法来处理这些异常

使用方法

  • 首先需要创建自定义类实现Callable
    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    Thread.sleep(5000);
    return "call方法的返回值";
    }
    }
  • 创建FutureTask 执行代码
    public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable callable = new MyCallable();

    FutureTask<String> futureTask = new FutureTask<>(callable);
    // 启动线程,执行callable的业务
    new Thread(futureTask).start();
    // 同步等待callable的返回值
    String result = futureTask.get();

    System.out.println(result);
    System.out.println("main线程的返回值");
    }
    }
  • 如果想处理call()方法中的异常可以通过自定义执行器进行处理afterExecute可以处理异常
    public class MainPool {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable callable = new MyCallable();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 5, 1,
    TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)){
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
    // 在call方法中执行有错误,可以在此处进行处理
    //super.afterExecute(r, t);
    System.out.println("任务执行完毕:"+t);
    }
    };
    Future<String> future = executor.submit(callable);
    String result = future.get();
    System.out.println(result);
    executor.shutdown();
    }
    }

    synchronized关键字

    锁的对象

    synchronized关键字就是给某个对象加锁。
  • synchronized关键字给对象加锁
    public class MyClass{
    public void synchronized method01(){

    }
    //等价于
    public void method01(){
    synchronized(this){

    }
    }
    }
  • synchronized给静态方法枷锁,相当于是给类的class对象加锁
    public class MyClass{
    public static void synchronized method01(){

    }
    //等价于
    public static void method01(){
    synchronized(MyClass.calss){

    }
    }
    }

    锁的本质

    如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量,一个对象或一个文件。
    锁是一个”对象”,作用如下:
    1、这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用,最简单的情况时这个state有0,1两个取值,0代表没有线程占用这个锁,1代表有某个线程占用这个锁
    2、如果这个对象被被某个线程占用,记录这个线程的thread ID
    3、这个对象维护一个thread id list,记录其他所有阻塞的、等待获取这个锁的线程。在大哥前线程释放之后,从这个thread id list里面取一个线程唤醒

要访问的共享资源本身也是一个对象,这两个对象可以合成一个对象。代码就变成synchronized(this){…},要访问的共享资源是对象a,锁加载对象a上。当然也可以另外新建一个对象,代码变成synchronized(obj1){…},这个时候访问的共享资源是对象a,而锁加在新建的对象obj1上。资源和锁合二为一,使得在java里面,synchronized关键字可以加在任何对象的成员上面,这意味着,这个对象既是共享资源,同时也具备”锁”的功能!

锁的实现原理

在对象头里面,有一块数据叫Mark Word。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异

wait与notify

生产者-消费者模型

生产者消费者模型是一个常见的多线程编程模型:

一个内存队列,多个生产者线程往内存队列中放数据,多个消费者线程从内存队列中取数据,要实现这样一个编程模型,需要做下面几件事:

  • 内存队列本身要加锁,才能实现线程安全
  • 阻塞,当队列满了,生产者放不进去时,会被阻塞,当内存队列是空的时候,消费者无事可做,会被阻塞
  • 双向通知,消费者被阻塞之后,生产者放入新数据,要notify()消费者,反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者

如何阻塞
办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()
办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的

如何双向通知
办法1:wait()和notify()机制
办法2:Condition机制

单个生产者单个消费者线程的情况
  • 主函数
文章作者: zenshin
文章链接: https://zlh.giserhub.com/2021/01/17/cl35o0nki00lvp4tg0nw5b6zd/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 zenshin's blog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论