Java复习---多线程

Posted 赵jc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java复习---多线程相关的知识,希望对你有一定的参考价值。

并行和并发有什么区别?

可以借鉴物理的知识。

  • 并发是指两个或者多个事件在同一时间发生,但在这个时间段中也是又先后顺序的,是假的同时),多个进程在同一个CPU下采用时间片轮转的方式,在一段时间内,让多个进程都得以推进。
  • 而并行是指两个或多个事件在同一时刻隔发生。(多个事件真正的在同一时刻发生),多个进程在多个CPU下同时运行

线程和进程的区别?

进程(系统资源分配得最小单位)

线程(系统调度的最小单位,进程的一个实体)

  • 一个程序至少有一个进程,一个进程至少有一个线程。

  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,从而减少切换次数,使效率更高。

  • 同一进程中的多个线程之间可以并发执行。

  • 线程的创建、切换即终止效率相对于进程来说相对较高。

多线程的好处

  • 程序运行的更快!
  • 充分利用cpu资源
  • 让阻塞的代码不影响后续代码的执行(后续的代码在其他线程执行)

守护线程是什么?

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。

创建线程有哪几种方式?

①. 继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
   // 方法一 继承Thread 单继承
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("子线程" + Thread.currentThread().getName());
        }
    }

②. 通过Runnable接口创建线程类

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象的参数,该Thread对象才是真正的线程对象。

③. 通过Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值

  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。

  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
       int n = 1;
       return n;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        int ret = futureTask.get();
        System.out.println(ret);
    }
}

④. 通过ThreadPoolExecutor创建线程
这种是最经典的线程池创建方式,也时最常用的方式,这种方式可以
优点

  • 这种方式可以解决线程数量不可控的问题
  • 这种方式可以解决任务数量不可控的问题
public class ThreadPoolExecuteTest {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5, 10, 60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.DiscardPolicy()
        );
        for(int i = 0; i < 10; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}

7个参数:

  • corePoolSIze:核⼼线程数,
  • maximumPoolSize:最⼤线程数。
  • keepAliveTime:空闲线程的保活时间,
  • TimeUnit:保活时间单位。
  • BlockingQueue:任务丢列,⽤于存储线程池的待执⾏任务的。
  • ThreadFactory:⽤于⽣成线程,⼀般我们可以⽤默认的就可以了。或者自定义我们需要的任务。
  • handler:拒绝策略,当线程池已经满了,但是⼜有新的任务提交的时候,该采取什么策略由这个来指定。有⼏种⽅式可供选择,像抛出异常、直接拒绝然后返回等,也可以⾃⼰实现相应的接⼝实现⾃⼰的逻辑。

5种拒绝策略
拒绝策略:达到最大线程数且阻塞队列已满,采取的拒绝策略

  • AbortPolicy:直接抛RejectedExecutionException(不提供handler时的默认策略)
  • CallerRunsPolicy:谁(某个线程)交给我(线程池)任务,我拒绝执行,由谁自己执行
  • DiscardPolicy:交给我的任务,直接丢弃掉(尾删)
  • DiscardOldestPolicy:丢弃阻塞队列中最旧的任务(头删)
  • 我们自定的拒绝策略,可以写进日志里或者存储到数据库当中

线程有哪些状态?(7种)

在这里插入图片描述

线程通常都有五种状态,创建(new)、、运行Runnable(running、和ready)、等待(waiting、timed_waitting、blocked)和终止terminated。

sleep() 和 wait() 有什么区别?

wait和sleep的区别
相同点

  • wait和sleep都是让线程进入休眠状态
  • wait和sleep在执行的过程中都可以接收到线程终止的通知

不同点

  • wait必须配合synchronized一起使用,而sleep不用
  • wait会释放锁,而sleep不会释放锁(sleep必须要传入一个最大等待时间,也就是说sleep是可控的(对于时间层面来说),而wait是不可以传递参数的,如果wait不主动释放锁的话就,没被唤醒前就会一直阻塞)
  • wait是Object的方法,而sleep是Thread(线程)的方法(wait需要操作锁,而锁是属于对象级别的(存放在对象头当中)它不是线程级别的,一个线程可以有多把锁,为了灵活起见,所以讲wait放在了Object当中)
  • 默认情况下wait(不传递任何参数或者参数为0的情况下)它会进入waiting状态,而sleep会进入timed_waiting状态
  • 使用wait时可以主动的唤醒线程,而使用sleep时不能主动地唤醒线程

解决wait/notify随机唤醒的问题(指定唤醒某个线程)

  • LockSupport park()/unpark(线程)
  • LockSupporrt()虽然不会报Interrupt的异常,但依然可以监听到线程终止的指令

在 java 程序中怎么保证多线程的运行安全?

CPU是抢占式执行的(万恶之源)所以导致了线程 不安全
线程安全在三个方面体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)(为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题)
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序.(编译器优化/指令重排序)(volatile关键字)

说一下volatile关键字?

  • 可以解决内存不可见(从主内存中取值,然后在工作内存中修改后存入主内存中,然后刷新情况自己的工作内存)和指令重排序的问题,
  • 但不可以解决原子性的问题

说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区
针对Java语言来说,是将锁信息存放在对象头(标识,锁的状态,所得拥有者)
对象头中保存着偏向锁的线程id(下面介绍什么是偏向锁)
在这里插入图片描述

针对JVM层面,它是依靠monitor来实现
monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,其中的owner字段表名拥有该锁的线程名称
在这里插入图片描述

针对操作系统层面,它是依靠互斥锁mutex

  • 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

synchronized锁升级的过程

在这里插入图片描述
当一个线程A刚创建出来便是无锁状态,当A竞争到锁之后会升级为偏向锁(偏向锁偏向锁,意思便是有偏向的意思啦,偏向第一个获取到的线程加锁,但这个线程比较坏,不会自己释放锁,只有当别的线程来竞争的时候才会释放锁),线程A升级为偏向锁之后,当线程B来尝试获取锁的时候,如果没有获取到会自旋等待一直尝试获取锁(而不会因没有获取到锁而阻塞),但当现线程B自旋一定次数,或者另一个线程C也来获取锁的时候,线程A又会升级为重量级锁,(当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程(B和C)进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。)

  • 上面说的云里雾里的下面我们来举个例子吧,有一个公共的娱乐设施(锁),线程A第一个来玩了(偏向锁),但线程A一直玩,这时线程B也想来玩,但是线程A脸皮比较厚接着玩,线程B就在旁边边吐槽边等(自旋),当线程B吐槽了好一会后线程A受不了了,但是还想玩,所以想着在玩一会(轻量级锁),但线程B还是在旁边吐槽(自旋),过了一会线程A实在受不了了,说你先等着,等我玩完了再叫你。(重量级锁)

synchronized 和 Lock 有什么区别?

  • synchronized自行进行加锁和释放锁,而lock需要手动进行加锁和解锁
  • lock是Java层面锁的实现的,而synchronized是JVM层面实现的
  • synchronized可以修饰代码块、静态方法、实例方法,而lock只能修饰代码块
  • synchronized只能实现非公平锁,但lock可以实现非公平锁和公平锁
  • lock的灵活性更高(tryLock)

synchronized 和 ReentrantLock 区别是什么?

  • synchronized是关键字,而ReentrantLock是一个类
  • synchronized只能实现非公平锁,但ReentrantLock 可以实现非公平锁和公平
  • ReentrantLock可以获取各种锁的信息

另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal是线程的本地变量,每一个线程会创建一个私有变量。
ThreadLocal三板斧,set(T)、get()、remove()

  • set(T):将内容存储到ThreadLocal。没有 set 操作的 ThreadLocal 容易引起脏数据。
  • get:从线程中取私有变量。没有 get 操作的 ThreadLocal 对象没有意义。
  • remove:从线程中移除私有变量。没有 remove 操作容易引起内存泄漏。

ThreadLocal 的使用场景

  • 解决线程安全的问题
  • 实现线程级别的数据传递(实现一定程度上的解耦)

ThreadLocal 带来的问题

  • 不可继承性(InheritableThreadLocal来解决不可继承性的问题,但前提两个线程必须是父子线程的关系(或者从属进程的关系),不能实现并列线程之间的数据传输(数据设置和获取)为什么会出现这种情况?因为⽆论是ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变量,所以不能跨线程进⾏数据共享也是正常的。)
  • 产生脏读数据(配合线程池使用时,没有remove)
  • 内存泄漏(配合线程池使用,没有remove,而且ThreadLocal中存的value是强声明周期且占用1mb)

死锁

什么是死锁

  • 在两个或者两个以上的线程运行中,因为资源抢占而造成线程一直等待的问题。

造成死锁的四个条件:

  • 互斥条件:当资源被一个线程拥有之后,就不能被其他的线程拥有了(不可更改)
  • 请求拥有条件:当一个线程拥有了一个资源之后又试图请求另一个资源(可以解决)
  • 不可剥夺条件:当一个资源被一个线程拥有之后,如果不是这个线程主动释放此资源的情况下,其他线程不能拥有此资源(不可更改)
  • 环路等待条件:两个或两个以上的线程在拥有了资源之后,试图获取对方资源的时候形成了一个环路(可以解决)

如何解决死锁

  • 控制加锁的顺序(解决环路的等待条件)

线程池都有哪些状态?(5种)

线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
在这里插入图片描述

线程池中 submit()和 execute()方法有什么区别?

  • 1.执行的任务无返回值 execute(new Runnable)execute执行任务如果有OOM异常会将异常打印到控制台,方便Exception处理
  • 2.执行的的任务有返回值 submit(可以有返回值也可以没有返回值)(new Runnable 无返回值/ new Callable 有返回值)execute执行任务如果有OOM异常不会将异常打印到控制台,

线程池的关闭

  • shutdown:拒绝新任务加入,等待线程池中的任务队列执行完之后在停止线程池
  • shutdownNow:拒绝执行新任务,并且会立即停止,不会等待任务队列中的任务执行完,才停止线程池

多线程的应用有哪些?

阻塞队列
线程池
单例模式(重要)

  • 饿汉模式
  • 懒汉模式
  • 静态内部类
  • 枚举实现

以上是关于Java复习---多线程的主要内容,如果未能解决你的问题,请参考以下文章

Java复习---多线程

java8--多线程(java疯狂讲义3复习笔记)

JAVA复习笔记之多线程并发

Day19:60 多个实例讲解,彻底搞懂Java 多线程 可查阅,可复习,可面试

[Java]多线程复习(更新未完)

Java知识复习——多线程