栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【Java八股40天-Day6】 多线程及高并发1

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Java八股40天-Day6】 多线程及高并发1

1. 创建线程有哪几种方式?

有三种方式:分别是继承Thread类、实现Runnable接口、实现Callable接口。 也可以加上线程池,就是四种方式。

  1. 直接定义Thread的子类,实现run方法作为线程执行体。调用start()方法启动线程
  2. 定义Runnable接口的实现类,并实现run方法作为线程执行体。调用线程对象的start()方法
  3. 创建Callable接口的实现类,并实现call()方法作为线程执行体,且call方法有返回值。创建Callable实现类的实例对象,使用FutureTask类来包装Callable对象。使用FutureTask对象作为Thread对象的target创建并启动线程。FutureTask对象的get方法获取子线程执行结束的返回值
    Callable用法
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //FutureTask传入实现了Callable的实现类
        FutureTask futureTask = new FutureTask<>(new MyThread());
        //创建线程传入FutureTask实例对象
        new Thread(futureTask).start();
        // futureTask.get()得到返回值
        Integer integer = futureTask.get();
        System.out.println("返回值:" + integer);
    }
}

class MyThread implements Callable {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable线程执行体....");
        return 2;
    }
}

一般都是采用实现Runnable接口、Callable接口的方式来创建多线程。
好处是,实现了接口还能基础其他类。多线程可以共享一个Runnable接口实现类,适合多个相同线程来处理同一份资源的情况,较好的体现了面向对象的思想。
劣势是编程稍复杂且如果要访问当前线程,必须使用Thread.currentThread()方法。而基础Thread类要访问当前线程可以直接用this关键字。

2. 说说Thread类的常用方法

Thread类常用构造方法:
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target, String name)
Thread类常用静态方法:
currentThread():返回当前正在执行的线程;
interrupted():返回当前执行的线程是否已经被中断;
sleep(long millis):使当前执行的线程睡眠多少毫秒数;
yield():使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行;
在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

3. run()和start()有什么区别?

run()方法是线程的执行体,表示线程需要完成的任务,start()方法是启动线程。
调用start()方法启动线程时,子线程会运行run()方法。
如果直接调用run()方法,会立即在当前线程执行run方法,不会启动新线程,相当于一个普通方法。

4.线程重复启动

会报错IllegalThreadStateException非法的线程状态。
用new 关键字创建一个线程出来以后,线程处于新建状态,可以进行一些设置,比如设置为守护线程。此时线程只是一个在堆空间的Java对象,没有动态特征,而对象调用了 start 方法后,该线程就处于就绪状态了,虚拟机会为其创建方法调用栈和程序计数器。
线程什么时候开始运行取决与系统线程调度。

5. 线程生命周期 操作系统线程状态

五个状态:新建、就绪、运行、阻塞、死亡

new 完处于新建状态
start 完处于就绪状态, 得到CUP资源进入运行状态
sleep、阻塞式IO、等待同步锁、等待通知、suspend方法挂起 会进入阻塞状态
死亡状态:run()或call()方法执行完成,线程正常结束。线程抛出一个未捕获的Exception或Error。直接stop(方法弃用了,可能导致死锁)

Java线程状态

Java中就绪和运行状态被合成RUNNABLE可执行状态, 阻塞状态被分为三类: 阻塞、等待、限时等待

public enum State {
	NEW, // 新建
	RUNNABLE, // 可执行
	BLOCKED, // 阻塞
	WAITING, // 等待
	TIMED_WAITING, // 限时等待
	TERMINATED; // 终止
}
sleep和wait的异同

相同点:一旦执行方法,都可以使得当前线程进入阻塞状态
不同:

  1. sleep是Thread类中的静态方法,wait() 是Object类中的方法
  2. sleep让出CPU资源,但是不会释放锁资源,wait会释放锁资源,需要给得时间过完,或者notify,notifyAll重新获取锁
  3. sleep可以在任何地方使用,wait()只能在同步代码块或同步方法中使用,因为wait会释放对象锁资源,必须得拿到这个资源才能释放
  4. sleep必须指定时间,必须捕获异常。wait可以不指定时间
6. 如何子线程先执行,主线程再执行?

启动子线程后,立即调用该线程的join方法。
当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

7.JMM是什么?

JMM是Java内存模型。每个线程创建时,JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。Java内存模型规定所有变量都存储在主内存,如果两个线程要对主内存的变量进行操作,它首先要将变量从主内存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存的变量,因此不同线程之间无法访问对方的工作内存。线程间的通信必须通过主内存来完成。

8. 重排序

在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,重排序不是随意的,它满足两个条件:

  1. 在单线程环境下,不能改变程序结果
  2. 存在数据依赖关系的不允许重排序

重排序不会影响单线程环境的执行结果, 但是会破坏多线程的执行语义
编译器优化重排序:在不改变程序执行结果的情况下,编译器对指令进程乱序的编译
CPU指令重排序:如果指令之间不存在数据依赖性,CPU可以改变语句的对应机器指令的执行顺序。
内存系统重排序: 在CPU内核进行读操作时,如果缓存没有就从主存取,对于写操作都是先写缓存,最后一次性写入主存。

9.as-if-serial 规则和 happens-before 规则的区别

编译器和 CPU 都必须遵循 as-if-serial: 无论如何重排序, 都必须保证代码在单线程下运行正确
为了避免编译优化对并发编程安全性的影响, 需要 happens-before 规则定义一些禁止编译优化的场景, 保证并发编程的正确性。

  1. 程序顺序规则: 一个线程中, 按照程序顺序, 前面的操作 happens-before 于后续的任意操作
  2. 监视器锁规则: 对一个锁的解锁, happens-before 于随后对这个锁的加锁。
  3. volatile 变量规则: 对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读。
  4. 传递性: 如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C。
  5. start()规则: 这条是关于线程启动的。 它是指主线程 A 启动子线程 B 后, 子线程 B 能够看到主线程在启动子线程 B 前的操作。
  6. join()规则: 如果线程 A 执行操作 ThreadB.join()并成功返回, 那么线程 B 中的任意操作先行发生于线程 A 的 ThreadB.join()操作。
10.volatile关键字有什么用?

当一个变量被定义成volatile之后,他将具备两项特性:

  1. 保证可见性
    在修改一个volatile变量时,JMM会把线程工作内存的变量强转刷新到主内存中,这个修改会导致其他线程中的volatile缓存无效
  2. 禁止指令重排
    使用volatile关键字修饰共享变量,可以禁止指令重排序,比如在懒汉式单例模式中,用双重校验锁+volatile。 必须使用volatile禁止指令重排,不然new对象对于JVM最少分为三步的,①分配内存和默认变量,②赋初始值,③对象指向空间。 如果先执行了③,其他线程进入立马返回对象,返回的这个对象是有问题的。
    即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见

虽然volatile能够保证可见性,但它能不保证原子性,volatile变量在各个线程的工作内存中是不存在一致性问题的,但是在Java里面的运算操作符并非原子操作,比如i++, 分为三步:①把 i 拷贝到线程工作内存,给 i自增, 把 i 放回主内存。如果线程A把 i 拷贝到内存的时候, 线程B也拷贝的 i 然后对 i 进行自增,刷新主内存的 i ,此时线程A 要对这个已经是旧值的 i 进行自增,然后刷入主内存。
对 volatile 域的写, happens-before 于任意线程后续对 volatile 域的读,这个规则只能保证线程A读的时候读取的是最新的数据,不能保证写的时候是还是最新的。

volatile是Java虚拟机提供的轻量级同步机制,他可见性、不保证原子性,禁止指令重排
底层通过内存屏障保障可见性和有序性,由于不能保证原子性所有可能会带来线程安全问题,可以考虑用原子类来保证操作的原子性

11.volatile 和 synchronized 的区别
  1. volatile 是变量修饰符, synchronized 可以修饰类、 方法、 变量。
  2. volatile 不会造成线程的阻塞, synchronized 可能会造成线程的阻塞。
  3. volatile 保证可见性, 不保证原子性, 禁止指令重排, synchronized 保证可见性, 原子性, 不禁止指令重排
12.如何实现线程同步
  1. 同步方法,synchronized关键字修饰方法
  2. 同步代码块,synchronized关键字修饰的语句块
  3. ReentrantLock可重入锁, 可重入、互斥
  4. volatile关键字,volatile为域变量的访问提供了一种免锁的机制,被volatile修饰的变量被线程更新后,其他线程会获取最新的volatile变量。
    但是需要注意的是volatile变量不提供任何原子操作,可能带来线程安全问题。这时就可以考虑原子类来保证操作的原子性
13.线程间通信方式

代码例子
主要由于三种
1.wait()、notify()、notifyAll()
如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。wait()会释放锁,所以wait最好在while循环里进行条件判断。否则可能出现虚假唤醒,下次被唤醒以后就不看if条件了,但是可能存在你被唤醒的瞬间不满足跳出等待, 直接往下走了

2.await()、signal()、signalAll()
如果线程之间采用Lock来保证线程安全,就利用await()、signal()、signalAll()。这三个方法都是Condition接口中的方法,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作, condition用lock对象的newCondition方法创建

3. BlockingQueue

当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/996927.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号