面试之多线程(三)

1.进程和线程的区别

  • 根本区别:进程是操作系统分配资源的最小单位;线程是CPU调度的最小单位
  • 所属关系:一个进程包含了多个线程,至少拥有一个主线程;线程所属于进程
  • 开销不同:进程的创建,销毁,切换所需要的资源远远大于线程
  • 拥有的资源:每一个进程都拥有自己的内存和资源;线程不会独立的拥有这些资源,而是共享所属进程申请来的资源
  • CPU利用率不同:进程的利用率比较低,因为上下文切换开销较大,而线程的CPU;利用率比较高,上下文切换速度比较快
  • 控制力和影响力不同:子进程无法影响父进程,而子线程可以影响父线程,如果子线程发生异常会影响其所在的进程和子线程。

2.线程安全是什么

通俗的来讲,线程安全就是在多线程环境下,运行的结果符合我们的预期(即与单线程运行的结果相同),此时我们就说是线程安全

3.为什么会出现线程不安全问题呢

  • 线程的抢占式执行
  • 多个线程同时修改同一个变量
  • 未保证操作的原子性内存可见性
  • 指令重排序

4.Synchronized和volatile

1.volatile

volatile 解决的是内存可见性问题

1.1 volatile 原理

volatile原理是基于CPU内存屏障指令实现的

1.2 volatile 修饰的变量可见性

volatile是变量修饰符,其修饰的变量具有内存可见性

一般情况下线程在执行时,Java中为了加快程序的运行效率,会先把主存数据拷贝到线程本地(寄存器或是CPU缓存),操作完成后再把结果从线程本地缓存刷新到主存中,这样就会导致修改后放入变量结果同步到主存中需要一个过程,而此时另外的线程看到的还是修改之前的变量值,这样就会导致不一致

为了解决上述多线程中内存可见的问题,引入了 volatile 关键字,那么它为什么可以解决内存可见性问题呢?

答案: volatile 它会使得所有对 volatile 变量的读写都会直接读写主存,而不是先读写线程本地缓存,这样就保证了变量的内存可见性

1.3 volatile 禁止指令重排 

volatile可以禁止进行指令重排

指令重排: 处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行时的正确性

线程执行到volatile修饰变量的读写操作时,其他线程对这个变量的操作肯定已经完成了,且结果已经同步到了主存中,即对其他的线程可见,本线程再对该变量操作完全没有问题的

1.4 volatile 使用范围

volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,不能保证复合操作的原子性,比如 i++

i++,实际上是由三个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但不能保证i结果的正确性,原因如下:

比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了,A和B线程同步到主存中的i的值都是1

1.5 volatile 使用场景

1、 对变量的写入操作不依赖变量的当前值,或者只有单个线程更新变量的值

2、 该变量没有包含在具有其他变量的不变式中

2.synchronized

  • synchronized 既解决了内存可见性问题,又解决了执行顺序问题
  • synchronized 可以修饰代码块或方法,既可以保证可见性,又能够保证原子性

2.1 synchronized 原理

synchronized 是基于 monitor 实现的

2.2 synchronized 修饰的代码块或方法保证内存可见性

通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中

2.3 synchronized 修饰的代码块或方法保证原子性

线程要么不执行(线程没有获取到对象锁),线程要么执行到底(线程获取到了对象锁),直到执行完释放锁

2.4 synchronized 使用范围

synchronized 不仅能修饰代码块,还可以修饰方法

2.5 synchronized 使用场景

需要控制多线程访问的方法或者更新的变量

3.volatile 和 synchronized 异同点


3.1 相同点

volatile 和 synchronized 都保证了内存可见性

3.2 不同点

  • volatile仅能使用在变量级别,synchronized则可以使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性,而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞
  • volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化
  • 由于 4 中的区别,在某些情况下 volatile 的性能优于 synchronized

5.线程的状态

  • NEW:表示创建了一个线程,但是还没有开始执行
  • RUNNABLE:线程的状态是运行 + 就绪状态,在CPU开始执行了
  • TERMINATED:线程在CPU上运行结束,系统线程已经销毁,但是Java对象还没有回收
  • TIMED_WAITING:带超时时间的等待状态,wait(time),sleep(time),join(time),过时不候
  • WAITING:没有指定超时时间的等待状态,一直等待
  • BLOCK:加了Synchronized之后,其他线程在等待竞争锁资源时的等待状态

6.wait(),sleep(),yield(),join()的区别

  1. wait():属于Object类的方法,wait()过程中会释放锁,只有notify才可以唤醒线程。wait使用时必须获取锁对象,也就是说必须要搭配synchronized来使用。如果没有synchronized,则会报错
  2. sleep():属于Thread类的方法,sleep过程中不会释放锁,一直占着锁资源,只会线程阻塞,让出CPU给其他线程执行,但是他的监控状态依然保持,当指定的时间到了之后又会回复运行的状态,可中断
  3. join():属于Thread类的方法,等待调用join()的线程结束之后,程序再继续执行一般用于等待异步线程执行完结果之后才能继续运行的场景
  4. yield():属于Thread类的方法,和sleep一样,都是暂停当前执行的线程对象,不会释放锁资源,和sleep不同的是,yield方法不会让线程进入阻塞状态,而是让线程重新回到就绪状态等待CPU调度。

7.创建线程的方法

7.1继承Theard类,重写run方法

class MyThread02 extends Thread{
//重写run方法
    @Override
    public void run() {
        while(true){
            System.out.println("hello my thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
          }
    }

7.2 实现Runnable接口,重写run方法

实现Runnable接口,重写run方法
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("MyRunnable Thread ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

7.3其他变形

  • 匿名内部类创建Thread子类对象
    // 使用匿名类创建 Thread 子类对象
    Thread t1 = new Thread() {
        @Override
        public void run() {
            System.out.println("使用匿名类创建 Thread 子类对象");
       }
    };
  •  匿名内部类创建Runnable子类对象
    // 使用匿名类创建 Runnable 子类对象
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("使用匿名类创建 Runnable 子类对象");
       }
    });
  • lambda表达式创建Runnable子类对象
    // 使用 lambda 表达式创建 Runnable 子类对象
    Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
    Thread t4 = new Thread(() -> {
        System.out.println("使用匿名类创建 Thread 子类对象");
    });

8.run()和start()方法的区别

start()

用start()方法来启动线程,是真正实现了多线程。通过Thread类的start()方法来启动一个线程,此时线程就会处于就绪(可运行)状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法。并且无需等待run()方法执行完毕,就可以执行后面的代码。

run()

run()方法只是类的一个普通方法,如果直接调用run()方法,程序中依然只有主线程这一个线程,其程序执行路径只有一条,代码还需要顺序执行。

两者区别

  • 当程序调用一个start方法,将会创建一个新的线程并执行run方法中的代码,但是如果直接调用run方法的话,会直接在当前线程执行run方法中的代码,也不会创建新的线程
  • 当线程启动之后,不能重复调用start方法,否则会报出异常;但是可以重复调用run方法
  • 总结:run方法就是一个普通方法,start方法会创建一个新的线程执行run方法的代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值