首页 > 学院 > 开发设计 > 正文

线程常用知识问答

2019-11-06 06:32:10
字体:
来源:转载
供稿:网友
1.什么是线程?
 线程是操作系统能够运行调度的最小单位,它被包含在进程之中。
 线程可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。
 线程可以将大部分的异步工作流转换成串行工作流。
 线程可以降低代码的复杂度,是代码更容易编写、阅读和维护。
 线程是一把双刃剑。
 编写多线程对开发人员要求高,线程安全不易控制.线程会遇到难以分析的问题,如:死锁、饥饿。
 线程同步和切换会带来性能的问题。
  
 2.线程和进程有什么区别?
 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行任务。
 不同的进程使用不同的内存空间,而同一进程中的所有的线程都将共享进程的内存地址空间。
  
 3.如何在java中实现线程?
 三种实现
 1.继承Thread
 2.实现Runnable接口
 3.使用Executor创建
 建议使用Exectutor来实现线程,它为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。
 它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命
 周期的支持以及统计信息收集、应用程序管理机制和性能监视等机制。
  
 4.Thread类中的start()方法和run()方法有什么区别?
 调用start()方法是启动一个新的线程,然后start()方法内部调用了run()方法。
 run()方法只是一个普通的方法,直接调用run()不会启动一个新的线程。
  
 5.Runnable和Callable有什么不同?
 Runnable和Callable都代表那些要在不同的线程中执行的任务。
 主要区别在于:Callable()的call()方法可以返回装载有计算结果的Future对象和抛出异常,而Runnable不可以。
  
 6.volatile变量
 Java语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新.
 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.提供了线程的可见性。(想象成get()和set())
 volatile只能确保可见性,不能确保原子性。
 通常用在某个操作完成、发生中断或者状态的标志。(个人认为常作用在 boolean 枚举 常量)
  
 7.线程安全
 当多个线程访问某个类时,不管这些程序将如何交替执行,都能表现正确的行为,则代表线程安全。
 要保证线程安全,外修排斥,内修可见。
  
 8.竞态条件
 竞态条件称为“先检查后执行”:首先观察到某个条件为真,然后根据这个观察结果采用相应的动作,但事实上,
 在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效了,从而导致各种问题。
 所以需要保证复合操作的原子性,使用synchronized或原子变量类。
  
 9.如何停止线程?
 如果使用Executor创建的线程直接shutdown就好了
 使用Thread停止线程
 1. 使用violate boolean变量来标识线程是否停止
 2. 停止线程时,需要调用停止线程的interrupt()方法,因为线程有可能在wait()或sleep(), 提高停止线程的即时性
 PRivate volatile Thread myThread;
 public void stopMyThread() {
 Thread tmpThread = myThread;
 myThread = null;
 if (tmpThread != null) {
 tmpThread.interrupt();
 }
 }
 public void run() {
 if (myThread == null) {
 return; // stopped before started.
 }
 // do some more work
 }
 10.在java中wait和sleep方法的不同?
 java 线程中的sleep和wait有一个共同作用,停止当前线程任务运行,不同之处在于:
 wait是Object的方法,sleep是Thread的方法
 最主要的wait方法进入等待后,会释放对象锁。而sleep方法在同步块内等待,不会释放锁对象。
 wait方法必须在同步方法或者同步块中使用,sleep方法可以在很多地方使用。
 sleep必须捕获异常,wait不需要
  
 11.什么是不可变对象,它对写并发应用有什么帮助?
 不可变的对象指的是一旦创建之后,它的状态就不能改变。String类就是个不可变类,它的对象一旦创建之后,值就不能被改变了。
 不可变对象对于缓存是非常好的选择,因为你不需要担心它的值会被更改。
 不可变类的另外一个好处是它自身是线程安全的,你不需要考虑多线程环境下的线程安全问题。
 要创建不可变类,要实现下面几个步骤:
 1.将类声明为final,所以它不能被继承
 2.将所有的成员声明为私有的,这样就不允许直接访问这些成员
 3.对变量不要提供setter方法
 4.将所有可变的成员声明为final,这样只能对它们赋值一次
 5.通过构造器初始化所有成员,进行深拷贝(deep copy)
 6.在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
  
 12.在Java中什么是线程调度?
 JVM调度的模式有两种:分时调度和抢占式调度。
 分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;
 抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式。
 既然是抢占调度,那么我们就能通过设置优先级来“有限”的控制线程的运行顺序,注意“有限”一次。
  
 14.ThreadLocal
 ThreadLocal类用来提供线程内部的局部变量。
 这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。
 ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
 ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
  
 15.BlockingQueue
 BlockingQueue
 线程安全的队列集合。具有阻塞功能。
 提供了可阻塞的put和take方法,以及支持定义的offer和poll方法。
 简化了生产者-消费者设计的实现过程,它支持任意数量的生产者和消费者。
 一种常见的生产者-消费者设计模式就是线程池与工作队列的组合。
  
 以两个人包饺子为例,两者的劳动分工也是一种生产者-消费者模式:其中一个人把做好的面皮放在案板上,而另一个人从案板上拿走面皮并包饺子。
 这个实例中,面板相当于阻塞。如果案板上没有面皮,那么消费者会一直等待,直到有面皮。
 如果案板上放满了,那么生产者会停止做面皮,直到盘加上有更多的空间。我们可以将这种类比扩展为多个生产者和多个消费者,每个人只需和案板打交道。
 人们不需要直到究竟有多少生产者或者消费者,或者谁生产了指定的工作项。
  
 16.ConcurrentHashMap
 同步容器类在执行每个操作期间都持有一个锁,在大量工作的HashMap.get或List.contains如果hashcode分布糟糕,就会导致大量同步集中访问某一处,
 导致其他线程不能访问该容器。
 ConcurrentHashMap 并不是将每个方法都在同一个锁上同步并使得每次只有一个线程访问容器,而是使用一种力度更细的加锁机制来实现更大程度的共享。
 这种机制成为分段锁,在这种机制中,任意数量的读取线程都可以并发地访问Map,执行读取操作地线程和执行写入操作地线程可以并发地访问Map,并且一定数量地写入
 襄城可以并发地修改Map。
 ConcurrentHashMap带来地结果是,在并发访问环境下将实现更高地吞吐量,而在单线程环境中只损失非常小地性能。
 分段锁机制:
 在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶有第(N%16)个锁来保护。
 假设散列函数具有合理的分布性,并且关键字能够实现均匀分布,那么这大约能把对于锁的请求减少到原来的1/16。
 正是这项技术使得ConcurrentHashMap能够支持多大16个并发的写入器。
  
 17.如何在两个线程间共享数据?
 多个线程访问共享对象和数据的方式
 1.如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
 2.如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
 3.将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,
 这样容易实现针对该数据进行的各个操作的互斥和通信。
 4.将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
 5.上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
 6.总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
 7.极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享。
 18.Java中notify 和 notifyAll有什么区别?
 wait导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或被其他线程中断。wait只能由持有对像锁的线程来调用。
 notify唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程(随机)。
 直到当前的线程放弃此对象上的锁,才能继续执行被唤醒的线程。
 同Wait方法一样,notify只能由持有对像锁的线程来调用.notifyall也一样,不同的是notifyall会唤配所有在此对象锁上等待的线程。
 "只能由持有对像锁的线程来调用"说明wait方法与notify方法必须在同步块内执行,即synchronized(obj)之内.
 再者synchronized代码块内没有锁是寸步不行的,所以线程要继续执行必须获得锁。相辅相成
  
 19.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
 因为这些是关于锁的,而锁是针对对象的,锁用于线程的同步应用,决定当前对象的锁的方法就应该在对象中
  
 20. Java中interrupted 和 isInterruptedd方法的区别?
 1.interrupt()是用来设置中断状态的。返回true说明中断状态被设置了而不是被清除了。我们调用sleep、wait等此类可中断
 (throw InterruptedException)方法时,一旦方法抛出InterruptedException,当前调用该方法的线程的中断状态就会被jvm自动清除了,
 就是说我们调用该线程的isInterrupted 方法时是返回false。如果你想保持中断状态,可以再次调用interrupt方法设置中断状态。
 这样做的原因是,java的中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位。
 interrupted是静态方法,返回的是当前线程的中断状态。例如,如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。
 然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false。如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。
  
 21.什么是线程池? 为什么要使用它?
 线程池,是指管理一组工作线程的资源池。线程池是与队列密切相关的,其中在工作队列中保存了所有等待执行的任务。可以从工作队列中获取一个任务,执行任务,
 返回线程池等待下一个任务。
 “在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。
 另外一个好处时,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。通过适当调整线程池的大小,可以创建
 足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程和相互竞争资源而使应用程序耗尽内存失败。
  
 22.如何避免死锁?
 一般造成死锁必须同时满足如下4个条件:
 1,互斥条件:线程使用的资源必须至少有一个是不能共享的;
 2,请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源;
 3,非剥夺条件:分配资源不能从相应的线程中被强制剥夺;
 4,循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程。
 因为要产生死锁,这4个条件必须同时满足,所以要防止死锁的话,只需要破坏其中一个条件即可。
  
 23.Java中活锁和死锁有什么区别?
 ⑴活锁是指当若干事务要对同一数据项加锁时,造成一些事务的永久等待,得不到控制权的现象
 ⑵死锁是指两个以上事务集合中的每一个事务都在等待加锁当前已被另一事物加锁的数据项,造成互相等待的现象。
 24.Java中synchronized 和 ReentrantLock 有什么不同?
 1、ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候
 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
 如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
 ReentrantLock获取锁定与三种方式:
 a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
 b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
 c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
 d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
 2、synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
 3、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
  
 25. Java中的ReadWriteLock是什么?
 对象的方法中一旦加入synchronized修饰,则任何时刻只能有一个线程访问synchronized修饰的方法。
 假设有个数据对象拥有写方法与读方法,多线程环境中要想保证数据的安全,需对该对象的读写方法都要加入 synchronized同步块。
 这样任何线程在写入时,其它线程无法读取与改变数据;如果有线程在读取时,其他线程也无法读取或写入。
 这种方式在写入操作远大于读操作时,问题不大,而当读取远远大于写入时,会造成性能瓶颈,因为此种情况下读取操作是可以同时进行的,
 而加锁操作限制了数据的并发读取。
 ReadWriteLock解决了这个问题,当写操作时,其他线程无法读取或写入数据,而当读操作时,其它线程无法写入数据,
 但却可以读取数据 。
  
 26.volatile 变量和 atomic 变量有什么不同?
 volatile变量
 在Java语言中,volatile变量提供了一种轻量级的同步机制,volatile变量用来确保将变量的更新操作通知到其它线程,
 volatile变量不会被缓存到寄存器或者对其它处理器不可见的地方,所以在读取volatile变量时总会返回最新写入的值,volatile变量通常用来表示某个状态标识。
 原子变量:
 原子变量是“更强大的volatile”变量,从实现来看,每个原子变量类的value属性都是一个volatile变量,
 所以volatile变量的特性原子变量也有。同时,原子变量提供读、改、写的原子操作,更强大,更符合一般并发场景的需求。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表