THREE------多线程

417 阅读20分钟

1、进程和线程区别,为什么使用多线程?

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程;
线程是进程的一个实体,也叫轻量级进程,是cpu调度和分派的基本单位  
一个进程会有1个或多个线程,同一进程中的多个线程之间可以并发执行。
进程有自己的独立地址空间,线程没有  
进程是资源分配的最小单位,线程是CPU调度的最小单位

• 与进程相比,线程的创建和切换开销更小
• 多CPU或多核计算机本身具有执行多线程的能力,在多CPU计算机上使用多线程能提高CPU的利用率

2、并行和并发有什么区别?

并行是指同一时刻内在不同实体上发生两个或多个事件;
并发是指同一时间间隔内发生在同一实体上两个或多个事件  

在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群[hædu:p]。 所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

3、同步和异步有什么区别?

所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或不继续执行后续操作
异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作

4、创建线程有哪几种方式? 哪种比较好?

• 继承Thread类,重写run( )方法  
• 实现Runnable接口,并实现该接口的run( )方法  
• 通过Callable和Future创建线程;即通过实现Executor框架中Callable接口,重写call( )方法 
• 基于线程池

实现Runnable 接口这种方式更受欢迎,因为这不需要继承 Thread 类。在应用设计中已经继 承了别的对象的情况下,这需要多继承(而 Java 不支持多继承),只能实现接口。

5、 说一下 Runnable 和 Callable 有什么区别?

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;

Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,
可以获取异常信息。

6、线程的生命周期或者线程有哪些状态?
线程的生命周期分为新建(New)、就绪(Runnable)、运行(Runing)、阻塞(Blocked)、死亡(Dead)
新建状态New\color{#008000}{新建状态New}
在Java中使用new关键字创建一个线程,新创建的线程将处于新建状态

就绪状态Runnable\color{#008000}{就绪状态Runnable}
新建的线程对象在调用start方法之后将转为就绪状态

运行状态Running\color{#008000}{运行状态Running}
就绪状态的线程竞争到CPU使用权并开始执行run方法的线程执行体时,会转为运行状态

阻塞状态Blocked\color{#008000}{阻塞状态Blocked}
运行中的线程主动或被动放弃CPU的使用权并暂停运行,此时该线程转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到CPU使用权并转为运行状态。阻塞状态分为3种:

• 等待阻塞:运行中的线程调用wait方法时,JVM会把该线程放入等待序列(Waitting Queue)中,线程转为阻塞状态
• 同步阻塞:运行中的线程尝试获取正在被其他线程占用的对象同步锁时,JVM会把该线程放入锁池(Lock Pool)中,
		   线程转为阻塞状态
• 其他阻塞:运行中的线程在执行Thread.sleep(long ms)、Thread.join()或者发送I/O请求时,
           JVM会把该线程转为阻塞状态

线程死亡Dead\color{#008000}{线程死亡Dead}
线程以3种方式结束后转为死亡状态

• 线程正常结束:执行完run方法或者call方法
• 线程异常退出:运行中的线程抛出一个Error或未捕获的Exception,线程异常退出
• 手动结束:调用线程对象的stop方法手动结束运行中的线程(该方法会瞬间释放线程占用的同步对象锁,
		   导致所混乱和死锁,不推荐使用)

7、什么是守护线程?

守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序  
Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法

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

sleep( )方法是Thread类(线程类)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,
等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,
它不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,
但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait( )是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,
同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程  

9、notify ( )和 notifyAll ( )有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),
被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。  

10、线程的 run( )和 start( )有什么区别?

start方法用于启动线程,此时线程处于就绪状态,并没有运行  
run方法也叫线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态  

11、Java 中的锁是什么?

在并发编程中,经常会遇到多个线程访问同一个共享变量,当同时对共享变量进行读写操作时,就会产生数据不一致的情况。 为了解决这个问题

JDK 1.5 之前,使用 synchronized 关键字,拿到 Java 对象的锁,保护锁定的代码块。
JVM 保证同一时刻只有一个线程可以拿到这个Java对象的锁,执行对应的代码块。

JDK 1.5 之后,引入了并发工具包 java.util.concurrent.locks.Lock,让锁的功能更加丰富

12、什么是死锁(deadlock)?死锁的四个必要条件和如何避免线程死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,
若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。  

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

• 互斥条件:即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有;  
• 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放;  
• 不可抢占/剥夺条件:已经分配给一个进程的资源不能强制性地被抢占,该资源只能被占有它的进程显式地释放;
• 循环等待条件:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源
(p0正在等待p1占用的资源,p1正在等待p2占用的资源...pn正在等待p0占用的资源)。

避免死锁可以概括成三种方法:

• 加锁顺序(线程按照一定的顺序加锁);
• 加锁时限(使用定时锁-->tryLock( );线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,
	      并释放自己占有的锁);        
• 死锁检测(JDK提供了两种方式来给我们检测:图形化界面工具JConsole,Jstack是JDK自带的命令行工具,
		  主要用于线程Dump分析)

13、多线程同步的实现方法有哪些?

当多个线程同时读访问同一个资源时,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,线程同步即几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作
• synchronized关键字

在Java中每个对象都有一个对象锁,该锁表明对象只允许被一个线程拥有,当一个线程调用对象的synchronized代码
时,需要先获取这个锁,再执行代码,执行结束后,释放锁  
synchronized方法和synchronized

• wait( )方法和notify ( )方法

在synchronized代码被执行期间,线程可以调用对象的wait( )方法,释放对象锁,进入等待状态,
并且可以调用notify ( )或 notifyAll ( )来唤醒其他等待的线程,并允许线程去获得锁  

• volatile关键字

volatile关键字为访问提供了一种免锁机制, 在CPU计算过程中,会将计算过程的数据加载到CPU计算缓存中,
而volatile修饰的变量是线程可见的,当JVM解释volatile修饰的变量时,会通知CPU,在计算过程中,
每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用CPU缓存中的数据,
可以保证计算结果的正确  

volatile只能保证可见性,不能保证原子性\color{#008000}{但volatile只能保证可见性,不能保证原子性}
• Lock

JDK5新增加了Lock接口以及它的一个实现类ReentranLock(重入锁),Lock也可以用来实现多线程的同步 

14、Java 中的锁有哪些?

公平锁/非公平锁\color{#008000}{公平锁/非公平锁}

公平锁是指多个线程按照申请锁的顺序来获取锁。对于 ReentrantLock而言,通过构造函数指定该锁是否是公平锁,
默认是非公平锁。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过
AQS(AbstractQueuedSynchronizer)的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁\color{#008000}{可重入锁}

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
ReentrantLock和Synchronized都是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。


独享锁/共享锁\color{#008000}{独享锁/共享锁}

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有  

ReentrantLock 和 synchronized都是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的

互斥锁/读写锁\color{#008000}{互斥锁/读写锁 }
互斥锁/读写锁就是上面独享锁/共享锁具体的实现

互斥锁在Java中的具体实现就是ReentrantLock

读写锁在Java中的具体实现就是ReadWriteLock   

乐观锁/悲观锁\color{#008000}{乐观锁/悲观锁 }
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度

对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,
不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。  

Java 里面的 synchronized 关键字的实现也是悲观锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

自旋锁\color{#008000}{自旋锁}

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,
这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU  

偏向锁/轻量级锁/重量级锁\color{#008000}{偏向锁/轻量级锁/重量级锁 }
这三种锁是指锁的状态,并且是针对Synchronized

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,
       其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,
       当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。
       重量级锁会让其他申请的线程进入阻塞,性能降低

15、 多线程锁的升级原理是什么?

在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁升级的图示过程: 16、synchronized锁和Lock锁有什么异同?

• 实现层面不一样

synchronized 是 Java 关键字,在JVM层面实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁  

• 是否自动释放锁

synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,
需要在 finally {} 代码块显式地中释放锁

• 是否可知获取锁成功

synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock()获得加锁是否成功(成功返回true

• 是否一直等待

synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时

• 功能复杂性

synchronized 的锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平

17、synchronized 和 ReentrantLock 区别是什么?

• synchronized 是Java关键字,synchronized 是JVM 层面实现的;ReentrantLock 是JDK代码层面实现  

• synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,
  需要在 finally{} 代码块显示释放  
  
• synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果

• synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间 

• synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁 

• synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();
  ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法   

18、volatile关键字作用是什么?

1.保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,
  共享变量修改后的值对其他线程立即可见;
  
2.volatile禁止指令重排,即volatile修饰的变量不会被缓存在寄存器中或者对其他处理器不可见的地方,
  因此在读取volatile类型的变量时总会返回最新写入的值。

19、什么是线程池?

线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,
处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。Java线程池是通过Executor框架实现的  

20、创建线程池有哪几种方式?或者说几种常用的线程池

newFixedThreadPool(intnThreads)\color{#008000}{newFixedThreadPool(int nThreads)}

创建一个固定线程数量的线程池,并将线程资源放在队列中循环使用  

newCachedThreadPool()\color{#008000}{newCachedThreadPool( ) }

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,
而当需求增加时,则可以自动添加新线程  

newScheduledThreadPool(intcorePoolSize)\color{#008000}{newScheduledThreadPool(int corePoolSize)}

创建了一个固定长度的线程池,可以设置给定的延迟时间的方式来执行任务  

newSingleThreadExecutor()\color{#008000}{newSingleThreadExecutor( )}

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;
它的特点是能确保依照任务在队列中的顺序来串行执行。  

newWorkStealingPool()\color{#008000}{newWorkStealingPool( ) }

创建一个拥有足够数量的线程池来达到快速运算的目的,JDK1.8新增    

21、为什么要使用线程池?

使用线程池框架 Executor 能更好的管理线程、避免频繁创建和销毁线程,提高系统资源使用率  

22、线程池有哪些状态?

线程池有5种状态:
Running\color{#008000}{Running }

线程池一旦被创建,就处于 RUNNING 状态  

Shutdown\color{#008000}{Shutdown}

不接收新任务,但能处理已排队的任务  

Stop\color{#008000}{Stop}

不接收新任务,不处理已排队的任务,并且会中断正在处理的任务  

Tidying\color{#008000}{Tidying}

线程池在Shutdown或者Stop状态,任务队列为空且执行中任务为空时,就会转变为Tidying状态

Terminated\color{#008000}{Terminated }

线程池彻底终止 

23、什么是 Executor 框架?为什么使用 Executor 框架?

Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。  

每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,
而且无限制的创建线程会引起应用程序内存溢出。
所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程,
利用Executors 框架可以非常方便的创建一个线程池

24、JDK中Atomic开头的原子类实现原子性的原理是什么?

在多线程程序中,诸如++i或i++等运算不具有原子性,i++需要经过读取--修改--写入三个步骤,因此不是安全的线程操作。我们通过synchronized或者ReentrantLock将该操作变成原子操作,但synchronized和ReentrantLock均属于重量级锁 ,只做一个++这么简单的操作,都用到了synchronized锁,未免有点小题大做了 因此JVM为此类原子操作提供了一些原子操作同步类 ,使得同步操作(线程安全操作)更方便高效

• JDK Atomic开头的类,是通过 CAS 原理解决并发情况下原子性问题;

• CAS 包含 3 个参数,CAS(V, E, N)。V 表示需要更新的变量,E 表示变量当前期望值,N 表示更新为的值。
只有当变量 V 的值等于 E 时,变量 V 的值才会被更新为 N。如果变量 V 的值不等于 E ,
说明变量 V 的值已经被更新过,当前线程什么也不做,返回更新失败;

• 当多个线程同时使用 CAS 更新一个变量时,只有一个线程可以更新成功,其他都失败。
失败的线程不会被挂起,可以继续重试 CAS,也可以放弃操作;

• 在并发量很高的情况,会有大量 CAS 更新失败,所以需要慎用。

25、什么是CAS?

Java 并发机制实现原子操作有两种: 一种是锁,一种是CASCAS是Compare And Swap(比较和替换)的缩写。  
java.util.concurrent.atomic中的很多类,如(AtomicInteger AtomicBoolean AtomicLong)都使用了CASCAS是一种基于锁的操作,而且是乐观锁。

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

ThreadLocal提供了线程的局部变量,属于线程自身所有,不在多个线程间共享,
该变量对其他线程而言是隔离的,实现了线程的数据隔离,是一种实现线程安全的方式。

但是在管理环境下(如 Web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,
工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,
Java 应用就存在内存泄露的风险。

27、什么是 AQS

所谓AQS,指的是AbstractQueuedSynchronizer,AQS 是一个用来构建锁和同步器的框架,
使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,
其他的诸如ReentrantReadWriteLock,FutureTask 等等皆是基于AQS 的。

28、谈一谈对CountDownLatch、CyclicBarrier、Semaphore的理解

CountDownLatch、CyclicBarrier、Semaphore是([ˈkaʊntdaʊnlætʃ]--[ˈsaɪklɪkˈbæriər]--['seməfɔ:(r)])Java为我们提供了三个同步工具类:

• CountDownLatch类位于java.util.concurrent包下,允许一个或多个线程
一直等待其它线程的操作执行完后再执行相关操作,CountDownLatch基于线程计数器来实现并发访问控制  

• CyclicBarrier可以实现让一组线程互相等待,直至某个状态之后再全部同时执行

• Semaphore指信号量,用于控制同时访问某些资源的线程个数

29、 在Java程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:

原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized)

可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,
      (happens-before原则)  

30、说一下synchronized 底层实现原理?
synchronized用法

修饰方法  
修饰代码块  

• 同步代码块是通过 monitorenter\color{#008000}{monitorenter} [ˈmɒnɪtə(r) ˈentə(r)]和 monitorexit\color{#008000}{monitorexit} [ˈeksɪt]指令获取线程的执行权

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,

线程执行monitorenter指令时尝试获取monitor的所有权,
过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0  

monitorexit指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,
不再是这个monitor的所有者

• 同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制

当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,
如果设置了,JVM可以知道这是一个需要同步的方法,执行线程将先获取monitor,
获取成功之后才能执行方法体,方法执行完后再释放monitor。