1.简介、线程安全性、对象的共享

一、QA

1、为什么要在计算机中加入操作系统?

  • 为了提升资源利用率:操作系统的出现使得计算机每次能运行多个程序,并且不同的程序都在单独的进程中执行。
  • 为了公平性:如果没有操作系统,那么就会变成一个程序从头运行到尾,然后再运行下一个程序。
  • 为了便利性:在计算多个任务时,应该编写多个程序,每个程序执行一个任务并在必要时相互通信,这比编写一个程序来计算所有任务更容易实现。

2、不同进程之间如何通信?

套接字、信号处理器、共享内存、信号量以及文件等。

3、为什么会出现线程?

进程的出现因素(资源利用率、公平性以及便利性等)同样也是促使线程出现的原因。线程运行在同一个进程中同时存在多个程序控制流。
线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器、栈以及局部变量等。

4、为什么要使用多线程,它的优势是什么?

  • 发挥多核处理器的强大能力:如果程序中只有一个线程,那么最多同时只能在一个处理器上运行。
  • 更高的吞吐率:使用多线程有助于在单处理器系统上获得更高的吞吐率。比如一个线程在等待IO操作完成,另一个线程可以继续执行,使程序能够在IO阻塞期间继续运行。
  • 建模简单:比如编写servlet的开发人员不需要了解有多少请求在同一时刻要被处理,也不需要套接字的IO是否被阻塞。这种方式可以简化组件的开发,并缩短掌握这种框架的学习时间。
  • 简化异步事件处理:如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其他请求的处理。

5、使用多线程有什么风险?

  • 线程安全问题:当多线程修改共享变量时,没有做同步处理。
  • 活跃性问题:死锁、饥饿以及活锁。
  • 性能问题:可能由于线程比较多,频繁的发生线程上下文的切换。也可能由于同步措施,抑制编译器的优化,使线程缓存区的数据无效,以及增加共享内存总线的同步流量。

6、怎样判断一个类是线程安全的?

  • 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
  • 无状态对象一定是线程安全的。

7、什么是竞态条件?

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。最常见的竞态条件类型就是“先检查后执行(check-then-act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。

8、多线程环境下读取变量时要不要同步?

当程序在没有同步的情况下读取变量时,可能会得到一个失效的值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证被称为最低安全性。最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但是对于非volatile类型的long和double变量,jvm允许将64位的读操作或写操作分解为两个32位操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值得高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享变量且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明他们,或者用锁保护起来。

9、volatile关键字的作用是什么?

  • 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
  • volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

10、什么是逸出?

当某个不该被发布的对象被发布时,这种情况就被称为逸出(Escape)。

  • 例如1:对象有一个Date类型的私有变量,然后该对象的getter方法返回该对象的引用,那么在对象的外部能够改变该对象的私有状态。
  • 例如2:在对象构造方法内将自己注册到其他监听器中,这可能导致在当前对象还未构造完毕时,就有其他线程方法该对象。

11、如何方式逸出?

  • 将其他对象要获取本对象的私有可变状态时,返回一个克隆后的状态。
  • 通过工厂方法将自己注册到监听器。

12、什么是不可变对象?

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的(没有逸出)。

13、被final修饰的域除了不能被修改还有什么作用?

java内存模型规定,final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无须同步。

14、怎样安全的发布一个对象?

  • 在静态初始化函数中初始化一个对象的引用。
  • 将对象的引用保存在volatile修饰的域或者AtomicReference对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

15、在并发程序中使用和共享对象时,有哪些方案可以保证线程安全?

  • 线程封闭:只有一个线程能够拥有和修改这个对象。
  • 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  • 线程安全共享:在对象内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
  • 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

16、线程封闭有哪些种方式?

  • Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。也就是说由开发人员保证只有一个线程使用该对象。
  • 栈封闭:比如在方法内部保存使用变量。因为线程栈是私有的,其他线程访问不到。
  • ThreadLocal:ThreadLocal这个类的实例相当于一个容器,每个线程只能从这个容器中获取属于自己在ThreadLocal中存放的值。

二、其他

  • 当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变形规范都能起到一定的帮助作用。(比如对共享变量的访问入口都在该共享变量所在对象的内部,由少数的入口控制对变量的访问)

  • 完全由线程安全的类构成的程序并不一定就是线程安全的,而在线程安全类中也可以包含非线程安全的类。

  • 在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。

  • 无状态对象一定是线程安全的。

  • 在实际情况中,应尽可能地使用现有的线程安全对象(例如AtomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。

  • 在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。(保证数据一致性)

  • 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

  • 每个共享的可变变量都应该只由一个锁保护,从而使维护人员知道是哪一个锁。

  • 一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。

  • 将每个方法都作为同步方法还可能导致活跃性问题和性能问题。

  • 当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁。

  • 在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。

  • 失效值可能不会同时出现:一个线程可能获取到某个变量的最新值,而获得另一个变量的失效值。

  • 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

  • 对于服务器应用程序,无论在开发阶段还是在测试阶段,当启动jvm时一定要指定-server命令行选项。server模式下jvm将比client模式的jvm进行更多的优化,例如将循环中未被修改的变量提升到循环内部,因此在开发环境(client模式的jvm)中能正确运行的代码,可能在部署环境(server模式的jvm)中运行失败。

  • 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可进行。

  • 不可变对象一定是线程安全的。

  • 静态初始化由jvm在类的初始化阶段执行,由于jvm内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

猜你喜欢

转载自blog.csdn.net/weixin_36899865/article/details/83540761