多线程与高并发

多线程的三大特性

原子性

原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。例如a++,对于共享变量a的操作,实际上会执行三个步骤,1.读取变量a的值 2.a的值+1 3.将值赋予变量a 。 这三个操作中任何一个操作过程中,a的值被人篡改,那么都会出现我们不希望出现的结果。所以我们必须保证这是原子性的。Java中的锁的机制解决了原子性的问题。

可见性

  • 可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
    我们知道,java线程通信是通过共享内存的方式进行通信的,而我们又知道,为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。

  • 多线程环境下,一个线程对于某个共享变量的更新,后续访问该变量的线程可能无法立刻读取到这个更新的结果,这就是不可见的情况。

  • 可见性就是指一个线程对共享变量的更新的结果对于读取相应共享变量的线程而言是否可见的问题

  • 可见性和原子性的联系和区别:

    • 原子性描述的是一个线程对共享变量的更新,从另一个线程的角度来看,它要么完成,要么尚未发生。
    • 可见性描述一个线程对共享变量的更新对于另一个线程而言是否可见

有序性(可见性是有序性的基础)

即程序执行的顺序按照代码的先后顺序执行。

其可以理解为在本线程内,所有的操作都是有序的。而如果在A线程中观察B线程,所有的操作都是无序的。在JMM中为了提升程序的执行效率,允许编译器和处理器对指令重排序。对于单线程来说,指令重排并不会产生问题,而在多线程下则不可以。

在Java中可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

名词解释:

进程与线程

  • 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。进程相当于程序的一次运行,线程依赖于进程,不可肚子存在。
    一个线程的生命周期:

  • 线程的优先级:
    每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
    Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
    默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
    具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台,具有高优先级只是被执行的概率更高,并非一定被执行。

并发与并行

  • 并行(parallel): 指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”(同一时间间隔)处理多个任务。

  • 并发(concurrency): 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

同步与异步

  • 同步和异步通常用来形容一次方法调用,强调的是消息通信机制。

  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。同步主要用于上下有递进关系的代码,特点是有序,串行执行,逻辑简单,但是执行效率较低。

  • 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。异步在java里面主要使用线程(包括一些封装类也是如此)实现,特点是执行效率高,但是逻辑相对复杂,容易出问题。

阻塞与非阻塞

  • 阻塞和非阻塞多用来形容线程,与线程等待消息通知(无所谓同步或者异步)时的状态有关。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

  • 注意区分同步与异步、阻塞与非阻塞,一个是消息通信机制,一个是线程状态;在不同场景下,同步阻塞、同步非阻塞、异步阻塞、异步非阻塞这四种组合都有应用。

volatile和synchronized

特点
  • 首先需要理解线程安全的两个方面:执行控制内存可见。

  • 执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

  • 内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

  • synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

  • volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

  • 使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

  • 在Java 5提供了原子数据类型atomic wrapper classes,对它们的increase之类的操作都是原子操作,不需要使用sychronized关键字。

  • 对于volatile关键字,当且仅当满足以下所有条件时可使用:

    • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    • 该变量没有包含在具有其他变量的不变式中。
  • 区别

    • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前 线程可以访问该变量,其他线程被阻塞住。
    • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
    • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
    • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

线程的实现方法

  • java提供了三种创建线程的方法
    -继承Thread类;
    -实现Runnable接口;
    -实现Callable接口。

继承Thread类

继承Thread之后,要实现父类的run方法,然后在起线程的时候,调用其start方法。

注意start和run方法的区别:

  • start:用start方法来启动线程,真正实现了多线程运行,通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行任务;
  • run: run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

如下图,通过debug可以看出,start方法比run方法多一个线程

实现Runnable接口

实现Runnable接口的run方法,在起线程的时候,如下,new一个Thread类,然后把类当做参数传进去。

在java中,Thread类也是通过实现Runnable接口实现的:

  • Runnable接口与Thread类选择:
    • java是单继承多实现,从这方面来说,Runnable接口的可拓展性高于Thread类;
    • Thread类每次执行任务,都需要new一个Thread,而Runnable是作为参数,直接把任务传入线程池,不需要每次新建销毁现场,大大降低了性能开销;
    • Runnable接口中只有一个run方法,而Thread类可设置属性、启动线程等。

实现Callable接口

Callable和Runnable可以认为是兄弟关系,两者功能类似:

  • Callable与Runnable
    • Callable实现call方法,Runnable实现run方法;
    • Callable有返回值(运行Callable任务可拿到一个Future对象, Future表示异步计算的结果),Runnable无返回值;
    • Callable需要处理异常,Runnable不需要
  • Future接口的5个方法:
    • boolean cancel(boolean mayInterruptIfRunning) 用于取消任务,取消成功返回true,否则false;mayInterruptIfRunning指是否允许取消正在运行但未执行完毕的任务;

  • boolean isCancelled() 若在Callable任务正常完成前被取消,返回True;

  • boolean isDone() 若Callable任务完成,返回True;

  • V get() throws InterruptedException, ExecutionException 返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值;

  • V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

posted @ 2023-02-05 23:34  不穿衣服去逛街  阅读(51)  评论(0)    收藏  举报