java多线程与高并发

多线程与高并发

1.线程的生命周期及状态

(1)线程通常有五种状态:新建、就绪、运行、阻塞和死亡状态
	a.新建状态:新创建了一个线程对象
	b.就绪状态:线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于
			  可运行线程池中,变为可运行, 等待获取CPU的使用权
	c.运行状态:就绪状态的线程获取了CPU的使用权,开始执行程序代码
	d.阻塞状态:阻塞状态是线程因为某种原因放弃了CPU的使用权,暂时停止运行。直到线程进入就绪状态,
			  才有机会转到运行状态
	e.死亡状态:线程执行结束或因为异常退出了run(),该线程结束生命周期

(2)阻塞的情况又分为三种:
	a.等待阻塞:运行的线程执行wait(),该线程会释放所占用的资源,JVM会把线程放入等待池中进。
			  入这个状态后,不能自动唤醒,必须依靠其他线程调用notify()或nitifyAll()
			  才能被唤醒,,wait是Object的方法
	b.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,
			  则JVM会把该线程放入锁池中
	c.其他阻塞:运行的线程执行sleep()或join(),或者发出了I/O请求时,JVM会把该线程设置为
			  阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,
			  线程重新转入就绪状态。sleep是Thread类的方法

2.sleep() wait() join() yield()的区别

(1)锁池:所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,
	则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,
	当某个线程得到后就会进入就绪队列进行等待CPU资源分配

(2)等待池:当我们调用wait()后,线程会放入等待池中,等待池的线程不会去竞争同步锁。
	只有调用了notify()或notifyAll()后等待池中的线程才会开始去竞争锁,notify()
	是随机从等待池中选出一个线程放到锁池,而notifyAll()是将等待池中的所有线程放到锁池当中

(3)sleep()是Thread类的静态本地方法    而wait()则是Object类的本地方法
	sleep()不会释放锁    但是wait()会释放,而且会加入等待队列中
	sleep()不依赖于同步器synchronized     但是wait()需要依赖synchronized关键字
	sleep()不需要被唤醒(休眠结束后会退出阻塞状态)    wait()需要被唤醒
	sleep()一般用于当前线程休眠或者轮询暂停操作   wait()则多用于多线程之间的通信
	sleep()会让出CPU执行时间且强制上下文切换   wait()后可能还有机会重新竞争到锁继续执行

(4)yield()执行后线程直接进入就绪状态,马上释放CPU的执行权,但是依然保留了CPU的执行资格,
	所以有可能CPU下次进行线程调度时还会让这个线程获取到执行权继续执行

(5)join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B会进入到阻塞队列,
	直到线程A结束或中断线程

3.对线程安全的理解

堆是共享内存,可以被所有线程访问,所以堆会存在线程安全问题。

当多个线程访问一个对象时,如果不进行额外的同步控制或其他协调操作,调用这个对象的行为都可以
获得正确的结果,我们就说这个对象是线程安全的

4.Thread和Runable的区别

Thread和Runnable的实质是继承关系,没有可比性。无论使用Thread还是Runable都会new Thread,
然后执行run()。

用法上:如果有复杂的线程操作需求,那就继承Thread;如果只是简单的执行一个任务,那就实现Runable

5.ThreadLocal的底层原理

(1)ThreadLocal是java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,
	该线程可以在任意时刻、任意方法中获取缓存的数据

(2)ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象中都存在一个
	ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值

(3)如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完后,
	应该要把设置的key和value,进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指
	ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象
	也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用
	ThreadLocal的remove方法,手动清除entry对象

(4)ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法
	之间进行传递,线程之间不共享同一个连接)

6.并发 并行 串行的区别

(1)并发:同一时刻多个线程访问同一个资源,允许多个任务彼此干扰,同一时间点只有一个任务运行,交替执行

(2)并行:多个任务同时进行,互不干扰,在时间上是重叠的

(3)串行:在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着

7.并发的三大特性

(1)原子性:指在一个操作中CPU不可以在中途暂停然后再调度(即不被中断操作),要不全部执行成功,
	要不都不执行synchronized可以实现原子性

(2)有序性:代码按照编写的顺序来执行,不能指令重排
	volatile synchronized可以实现有序性

(3)可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
	volatile synchronized final 可以实现可见性

8.为什么使用线程池,解释一下线程池参数

(1)为什么使用线程池:
	a.降低资源消耗、提高线程的利用率,降低创建和销毁线程的消耗
	b.提高响应速度,执行一个任务是直接可以使用线程池中的线程,不需要再去创建线程
	c.提高线程的可管理性,线程是稀缺资源,使用线程池可以统一分配调优监控

(2)核心参数
	a.核心线程数(corePoolSize):线程池正常情况下创建工作的线程数,这些线程创建后并不会被销毁,
	  而是一种常驻线程
	b.最大线程数(maxinumPoolSize):表示最大允许被创建的线程数,当前任务比较多时,
	  核心线程数都用完了并且阻塞队列已满,此时就会创建新的线程,但是线程池内线程的总数
	  不会超过最大线程数
	c.缓冲队列(workQueue):用来存放待执行的任务,假设核心线程都已被使用,还有任务进来则
	  全部放入队列,直到整个队列被放满但任务还在持续进入则会开始创建新的线程
	d.最大存活时间(KeepAliveTime):表示超过核心线程数之外的线程的空闲存活时间,
	  可以通过setKeepAliveTime来设置空闲时间
	e.拒绝策略(handler):有两种情况:一种是当达到最大线程数,线程池已经没有能力继续处理
	  新提交的任务时,就会执行拒绝策略;另一种是当我们调用shutdown等方法关闭线程池后,
	  这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,
	  我们再继续向线程池提交任务就会遭到拒绝
	f.最大存活时间的单位
	g.线程工厂:构建thread对象

9.线程池的底层工作原理

线程池内部是通过队列 + 线程实现的,当我们利用线程池执行任务时:

(1)当线程池中线程的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程
	来处理被添加的任务

(2)当线程池中线程的数量等于corePoolSize,但是缓冲队列未满,那么任务被放入缓冲队列

(3)当线程池中线程的数量大于等于corePoolSize,缓冲队列已满,并且线程池中线程的数量小于最大线程数,
	此时就新建线程来处理新添加的任务

(4)如果此时线程池中线程的数量大于corePoolSize,缓冲队列已满,并且线程池中线程的数量等于
	最大线程数,那么通过handler所指定的策略来处理此任务

(5)当线程池中线程的数量大于corePoolSize时,如果某线程空闲时间超过KeepAliveTime,
	该线程终止。这样,线程池可以动态的调整池中的线程数

10.线程池中阻塞队列的作用,为什么先添加队列而不是创建最大线程数

(1)阻塞队列的作用:
	a.一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲区长度,就无法保留当前任务了,
	  而阻塞队列可以保留住当前想要继续入队的任务
	b.阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放CPU资源
	c.阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法
	  挂起,从而维持核心线程的存活、不至于一直占用CPU资源

(2)为什么先添加队列而不是创建最大线程数
	a.因为创建新的线程需要更多的资源
	b.在创建新的线程的时候,是要获取全局锁的,这个时候其他线程就得阻塞,影像了整体效率

11.线程池中线程的复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的
一个线程必须对应一个任务的限制

线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理是在于线程池对Thread
进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行
一个"循环任务",在这个"循环任务"中不停的检查是否有任务需要被执行,如果有则直接执行,也就是
调用任务中的run(),将run()当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有的
run()串联起来

12.ReentrantLock中的公平锁和非公平锁的底层实现

(1)公平锁和非公平锁的底层实现都会使用AQS来进行排队的

(2)区别:线程在使用lock()加锁时:
	a.公平锁:会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
	b.非公平锁:不会去检查是否有线程在排队,而是直接竞争锁

(3)不管是公平锁还是非公平锁,一旦没有竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,
	所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段

(4)ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的

13.ReentrantLock中tryLock()和lock()方法的区别

(1)tryLock:表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁就返回true,
	如果没有加到则返回false

(2)lock:表示阻塞加锁,线程会阻塞直到加到锁,方法没有返回值

14.CountDownLatch和Semaphore的区别和底层原理

(1)CountDownLatch:表示计数器,可以给CountDownLatch设置一个数字,一个线程调用
	CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()
	来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒
	对应底层原理:调用await()的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒

(2)Semaphore:表示信号量,可以设置许可个数,表示同时允许最多多少个线程来使用该信号量,
	通过acquire()来获取许可,如果没有许可可用则该线程阻塞,并通过AQS来排队,可以通过
	release()来释放许可,当某个线程释放了许可后,会从AQS中正在排队的第一个线程开始依次唤醒,
	直到没有空闲许可

15.Sychronized的偏向锁 轻量级锁 重量级锁

(1)偏向锁:在锁对象的对象头中记录一下当前获取该锁的线程ID,该线程下次如果又来获取该锁就可以
		   直接获取到了

(2)轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程
			来竞争锁,偏向锁就会升级为轻量级锁,轻量级锁底层是通过自旋来实现的,并不会阻塞线程

(3)重量级锁:如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞

16.ReentrantLock和Sychronized的区别

(1)Sychronized是一个关键字    ReentrantLock是一个类

(2)Sychronized会自动加锁和释放锁   ReentrantLock需要手动加锁和释放锁

(3)Sychronized是非公平锁    ReentrantLock可以选择公平锁或非公平锁

(4)Sychronized是JVM层面的锁    ReentrantLock是API层面的锁

(5)Sychronized锁的是对象,锁信息保存在对象头中    ReentrantLock通过代码中int类型的
											 state标识来标识锁的状态

(6)Sychronized底层有一个锁升级的过程

17.java如何开启线程?怎么保证线程安全

(1)线程和进程的区别:进程是操作系统进行资源分配的最小单元,线程时操作系统进行任务分配的最小单元,
	线程隶属于进程

(2)如何开启线程:
	a.继承Thread类,重写run()
	b.实现Runnable接口,是西安run()
	c.实现Callable接口,实现cll(),通过FutureTask创建一个线程,获取到线程执行的返回值
	d.通过线程池来开启线程

(3)如何保证线程安全:加锁  JVM提供的锁:synchronized关键字   JDK提供的各种锁

18.volatile和Sychronized有什么区别?volatile能不能保证线程安全?DCL单例为什么要加volatile

(1)区别:Sychronized关键字是用来加锁   volatile只是保持变量的现成的可见性(通常适用于一个线程写,
									多个线程读的场景)

(2)volatile不能保证线程安全,因为它只能保证有序性和可见性,不能保证原子性

(3)volatile禁止指令重排(使用内存屏障),在DCL中,防止高并发情况下,指令重排造成的线程安全问题

19.java线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?

(1)锁机制:java的锁就是在对象的Markword中记录一个锁状态,偏向锁、轻量级锁、重量级锁对应不同的锁状态

(2)锁机制是根据资源竞争的激烈程度不断进行锁升级的过程

20.ThreadLocal的原理和使用场景

(1)原理:每一个Thread对象均含有一个ThreadLocalMap类型的成员变量,它存储本线程所有
		 ThreadLocal对象及其对应的值,ThreadLocalMap由一个个Entry对象构成,Entry
		 的key是ThreadLocal对象,value是存储的值。
		 每一个线程均有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在
		 线程安全问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性

(2)使用场景:
	a.在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
	b.线程间数据隔离
	c.进行事务操作,用于存储线程事务信息
	d.数据库连接,Session会话管理

21.ThreadLocal内存泄漏原因,如何避免

(1)什么是内存泄漏:内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,
	但内存泄漏堆积后果很严重。不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。

(2)内存泄漏原因:ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应的key就会导致内存泄漏

(3)避免:
	a.每次使用完ThreadLocal都调用它的remove()清除数据

22.如何查看线程死锁

23.线程之间如何进行通讯的

(1)线程之前可以通过共享内存或者基于网络来进行通信

(2)如果是通过共享内存进行通信,则需要考虑并发问题

(3)通过网络通信就是,通过网络连接将通信数据发送给对方,也要考虑到并发问题,处理方式就是加锁等

24.并发编程三要素

(1)原子性:不可分割的操作,多个步骤要保证同时成功或者同时失败

(2)有序性:程序执行的顺序和代码的顺序保持一致

(3)可见性:一个线程对共享变量的修改,其他线程可以马上看到

25.java死锁如何避免

(1)造成死锁的原因:
	a.一个资源每次只能被一个线程使用
	b.一个线程在阻塞等待某个资源时,不释放已占有的资源
	c.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
	d.若干线程形成头尾相接的循环等待资源关系

(2)避免:不出现循环等待锁的关系
	a.要注意加锁顺序,保证每个线程按同样的顺序进行加锁
	b.要注意加锁时限,可以针对锁设置一个超时时间
	c.要注意死锁检查,这是一种预防机制,确保第一时间发现死锁并进行解决

26.如果提交任务时线程池队列已满,这时会发生什么

(1)如果使用的无界队列,那么可以继续提交任务

(2)如果使用的有界队列,线程池队列已满的情况下,如果线程池中的线程数没有达到最大线程数,
	则可以增加线程,如果线程数已经达到了最大值,那么就是用拒绝策略进行拒绝

27.volatile关键字如何保证可见性和有序性

(1)可见性:加了volatile关键字的成员变量,在对这个变量进行修改时,会直接将CPU高级缓存中的数据
	写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性

(2)有序性:在对volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到
	禁止重排序的效果,从而可以保证有序性

28.同步和异步的区别

(1)同步:功能调用时,在没有得到结果前,该调用就不返回或继续执行后续操作。这时程序时阻塞的,只有
	接收到返回的值或消息后才往下执行其他的命令。

(2)异步:与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作,
	当这个调用完成后,一般通过状态或者回调来通知调用者

29.通过ThreadPoolExecutor如何创建线程池

public class ThreadPoolTest {

    public static void main(String[] args) {

        int corePoolSize=3;//核心线程数
        int maximumPoolSize=6;//最大线程数
        long keepAliveTime=2;//非核心线程的最大空闲时间
        TimeUnit unit=TimeUnit.SECONDS;//最大空闲时间以秒为单位
        //创建工作队列,用于存放提交的等待执行任务,容量为2
        BlockingQueue<Runnable> workQueue=new ArrayBlockingQueue<>(2);
        ThreadPoolExecutor threadPoolExecutor=null;//定义线程池

        try {
            //创建线程池
            threadPoolExecutor=new ThreadPoolExecutor(corePoolSize,maximumPoolSize,
            										 keepAliveTime,unit,workQueue,
            										 new ThreadPoolExecutor.AbortPolicy());

            //循环提交任务
            for (int i=0;i<8;i++){
                //提交任务的索引
                final int index=i+1;
                threadPoolExecutor.submit(()->{
                    //线程打印输出
                    System.out.println("我是线程: "+index);
                    try {
                        //模拟线程执行时间 设置10s的原因是为了更好的显示新任务提交时队列的作用
                        Thread.sleep(10000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                });
                //每个任务提交后休眠500ms再提交
                Thread.sleep(500);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            threadPoolExecutor.shutdown();
        }

    }

}

30.线程池的拒绝策略

(1)AbortPolicy:该策略会直接抛出异常,阻止系统正常工作

(2)CallerRunsPolicy:只要线程池未关闭,被拒绝的任务将由调用者线程去执行;如果执行程序已关闭,
					  则会丢弃该任务

(3)DiscardPolicy:丢弃无法处理的任务,什么都不做

(4)DiscardOldestPolicy:当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最老的未处理
						 任务,然后将被拒绝的任务添加到等待队列中

31.ThreadPoolExecutor的execute()方法和 submit()方法的区别是什么

(1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否

(2)submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,
	通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法
	来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 
	get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,
	这时候有可能任务没有执行完

32.线程池如何关闭

(1)shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会
				   返回被中断的队列中的任务列表

(2)shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被
				执行拒绝策略

(3)isTerminated():当正在执行的任务及队列中的任务全部都执行(清空)完就会返回true

33.线程池复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程
必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行
了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去不停的检查
是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run(),将run()当成一个普通的方法
执行,通过这种方式将只使用固定的线程就将所有人物的run()串联起来

34.线程池都有哪几种工作队列

(1)ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序

(2)LinkedBlockingQueue:是一个基于链表结构的阻塞队列,按FIFO排序元素,吞吐量通常要高于
	ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列

(3)SynchronousQueue:是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除
	操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。静态工厂方法
	Executors.newCachedThreadPool()使用了这个队列。

(4)PriorityBlockingQueue:是一个具有优先级的无界阻塞队列

35.ThreadLocal和Synchonized区别

两者都用于解决多线程并发访问,但是它们有本质区别

(1)Synchronized用于线程间的数据共享   ThreadLocal则用于线程间的数据隔离

(2)Synchronized是利用锁的机制,使变量或者代码块在某一时刻只能被一个线程访问
	ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,‘
	这样就隔离了多个线程对数据的共享。
	而Synchronized用于在多线程间通信时能够获得数据共享

36.ThreadLocal内存泄漏以及解决方案

(1)如果ThreadLocal没有外部强引用,那么在发生垃圾回收的时候,ThreadLocal就必定会被回收,
	而ThreadLocal又作为ThreadLocalMap中的key,ThreadLocal被回收会导致一个key为null
	的entry,外部就无法通过key来访问这个entry,垃圾回收也无法回收,这就造成了内存泄露

(2)解决方案:每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将
	ThreadLocal变量定义为private static,这样就一直存在ThreadLocal的强引用,也就能
	保证任何时候都能通过ThreadLocal的弱引用访问到entry的value值,进而清除掉

37.java的线程状态

new 新建:普通的类,还没有和真正的线程关联起来,调用start()之后才会和真正的线程关联起来
RUNNABLE:获取锁之后发现条件不满足,释放锁,进入WAITING等待,会被其他线程唤醒

38.lock 和 synchronized

(1)语法层面

synchronized是关键字,源码在JVM中,用C++实现的
lock是接口,源码由JDK提供,用java语言实现的
使用synchronized时,退出同步代码块锁会自动释放;而使用lock时,需要手动调用unlock方法释放锁

(2)功能层面

二者均属于悲观锁,都具备基本的互斥、同步、锁重入功能
lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock

(3)性能层面

在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能较好
在竞争激烈时,lock的实现通常会提供更好的性能

39.volatile能否保证线程安全

线程安全要考虑三个方面:可见性、有序性、原子性
可见性:一个线程对共享变量修改,另一个线程能看到最新的结果
有序性:一个线程内代码按编写的顺序执行
原子性:一个线程内多行代码以一个整体运行,期间不能有其他线程代码插队

volatile能够保证可见性与有序性,不能保证原子性

(1)可见性–不可见的原因

JVM中有一个JIT,会将一些热点的(频繁在内存调用的变量)字节码的机器码缓存起来,这样就会导致
某个线程访问到的变量的值一直都是之前某个时间点访问到的变量的值。这样就会导致即使其他线程
已经修改了这个变量,他也不会读取到被修改的变量,这就造成了不可见。

而如果使用volatile修饰后,JIT就不会去修改这个变量的值了

(2)有序性

有序性的检测需要大量数据进行压力测试
内存屏障:写变量时:保证屏障上边的数据不能在屏障下方再写,所以此时需要把加了volatile的
				变量放在下边执行
        读变量时:保证屏障下边的数据不能在屏障上方读取,所以此时需要把加了volatile的变量
        		放在上边执行

40.java中的悲观锁和乐观锁

(1)悲观锁

悲观锁的代表是lock 和 synchronized
(1)核心思想是:线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程
			  都需要停下来等待
(2)线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
(3)实际上,线程在获取lock 和 synchronized锁时,如果锁已被占用,都会做几次重试操作,
	减少阻塞的机会

(2)乐观锁

乐观锁的代表是AtomicInteger,使用 CAS 来保证原子性
(1)核心思想是:无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,
	不断重试直至成功
(2)由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
(3)需要多核CPU支持,且线程数不应超过CPU核数

使用CAS时需要配合volatile一起使用

41.Hashtable和ConcurrentHashMap

Hashtable和ConcurrentHashMap都是线程安全的Map集合

(1)Hashtable并发度低,整个Hashtable对应一把锁,同一时刻,只能有一个线程操作他

(2)JDK1.8之前ConcurrentHashMap使用了Segment+数组+链表的结构,每个Segment对应一把锁,
	如果多个线程访问不同的Segment,则不会冲突
JDK1.8之后ConcurrentHashMap将数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突

42.对ThreadLocal

ThreadLocal可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题。
ThreadLocal同时实现了线程内的资源共享

原理:每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象
(1)调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,方法当线程的
	ThreadLocalMap集合中
(2)调用get方法,就是以ThreadLocal自己作为Key,到当前线程中查找关联的资源值
(3)调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值

为什么ThreadLocalMap中的key(即ThreadLocal)要设计为弱引用:
(1)Thread可能需要长时间运行(如线程池中的线程)。如果key不再使用,需要在内存不足(GC)时
	释放其占用的内存
(2)但GC仅是让key的内存释放,后续还要根据key是否为null来进一步释放值的内存,释放时机有:
      获取key发现null key(ThreadLocalMap与其他Map集合不同,他发现要获取的ke为null时会把这
      					个key加进去)
      set key时,会使用启发式扫描,清除临近null key,启发次数与元素个数,是否发现null key有关
      remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变量,因此GC无法回收

43.多线程的好处

(1)采用多线程的应用程序可以更好的利用系统资源

(2)充分利用CPU的空闲时间片,用尽可能少的时间来对用户的要求做出响应

(3)使进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性

(4)同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或
	共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决

44.多线程可能带来的问题

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

45.线程和进程的区别

(1)进程是一段正在运行的程序,是操作系统进行资源分配和调度的基本单位
	线程是进程的子任务,是任务调度和执行的基本单位

(2)进程在执行过程中拥有独立的内存单元
	多个线程共享进程的内存

(3)同一个进程的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈
	和本地方法栈。所以系统在产生一个线程或者是各个线程之间切换时,负担要比进程小得多

46.线程创建的方式

(1)继承Thread类
(2)实现Runnable接口
(3)使用线程池创建

47.线程的run()和start()的区别

(1)启动一个线程需要调用Thread对象的start()

(2)调用线程的start()后,线程处于可运行状态,此时它可以由JVM调度并执行,这并不意味着
	线程就会立即运行

(3)run()是线程运行时由JVM回调的方法,无需手动写代码调用

(4)直接调用线程的run(),相当于在调用线程里继续调用一个普通方法,并未启动一个新的线程

48.什么是上下文切换

当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,
可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

49.如何创建守护线程

在调用start()方法前调用Thread类的setDaemon(true)方法,可以将线程设置为守护线程

50.用户线程和守护线程的区别

(1)当没有用户线程在运行的时候,JVM关闭程序并且退出

(2)守护线程不会影响到JVM的终止,即使后台还有守护线程在执行,JVM也会关闭退出

51.线程数过多会造成什么异常

(1)消耗过多的CPU资源

(2)如果课运行的线程数量多于可用处理器的数量,那么有线程会被闲置,大量空闲的线程会占用很多
	内存,给垃圾回收器带来压力,而且大量的线程竞争CPU资源时还将产生其他性能的开销

(3)降低稳定性

52.线程之间是如何进行通讯的

(1)wait()和notify()

(2)join()
	使A线程加入B线程中执行,B线程进入阻塞状态,只有A线程运行结束后B线程才会继续执行

(3)volatile关键字
	实现线程变量之间真正的共享

53.同步和异步的区别

(1)同步:功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作

(2)异步:当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作

54.为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用

(1)当一个线程调用对象的wait()时,这个线程必须拥有该对象的锁;当一个线程调用对象的
	notify()时,也必须拥有该对象的锁

(2)由于这些方法都需要线程持有对象的锁,这样就只能通过同步来实现

55.为什么要使用并发编程

(1)为了能提高程序的执行效率提高程序的运行速度

(2)充分利用多核CPU资源

56.并发编程的缺点

(1)线程数量控制不好,频繁地创建、销毁线程和线程间的切换,比较消耗内存和时间

(2)容易带来线程安全问题

(3)多线程容易造成死锁、活锁、线程饥饿等问题

57.什么是重排序

(1)重排序指在执行程序时,为了提高性能,从源代码到最终执行指令的过程中,编译器和处理器
	会对指令进行重排的一种手段

(2)源代码 -> 编译器优化重排序 -> 指令级并行重排序 -> 内存系统重排序 -> 最终执行的指令序列

58.互斥锁和自旋锁理解

(1)互斥锁:在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作。加锁后,任何其他
		   试图再次加锁的线程都会被阻塞,直到当前线程解锁。

(2)自旋锁:是一种特殊的互斥锁,当资源被加锁后,其它线程想要再次加锁,此时该线程不会被阻塞而是
		   陷入循环等待状态(CPU不能做其它事情),循环检查资源持有者是否已经释放了资源,这样做
		   的好处是减少了线程从睡眠到唤醒的资源消耗,但会一直占用CPU的资源。

59.CAS怎么保证原子性的

当前线程做完比较,发现还是旧值,在做写入的时候,有其他线程来了,把值修改了,就会出现数据不一致的
问题

解决:
	JDK -> Unsafe -> native(C/C++) -> asm(汇编实现) cmpxchg(CPU 的汇编指令),所以CAS
	操作CPU本身有指令支持,但是cmpxchg这条指令不保障原子性
	当有多个CPU时,会加 lock cmpxchg,相当于锁总线,其它CPU不能使用。
	lock 优先锁定 缓存行,其次锁定北桥信号

60.是不是乐观锁一定比悲观锁效率更高

不一定

因为悲观锁当线程获取不到锁的时候,就会去阻塞队列等待;但是乐观锁会一直自旋,会很消耗CPU资源

所以当线程执行速度比较快、线程数少,就用乐观锁;否则用悲观锁

61.什么是偏向锁

第一个线程来的时候,发现没有其它线程加锁,就会在锁对象的头记录自己的线程id,那么它下次再来的时候
就不会在加锁了。

当有其它线程来的时候,会撤销偏向锁

为什么设置偏向锁:在大量实践中统计,70%-80%的时间,只有一个线程来访问

62.ABC问题

三个线程 分别可以输出 A B C,实现 ABC ABC ABC...
public class ABC {
	private static ReentrantLock lock = new ReentrantLock();
    private volatile static Condition conditionA = lock.newCondition();//A等待的地方
    private volatile static Condition conditionB = lock.newCondition();//B等待的地方
    private volatile static Condition conditionC = lock.newCondition();//C等待的地方

    private static CountDownLatch latchB = new CountDownLatch(1);
    private static CountDownLatch latchC = new CountDownLatch(1);

    public static void main(String[] args) {

        Thread threadA = new Thread(() -> {

            lock.lock();

            try{
                for (int i = 0;i < 10;i++){
                    System.out.print("A");
                    conditionB.signal();//唤醒B
                    if(i == 0){//保证B在A之后执行
                        latchB.countDown();
                    }
                    conditionA.await();//A阻塞
                }
                conditionB.signal();//为了让所有线程都运行结束
            } catch (InterruptedException e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        },"a");

        Thread threadB = new Thread(() -> {

            //B等待A唤醒
            try {
                latchB.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            lock.lock();

            try{
                for (int i = 0;i < 10;i++) {
                    System.out.print("B");
                    conditionC.signal();//唤醒C
                    if (i == 0) {//保证C在B之后输出
                        latchC.countDown();
                    }
                    conditionB.await();
                }
                conditionC.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }

        },"b");

        Thread threadC = new Thread(() -> {

            //C等待B唤醒
            try {
                latchC.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            lock.lock();

            try{
                for (int i = 0;i < 10;i++) {
                    System.out.println("C");
                    conditionA.signal();//唤醒A
                    conditionC.await();
                }
                conditionA.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }

        },"c");

        threadA.start();
        threadB.start();
        threadC.start();
    }

}

63.synchronized锁对象的时候不能锁哪些

String常量
Integer Long 等包装类型对象

64.线程池状态

JDK内部使用一个原子整形常量 ctl ,它的低29位用来描述工作线程的个数 高三位用来表示线程池的状态

65.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cw旧巷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值