Java多线程部分(重要)

2019-05-27  本文已影响0人  久伴_不离

1.并行和并发有什么区别?

答案一:

并发的关键是你有处理多个任务的能力,不一定要同时。  

并行的关键是你有同时处理多个任务的能力。  

所以我认为它们最关键的点就是:是否是『同时』。

答案二:

并发(concurrency)和并行(parallellism)是:

解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。

解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件

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


2.线程和进程的区别?

答:

线程:是程序执行流的最小单元,是系统独立调度和分配CPU(独立运行)的基本单位

进程:是资源分配的基本单位。一个进程包括多个线程

区别:地址空间、资源拥有

1.线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源

2.每个进程都有自己一套独立的资源(数据),供其内的所有线程共享

3.不论是大小,开销线程要更“轻量级”

4.一个进程内的线程通信比进程之间的通信更快速,有效。(因为共享变量)


3.守护线程是什么?(守护线程和本地线程有什么区别?)

答:守护线程是个服务线程,服务于其他线程

典型案例:垃圾回收线程、nginx两个master worker进程

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,

1、守护线程,比如垃圾回收线程,就是最典型的守护线程。

2、用户线程,就是应用程序里的自定义线程。


4.创建线程有哪几种方式?

1.继承Thread类,然后调用 start 方法。

2.实现Runnable 接口的 run 方法, 然后再用 Thread 类包裹后,调用 start 方法。

3.实现Callable 接口的 call 方法,用 FutureTask 类包裹 Callable 对象。然后再用 Thread 类包裹 FutureTask 类,并调用 start 方法。call() 方法可以有返回值。

    注:Callable 方法在 Java 8 后,支持拉姆达表达式的写法,可以创建一个 FutureTask 类,语句上不是太罗嗦。 Callable 方式有以下几个优点:

     可以捕获线程上的异常。

     可以通过get 方法得到返回值。

     get 方法阻塞当前线程,直到调用的线程运行结束。

     可以取消线程的运行。

4.Java 6 之后,还可以通过创建线程池来创建线程,使用 ExecutorService 的 execute 方法:


5.说一下runnable和 callable 有什么区别?

相同点:

1.两者都是接口;(废话)

2.两者都可用来编写多线程程序;

3.两者都需要调用Thread.start()启动线程;

不同点:

1.两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;

2.Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意点:

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!


6.线程有哪些状态?

线程状态有 5 种,新建,就绪,运行,阻塞,死亡。

第一是创建(New)状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。 

  第二是就绪(Runnable)状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。 

  第三是运行(Running)状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。 

  第四是阻塞(Blocked)状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。 

  第五是死亡(Terminated)状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。


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

一:

1、这两个方法来自不同的类分别是Thread和Object,sleep方法属于Thread类中的静态方法,wait属于Object的成员方法。

2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)。

二:

1..sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

2.一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该线程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。


8.notify()和 notifyAll()有什么区别?

notify():

唤醒一个处于等待状态的线程,

注意的是在调用此方法的时候,

并不能确切的唤醒某一个等待状态的线程,

而是由JVM确定唤醒哪个线程,而且不是按优先级。

notifyAll():

唤醒所有处入等待状态的线程;

并可以理解为把他们排进一个队列;

只不过只有头部的线程获得了锁,才能运行;

注意!!并不是给所有唤醒线程一个对象的锁,而是让它们竞争,

当其中一个线程运行完就开始运行下一个已经被唤醒的线程,因为锁已经转移了。

(这个时候是否运行已经不是因为等待状态,而是处于runnning队列中)


9.线程的 run()和 start()有什么区别?

1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

2.run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。


10.创建线程池有哪几种方式?

1.ThreadPoolExecutor,

public class Main {

public static void main(String[] args) throws Exception {

// 有界队列

BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);

// 放弃拒绝的任务并抛出异常

RejectedExecutionHandler abortPolicyHandler = new ThreadPoolExecutor.AbortPolicy();

RejectedExecutionHandler discardPolicyHandler = new ThreadPoolExecutor.DiscardPolicy();

ThreadPoolExecutor threadPool =

new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS, workQueue, discardPolicyHandler);

long start = System.currentTimeMillis();

for (int i = 0; i < 40; i++) {

threadPool.execute(new MyTask());

System.out.println("核心线程数" + threadPool.getCorePoolSize());

System.out.println("最大线程数" + threadPool.getMaximumPoolSize());

System.out.println("线程池数" + threadPool.getPoolSize());

System.out.println("队列任务数" + threadPool.getQueue().size());

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");

}

System.out.println(System.currentTimeMillis()-start);

threadPool.shutdown();

if (threadPool.awaitTermination(6, TimeUnit.SECONDS)) {

threadPool.shutdownNow();

}

}

}

class MyTask implements Runnable {

@Override

public void run() {

try {

Thread.currentThread().sleep(5000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + ", hello");

}

}

2.Executors

因为ThreadPoolExecutor线程池类创建线程较为麻烦,需要设置的参数比较多。线程池工具类,提供了一些工厂方法或者实用方法用来创建或者使用线程池,使我们更加方便快捷的创建和使用线程池。

/* 

* Java通过Executors提供四种线程池,分别为:

* newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

* newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

* newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

* newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

*/


11.线程池都有哪些状态?

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

1、RUNNING

(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。 

(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

2、 SHUTDOWN

(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 

(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP

(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 

(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING

(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 

(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 

当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。 

(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。


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

execute():只能执行 Runnable 类型的任务。

submit():可以执行 Runnable 和 Callable 类型的任务。

Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。


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

答:

方法一:使用安全类,比如 Java. util. concurrent 下的类。

方法二:使用自动锁 synchronized。

方法三:使用手动锁 Lock。

手动锁Java示例代码如下:

Lock lock = new ReentrantLock();

lock.lock();try {

    System. out. println("获得锁");

} catch (Exception e) {

    // TODO: handle exception

} finally {

    System. out. println("释放锁");

    lock. unlock();

}


14.多线程锁的升级原理是什么?

答:

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。


15.什么是死锁?

答:当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。


16.怎么防止死锁?

答:

尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。

尽量使用 Java. util. concurrent 并发类代替自己手写锁。

尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。

尽量减少同步的代码块。


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

答:ThreadLocal用于保存某个线程共享变量。使用场景:解决数据库连接,Session管理


18.说一下synchronized 底层实现原理?

答:synchronized是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。


19.synchronized 和 volatile 的区别是什么?

答:

volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。


20.synchronized 和 Lock 有什么区别?

synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。


21.synchronized 和 ReentrantLock 区别是什么?

答:

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

synchronized是Java内建的同步机制,所以也有人称其为Intrinsic Locking(固有锁),它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在Java 5以前,synchronized是仅有的同步手段,在代码中,synchronized可以用来修饰方法,也可以使用在特定的代码块上,本质上synchronized方法等同于把方法全部语句用synchronized块包起来。

ReentrantLock,通常翻译为再入锁,是Java 5提供的锁实现,它的语义和synchronized基本相同。再入锁通过代码直接调用lock()方法获取,代码书写更加灵活。与此同时,ReentrantLock提供了很多实用的方法,能够实现很多synchronized无法做到的细节控制,但是在编码中也需要注意,必须要明确调用unlock()方法释放,不然就会一直持有该锁。synchronzied和ReentrantLock的性能不能一概而论,早期版本synchronized在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于ReentrantLock。


22.说一下 atomic 的原理?

答:atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。


23.死锁与活锁的区别,死锁与饥饿的区别?

首先死锁是同步的,饥饿时异步的。也就是说,死锁可以认为是两个线程或进程同时在请求对方占有的资源,饥饿可以认为是一个线程或是进程在无限的等待另外两个或多个线程或进程占有的但是不会往外释放的资源。

1.死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2.活锁:线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去.(很绅士,互相谦让)


24.什么是线程组,为什么在 Java 中不推荐使用?

ThreadGroup类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

线程组ThreadGroup对象中的stop,resume,suspend会导致安全问题,主要是死锁问题,已经被官方废弃,多以价值已经大不如以前。

线程组ThreadGroup不是线程安全的,在使用过程中不能及时获取安全的信息。


25.如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时间最长?

(Linux)

1. 找出cpu耗用厉害的进程pid

2. 根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查找出cpu利用率最厉害的线程号

3. 将获取到的线程号转换成16进制,去百度转换一下就行

4. 使用jstack工具将进程信息打印输出

5. 编辑/tmp/t.dat文件,查找线程号对应的信息

(Windows)

1.使用Process Explorer,第三方工具定位,使用比较简单,容易上手。(https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer )

第一步: 获得该程序的进程ID

第二步: 打开Process Explorer工具

第三步: 查找进程号为“11964”的进程

第四步: 打开该进程Properties界面

第五步: 选中“Threads”标签页,查看线程统计信息

第六步: 线程ID转换成十六进制

第七步: dump线程信息

第八步: 查找线程ID为“4E38”的线程

2.使用window自带的perfmon 性能监控工具进行监控,功能强大,但稍微有点复杂。

第一步: 首先使用 jps 获取当前程序运行的进程ID

第二步: 打开perfmon 性能监视器

第三步: 打开添加计数器页面

第四步: 添加计数器

第五步: 切换报告显示方式

第六步: 查找使用CPU比较高的线程

第七步: 线程ID 转换成十六进制

第八步: dump 线程信息

第九步: 查找线程ID的线程


26.什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在Java中可以通过锁和循环CAS的方式来实现原子操作。CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作。

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

java.util.concurrent这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解决ABA问题的原子类:AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个int来累加来反映中间有没有变过)


27.Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。

他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:

可以使锁更公平

可以使线程在等待锁的时候响应中断

可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

可以在不同的范围,以不同的顺序获取和释放锁

整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。


28.什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景(有点像mq),生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7提供了7个阻塞队列。分别是:

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Java 5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait ,notify,notifyAll,sychronized这些关键字。而在java 5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。


29.多线程同步和互斥有几种实现方法,都是什么?

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。


30.什么是竞争条件?你怎样发现和解决竞争?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。


31.什么是线程安全? 

线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的可修改的状态的正确性,这里的状态反映在程序中其实可以看做是数据。

换个角度来看,如果状态不是共享的,或者不是可修改的,也就不存在线程安全问题,进而可以推理出保证线程安全的两个办法:第一个是封装,我们可以将对象内部状态隐藏保护起来。第二个是不可变。

线程安全需要保证几个基本特性:第一个是原子性,简单来说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。第二是可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存中,volatile关键字就是负责保证可见性的。第三个是有序性,是保证线程内串行语义,避免指令重排等。


32.Java并发类库提供的线程池有哪几种? 分别有什么特点?

经典回答:

通常开发者都是利用Executors提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的ExecutorService类型或者不同的初识参数。

Executors目前提供了5种不同的线程池创建配置:

newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60S,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗资源。

newFixedThreadPool(int nThreads),重用指定数目的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads。

newSingleThreadExecutor(),它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改造线程实例,因此可以避免其改变线程数目。

newSingleThreadScheduledExecutor()和newSecheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。


未完待续。。。。将不定时更新

上一篇下一篇

猜你喜欢

热点阅读