Java面试--Java并发知识点

本文详细介绍了synchronized关键字的作用机制,包括其实现原理、使用方式及其在Java内存模型中的地位。此外,还对比了synchronized与ReentrantLock、volatile关键字的区别,并探讨了ThreadLocal、线程池等并发工具类的应用。

synchronized关键字

说说你对synchronized关键字的了解
synchronized关键字解决的是多线程之间访问资源的同步性,
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

Java6之后Java官方从JVM层面对synchronized引入大量优化,如自旋锁、适应性自旋锁、锁消除、偏向锁、轻量级锁等技术来减少锁操作的开销。
说说你是怎么使用synchronized关键字的
synchronized关键字最主要的三种使用方式:
1.修饰实例方法:作用于当前对象实例加锁,进入同步方法前要获得当前对象实例的锁。
synchronized void method(){}
2.修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员。所以,如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是不会发生互斥的,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象的锁。
synchronized void static method(){}
3.修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object)表示进入同步代码前要获得给定对象的锁。synchronized(类.class)表示进入同步代码前要获得当前class的锁。
synchronized (this){}
总结:
关键字加到static静态方法和synchronized (class)代码块上都是给class类上锁。

关键字加到实例方法上是给对象实例上锁。

尽量不要使用synchronized (string a)因为JVM中,字符串常量池有缓存功能。

应用:双重检验锁方式实现单例模式(线程安全)


```java
public class Singleton{
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getUniqueInstance(){
        //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if(uniqueInstance == null){
            //类对象加锁
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

为 uniqueInstance 分配内存空间

初始化 uniqueInstance

将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
构造方法本来就是线程安全的不能用synchronized关键字修饰

说说synchronized关键字的底层原理

synchronized同步语句快的实现使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指向同步代码块的结束位置。

synchronized修饰的方法并没有monitorenter指令和monitorexit指令,而是用ACC_SYNCHRONIZED标识,标识指明了该方法是一个同步方法。

两者的本质都是对对象监视器monitor的获取。
1.6之后对synchronized做的优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
synchronized和ReentrantLock的区别
1.两者都是可重入锁:指的是可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当再次想要获取这个对象锁的时候还是可以获取。
2.synchronized依赖JVM实现,在虚拟机层面,并没有直接暴露给我们。ReentrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/final语句块来完成),所以可以查看源码是怎么实现的。
3.ReentrantlLock比synchronized增加了一些高级功能
等待可中断:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。

可实现选择性通知:synchronized关键字与wait()和notify()/notifyAll()方法结合可以实现等待/通知机制。ReentrantLock借助condition接口和newCondition()方法。

volatile关键字

synchronized可以保证代码片段的原子性。

当一个线程对共享变量进行了修改,那么另外的线程都可以立即看到修改后的最新值。volatile关键字可以保证共享变量的可见性。

Java在编译以及运行期间的优化,使得代码的执行顺序未必就是编写代码时的顺序。volatile关键字可以禁止指令重排。
synchronized关键字和volatile关键字的区别
volatile关键字是线程同步的轻量级实现,性能比synchronized关键字好。但是volatile关键字只能用于变量而synchronized可以修饰方法以及代码块。

volatile关键字能保证数据的可见性,但是不能保证数据的原子性。synchronized关键字两者都能保证。

volatile关键字主要用于解决变量在多个线程之间的可见性,synchronized关键字解决多个线程之间访问资源的同步性。

ThreadLocal

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
ThreadLocal原理
从Thread类源码可以看出Thread类中有一个threadLocals和一个inheritableThreadLocals变量,它们都是ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

所以最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解是ThreadLocalMap的封装,传递了变量值。ThreadLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的threadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key,value就是ThreadLocal对象调用set方法设置的值。
ThreadLocal内存泄露的问题
ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。假如不做任何措施,value永远不会被GC回收,这时候可能会产生内存泄漏。ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法的时候,会清理掉key为null的记录。使用完ThreadLocal方法之后,最好手动调用remove()方法。

线程池

为什么要用线程池
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。

提高效应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一的分配、调优和监控。
实现Runnable接口和Callable接口的区别
Runnable接口不会返回结果或者抛出检查异常,但是Callable接口可以。
执行execute()方法和submit()方法的区别
1.execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2.submit方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功。
如何创建线程池
方式一:通过构造方法实现
ThreadPoolExecutor
方式二:通过Executor框架的工具类Executors来实现,可以创建三种类型的ThreadPoolExecutor
FixedThreadPool:该方法返回一个固定数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲的线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲线程时,便处理在队列中的任务。

SingleThreadExecutor:该方法返回只有一个线程的线程池。当有一个新的任务提交时,新的任务会被暂存在一个任务队列中,待有线程空闲线程时,便按照先入先出的顺序处理在队列中的任务。

CachedThreadPool:该方法返回一个可以根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但如果有空闲线程可以复用,会优先使用可复用的线程。当有一个新的任务提交时,会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池复用。
ThreadPoolExecutor构造函数重要参数分析
ThreadPoolExecutor三个最重要的参数:
corePoolSize:核心线程数定义了最小可以同时运行的线程数量。

maxmumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

workQueue:当新任务来的时候会先判断当前运行的线程数量是否到达核心线程数,如果达到,新任务就会被存放在队列中。
其他常见参数:
keepAliveTime:当线程池中线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,知道等待时间超过了keepAliveTime才会被销毁。

unit:keepAliveTime参数的时间单位。

threadFactory:executor创建新线程的时候会用到。

handler:饱和策略。
ThreadPoolExecutor饱和策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor定义一些新策略:
ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,如果执行程序已经关闭,则会丢弃该任务。这种策略会降低对于新任务提交的速度。
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃。
ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
线程池原理

Atomic原子类

什么是Atomic原子类
原子类就是具有原子操作特征的类。
JUC包中的原子类是哪四类
基本类型:
使用原子的方式更新基本类型
AtomicInteger:整形原子类
AtomicLong:长整形原子类
AtomicBoolean:布尔型原子类
数组类型:
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray:引用类型数组原子类
引用类型:
AtomicReference:引用类型原子类
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
AtomicMarkableReference:原子更新带有标记位的引用类型。
对象的属性修改类型:
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
AtomicInteger的使用
使用AtomicInteger之后,不用对increment()方法加锁也可以保证线程安全。
AtomicInteger的原理
AtomicInteger类主要利用CAS+volatile和native方法保证原子操作,避免了synchronized的高开销。

AQS

AQS介绍
AQS是一个用来构建锁和同步器的框架,比如ReentrantLock等。
AQS原理分析
AQS原理概览
AQS核心思想是,如果被请求的共享资源空闲,当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的资源被占用,那么就需要一套线程阻塞等待及被唤醒时锁分配的机制,这个机制是AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS对资源的共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁。
Share(共享):多个线程可同时执行。如ReadWriteLock允许多个线程同时对某一资源进行读。
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是:
1.使用者继承AbstractQueuedSynchronized并重写指定方法。
2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会使用使用者重写的方法。
AQS组件总结
Semaphore(信号量)-允许多个线程同时访问:synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,信号量可以指定多个线程同时访问某个资源。
CountDownLatch(倒计时器):CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,可以让某个线程等待直到倒计时结束再开始执行。
CycliBarrier(循环栅栏):CycliBarrier让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被拦截的线程才会继续干活。
用过CountDownLatch吗?什么场景下用的
CountDownLatch允许count个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

什么是线程和进程

什么是进程
进程是程序一次执行的过程,是系统运行程序的基本单位,进程是动态的。
什么是线程
线程是一个比进程更小的执行单位。一个进程在执行过程中会产生多个线程。同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。

线程与进程的关系,区别及优缺点

线程和进程的关系
同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。线程和进程最大的不同在于基本上各进程是独立的,各线程不一定,因为统一进程中的线程有可能相互影响。线程执行开销小,但是不利于资源的管理和保护;进程正好相反。
程序计数器为啥是私有的
为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的
为了保证线程中局部变量不被别的线程访问到。
简单说说堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要存放新创建的对象,方法区主要放已经被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

并发和并行的区别

并发
同一时间段内,多个任务执行(单位时间内部一定同时执行)
并行
单位时间,多个任务同时执行

为什么使用多线程

从计算机底层来说:线程是轻量级的进程,是程序执行的最小单位,线程之间的切换和调度成本小于进程。多核CPU时代意味多个线程可以同时运行,减少了线程上下文切换的开销。
从当代互联网发展趋势来说:多线程编程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

多线程可能带来的问题

内存泄漏、死锁、线程不安全。

线程生命周期和状态

线程创建之后它处于new(新建)状态,调用start()方法之后开始运行,线程这时候处于READY(可运行状态)。可运行的线程获得了CPU时间片后处于RUNNING(运行)状态。
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。

什么是上下文切换

任务从保存到再加载的过程就是一次上下文切换。

什么是线程死锁?如何避免死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁必须具备以下四个条件:

互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
怎么避免线程死锁
破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

sleep方法和wait方法的区别和共同点

两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁
两者都可以暂停线程的执行。
wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值