1.volatile关键字:
- 内存可见性问题是在编译器优化的背景下,一个线程把内存给改了,另外一个线程不能及时感知到。为了解决这种问题就引入了volatile。
- volatile的存在是为了保证内存可见性,但是并不保证原子性。针对一个线程读,一个线程修改,这个场景volatile合适,而针对俩个线程修改,volatile做不到!
- 当使用volatile来修饰变量时,编译器就不会做出只读寄存器不读内存这样的优化。这个关键字只能修饰变量,没有别的用法。
- volatile禁止了编译器优化,避免直接读取CPU寄存器中缓存的数据,而是每次都重新读内存。
- 面试中遇到volatile,多半也不会脱离JMM(Java Memory Model java内存模型);工作内存不是真的内存,主内存才是真的内存。
- 站在JMM的角度来看待volatile;正常程序执行的过程中会把内存的数据先加载到工作内存中,再进行计算处理。编译器优化可能会导致不是每次都真的读取主内存,而是直接读取工作内存中的缓存数据(这就可能导致内存可见性问题)。而volatile起到的作用就是保证每次读取数据都是从工作内存上重新读取。
2.wait和notify:
- 线程有个特别的地方,抢占式执行,线程调度的过程是随机的!而wait和notify是用来调配线程顺序的,让线程按照想要的顺序来调配,控制多线程之间的执行先后顺序。
- wait是Object的方法,线程执行到wait就会发生阻塞,直到另外一个线程调用notify把这个wait唤醒,才会继续往下走。wait只会影响调用的那个线程,不影响其他线程。
- wait操作本质上做了三件事:
1.释放当前锁
2.进行等待通知
3.满足一定条件的时候(别人调用了notify),
wait被唤醒然后尝试重新获取锁
- wait等待通知的前提是要把锁释放,而释放锁的前提是你得先加了锁。没锁怎么释放!因此wait的第一步操作就是先释放锁,保证其他线程能够正常往下运行,wait和加锁操作是密不可分的。
- 线程1没有释放锁的话,线程2就无法调用到notify;线程1调用了wait,在wait里面就自动释放锁了,这个时候虽然线程1阻塞在synchronized里面,但是此时锁是释放状态,线程2能拿到锁。其他线程必须要上锁才能调用notify,调用了notify才会唤醒wait,但是notify所在的线程也得先释放锁,wait才会在唤醒后的第一件事就是尝试重新加锁。
- 要保证加锁的对象和调用wait的对象是同一个对象,还要保证调用wait的对象和调用notify的对象是同一个对象。
Object object = new Object(); Thread t1 = new Thread(()->{ synchronized(object){ try { object.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); a.wait(); b.notify();//不能唤醒wait!
- 如果线程t1先执行了wait,线程t2后调用notify,此时notify会唤醒wait;但是如果线程t2先执行了notify,线程t1后调用wait,此时就错过了,特别注意即使没人调用wait,调用notify也不会有异常和副作用。
- 还有一个notifyAll。多个线程都在调用wait,notify是随机唤醒一个,而notifyAll则是全部唤醒,即使唤醒了所有的wait,这些wait就需要重新竞争锁,重新竞争锁的过程依然是串行的。
- wait和sleep的对比:
对比:
1.都是让线程进入阻塞等待的状态
2.sleep是通过时间来控制何时唤醒的
wait是由其他线程通过notify来唤醒的
3.wait有个重载的版本,参数可以传时间,表示等待的最大时间(类似于join)
3.单例模式:
- 单例模式和工厂模式是常见的设计模式。
- 单例模式本质上借助了编程语言自身的语法特性,强行限制某个类,不能创建多个实例,有且只有一个实例。
- static修饰的成员(属性)变成了类成员(类属性),此时当属性变成类属性的时候此时及已经是单个实例了。更具体地说是类对象的属性,而类对象是通过JVM加载.class文件来的,其实类对象在JVM中也是单例。换句话说,JVM针对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的static修饰的成员也就只有一份。
- 饿汉模式:
1. //饿汉模式 2. class Singletion{ 3. //这个instance就是Singletion的唯一实例 4. private static Singletion instance = new Singletion(); 5. 6. //在类外可以通过getInstance来获取到实例 7. public static Singletion getInstance(){ 8. return instance; 9. } 10. 11. //把构造方法设置为private。此时类外就无法继续调用new实例 12. private Singletion(){} 13. 14. } 15. 16. public class Demo8 { 17. public static void main(String[] args) { 18. //要继续使用这个实例 19. Singletion singletion = Singletion.getInstance(); 20. } 21. }
- 懒汉模式,比饿汉模式更加的高效:
1. //懒汉模式(bug版本) 2. class Singletion{ 3. 4. private static Singletion instance = null; 5. 6. public static Singletion getInstance(){ 7. if(instance == null){ 8. //这里才是创建实例, 9. // 首次调用getInatance才会触发,后续调用就立即返回 10. instance = new Singletion(); 11. } 12. return instance; 13. } 14. 15. private Singletion(){} 16. 17. } 18. 19. public class Demo8 { 20. public static void main(String[] args) { 21. Singletion singletion = Singletion.getInstance(); 22. } 23. }
- 上面写的懒汉和饿汉,谁的线程安全,谁的不安全?考虑这俩代码是否线程安全本质上是在考虑多个线程下同时调用getInstance,是否会有问题。饿汉模式中的getInstance只是单纯的读取数据的操作,不涉及修改,因此线程安全。而懒汉模式的getInstance既涉及到读又涉及到修改操作,则线程不安全。
- 如何修改让懒汉模式线程安全?加锁!,把多个操作打包成一个原子操作。
//解决方案 1
public static Singletion getInstance(){ synchronized (Singletion.class){ if(instance == null){ instance = new Singletion(); } } return instance; }
//分析:这种加锁方式把线程不安全的问题解决了,但是又有了新的问题
懒汉模式的线程不安全也只是实例创建之前(首轮调用的时候)才会触发线程不安全问题
一旦实例创建好后,线程就安全了。导致当后续线程安全的时候仍然还得加锁,加锁开销挺大,代价大!
//解决方案 2
public static Singletion getInstance(){ //实例创建之前,线程不安全,需要加 //实例创建之后,线程安全,不需要加 if(instance == null){//判断是否要加锁 synchronized (Singletion.class){ if(instance == null){//判断是否要创建实例 instance = new Singletion(); } } } return instance; }
- 理解双重if判定,当多线程首次调用getInstance的时候,这些线程发现instance都为空,进入了外层if并且开始往下执行竞争锁,竞争成功的锁再来完成实例创建的操作;当这个实例创建好了之后,其他竞争到锁的线程就被里层的if挡住了,便不会再创建实例了。当再有第二批线程想来,直接就被挡在了外层if,直接就return了。
- 还有一个重要的问题,假设俩个线程同时调用getInstance,第一个线程拿到了锁,进入第二层if,开始new对象。new操作本质上分成三个步骤,先是申请内存,得到内存首地址;再调用构造方法,来初始化实例;最后把内存的首地址赋值给instance引用。
- 这个场景下,编译器可能会进行指令重排序的优化操作,在单线程的角度下,步骤2和步骤3是可以调换顺序的(单线程的情况下,此时步骤2和步骤3谁先执行后执行效果都一样)。多线程情况下,假设此处触发了指令重排序,并且按照步骤1、3、2的顺序来执行,有可能线程t1执行了步骤1、3之后,执行步骤2之前,线程t2调用了getInstance,得到了不完全的对象,只是有内存,内存上的数据无效,这个getInstance就会认为instance非空,就直接返回了instance并且在后续可能就会针对instance进行解引用操作(使用里面的属性和方法),这就会出现异常!
- 这就是指令重排序带来的问题,要想解决这个问题,就是要禁止指令重排序,使用volatile,既能保证内存可见性,又能解决指令重排序的问题。
1. //懒汉模式(完整版) 2. class Singletion { 3. //加上volatile就禁止了指令重排序 4. private volatile static Singletion instance = null; 5. 6. public static Singletion getInstance() { 7. //实例创建之前,线程不安全,需要加 8. //实例创建之后,线程安全,不需要加 9. if (instance == null) {//判断是否要加锁 10. synchronized (Singletion.class) { 11. if (instance == null) {//判断是否要创建实例 12. instance = new Singletion(); 13. } 14. } 15. } 16. return instance; 17. } 18. 19. private Singletion(){} 20. }
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹