多线程与高并发

多线程的三大特性

原子性

原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。例如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): 指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
  • 并发(concurrency): 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
  • 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  • 并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”(同一时间间隔)处理多个任务。

同步与异步

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

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

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

阻塞与非阻塞

  • 阻塞和非阻塞多用来形容线程,与线程等待消息通知(无所谓同步或者异步)时的状态有关。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
  • 注意区分同步与异步、阻塞与非阻塞,一个是消息通信机制,一个是线程状态;在不同场景下,同步阻塞、同步非阻塞、异步阻塞、异步非阻塞这四种组合都有应用。

volatile关键字

volatile关键字有两个作用:

  1. 保证此变量对所有线程可见: 在Java中有一块主内存,不同的线程有自己的工作内存,同一个变量值在主内存中有一份,如果线程用到了这个变量的话,自己的工作内存中有一份一模一样的拷贝。每次进入线程从主内存中拿到变量值,每次执行完线程将变量从工作内存同步回主内存中。volite关键字修饰的变量不允许线程内部缓存,即直接修改内存。但是volatile关键字只能保证改变量的可见性,并不保证其原子性;

同样的synchronized除了保障了原子性外,其实也保障了可见性。因为synchronized无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。

  1. 禁止指令重排序优化: 首先解释一下什么是指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序是有一定规则的,例如:1. a=1; 2.b = 3; 3. a = a+2;语句1是不会优化到语句3后面去的,因为处理器在重排序的时候会考虑到数据依赖性,但是该规则即数据依赖性并不适用于多线程。
  2. 为什么不能保证原子性? 既然volatile能保证每次对变量的修改都直接同步到主存,那为什么无法保证原子性呢?因为Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。

线程的实现方法

java提供了三种创建线程的方法

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

实现Callable接口

Callable和Runnable可以认为是兄弟关系,两者功能类似:
在这里插入图片描述

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

    2. boolean isCancelled()

      若在Callable任务正常完成前被取消,返回True;

    3. boolean isDone()

      若Callable任务完成,返回True;

    4. V get() throws InterruptedException, ExecutionException

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

    5. V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

      用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值