05.java多线程问题

2,666

目录介绍

  • 5.0.0.1 线程池具有什么优点和缺点?为什么说开启大量的线程,会降低程序的性能,那么该如何做才能降低性能?
  • 5.0.0.3 线程中start和run方法有什么区别?wait和sleep方法的不同?sleep() 、join()、yield()有什么区别?
  • 5.0.0.4 用Java手写一个会导致死锁的程序,遇到这种问题解决方案是什么?如何预防死锁的产生?
  • 5.0.0.5 ThreadLocal(线程变量副本)这个类的作用是什么?ThreadLocal为何要设计key存储当前的threadlocal对象?
  • 5.0.0.6 什么是线程安全?线程安全有那几个级别?保障线程安全有哪些手段?ReentrantLock和synchronized的区别?
  • 5.0.0.7 Volatile和Synchronized各自用途是什么?有哪些不同点?Synchronize在编译时如何实现锁机制?
  • 5.0.0.8 wait()和sleep()的区别?各自有哪些使用场景?怎么唤醒一个阻塞的线程?Thread.sleep(0)的作用是啥?
  • 5.0.0.9 同步和非同步、阻塞和非阻塞的概念?分别有哪些使用场景?说说你是如何理解他们之间的区别?
  • 5.0.1.0 线程的有哪些状态?请绘制该状态的流程图?讲一下线程的执行生命周期流程?线程如果出现了运行时异常会怎么样?
  • 5.0.1.1 synchronized锁什么?synchronized同步代码块还有同步方法本质上锁住的是谁?为什么?
  • 5.0.1.3 CAS是什么?CAS原理是什么?CAS实现原子操作会出现什么问题?对于多个共享变量CAS可以保证原子性吗?
  • 5.0.1.4 假如有n个网络线程,需要当n个网络线程完成之后,再去做数据处理,你会怎么解决这个问题?
  • 5.0.1.5 Runnable接口和Callable接口的区别?Callable中是如何处理线程异常的情况?如何监测runnable异常?
  • 5.0.1.6 如果提交任务时,线程池队列已满,这时会发生什么?线程调度算法是什么?
  • 5.0.1.7 什么是乐观锁和悲观锁?悲观锁机制存在哪些问题?乐观锁是如何实现冲突检测和数据更新?
  • 5.0.1.8 线程类的构造方法、静态块是被哪个线程调用的?同步方法和同步块,哪个是更好的选择?同步的范围越少越好吗?
  • 5.0.1.9 synchonized(this)和synchonized(object)区别?Synchronize作用于方法和静态方法区别?
  • 5.0.2.0 volatile是什么?volatile的用途是什么?线程在工作内存进行操作后何时会写到主内存中?
  • 5.0.2.1 被volatile修饰变量在多线程下如何获最新值?理解volatile的happens-before关系?多线程下执行volatile读写后的内存状态?
  • 5.0.2.2 Volatile实现原理?一个int变量,用volatile修饰,多线程去操作++,线程安全吗?那如何才能保证i++线程安全?
  • 5.0.2.3 在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?

好消息

  • 博客笔记大汇总【15年10月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计500篇[近100万字],将会陆续发表到网上,转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!所有博客将陆续开源到GitHub!

5.0.0.1 线程池具有什么优点和缺点?为什么说开启大量的线程,会降低程序的性能,那么该如何做才能降低性能?

  • 线程池好处:
    • 1)降低资源消耗;
    • 2)提高相应速度;
    • 3)提高线程的可管理性。技术博客大总结
  • 线程池的实现原理:
    • 当提交一个新任务到线程池时,判断核心线程池里的线程是否都在执行。如果不是,则创建一个新的线程执行任务。如果核心线程池的线程都在执行任务,则进入下个流程。
    • 判断工作队列是否已满。如果未满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
    • 判断线程池是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果满了,则交给饱和策略来处理这个任务。
  • 线程池是如何提高性能的?

5.0.0.3 线程中start和run方法有什么区别?wait和sleep方法的不同?sleep() 、join()、yield()有什么区别?

  • 线程中start和run方法有什么区别
    • 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?这是一个非常经典的java多线程面试问题。当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。
  • wait和sleep方法的不同
    • 最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
  • 1、sleep()方法
    • 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有Synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常
    • 比如有两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程才能执行;但当高优先级的线程sleep(5000)后,低优先级就有机会执行了。
    • 总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
  • 2、yield()方法技术博客大总结
    • yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
  • 3、join()方法
    • Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。
    • Thread t = new MyThread(); t.start(); t.join();保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
  • Thread的join()有什么作用?
    • Thread的join()的含义是等待该线程终止,即将挂起调用线程的执行,直到被调用的对象完成它的执行。比如存在两个线程t1和t2,下述代码表示先启动t1,直到t1的任务结束,才轮到t2启动。
    t1.start();
    t1.join(); 
    t2.start();
    

5.0.0.4 用Java手写一个会导致死锁的程序,遇到这种问题解决方案是什么?如何预防死锁的产生?

  • 死锁是怎么一回事
    • 线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。
  • 深入理解死锁的原理
    • 两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
    • 线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁
    • 线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的
  • 死锁的简单代码
    • 思路是创建两个字符串a和b,再创建两个线程A和B,让每个线程都用synchronized锁住字符串(A先锁a,再去锁b;B先锁b,再锁a),如果A锁住a,B锁住b,A就没办法锁住b,B也没办法锁住a,这时就陷入了死锁。
    • 打印结果:可以看到,Lock1获取obj1,Lock2获取obj2,但是它们都没有办法再获取另外一个obj,因为它们都在等待对方先释放锁,这时就是死锁。
    public class DeadLock {
        public static String obj1 = "obj1";
        public static String obj2 = "obj2";
        public static void main(String[] args){
            Thread a = new Thread(new Lock1());
            Thread b = new Thread(new Lock2());
            a.start();
            b.start();
        }    
    }
    class Lock1 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
                        synchronized(DeadLock.obj2){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    class Lock2 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock2 running");
                while(true){
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock2 lock obj2");
                        Thread.sleep(3000);
                        synchronized(DeadLock.obj1){
                            System.out.println("Lock2 lock obj1");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    
    • 如果我们只运行Lock1呢?修改一下main函数,把线程b注释掉。
  • 如何预防死锁的产生?
    • 死锁发生时的四个必要条件,只要破坏这四个必要条件中的任意一个条件,死锁就不会发生。这就为我们解决死锁问题提供了可能。一般地,解决死锁的方法分为死锁的预防,避免,检测[定位死锁的位置]与恢复三种(注意:死锁的检测与恢复是一个方法)。 锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
    • 打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
    • 打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
    • 打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:
      • 在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;
      • 资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
      • 降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。
    • 打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:
      • 限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
      • 为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

5.0.0.5 ThreadLocal(线程变量副本)这个类的作用是什么?ThreadLocal为何要设计key存储当前的threadlocal对象?

  • ThreadLocal即线程变量
    • ThreadLocal为每个线程维护一个本地变量。
    • 采用空间换时间,它用于线程间的数据隔离,它为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。ThreadLocal的实现是以ThreadLocal对象为键。任意对象为值得存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
  • ThreadLocal类是一个Map
    • ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。
    • ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。
    • Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
  • 更多详细参考博客:深入研究java.lang.ThreadLocal类

5.0.0.6 什么是线程安全?线程安全有那几个级别?保障线程安全有哪些手段?ReentrantLock和synchronized的区别?

  • 什么是线程安全
    • 线程安全就是当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
  • 线程安全也是有几个级别
    • 技术博客大总结
    • 不可变:
      • 像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
    • 绝对线程安全
      • 不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
    • 相对线程安全
      • 相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
    • 线程非安全技术博客大总结
      • ArrayList、LinkedList、HashMap等都是线程非安全的类.
  • 保障线程安全有哪些手段。保证线程安全可从多线程三特性出发:
    • 原子性(Atomicity):单个或多个操作是要么全部执行,要么都不执行
      • Lock:保证同时只有一个线程能拿到锁,并执行申请锁和释放锁的代码
      • synchronized:对线程加独占锁,被它修饰的类/方法/变量只允许一个线程访问
    • 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
      • volatile:保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;
      • synchronized:在释放锁之前会将工作内存新值更新到主存中
    • 有序性(Ordering):程序代码按照指令顺序执行
      • volatile: 本身就包含了禁止指令重排序的语义
      • synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入
  • ReentrantLock和synchronized的区别
    • ReentrantLock与synchronized的不同在于ReentrantLock:
      • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
      • 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。而synchronized是非公平的,即在锁被释放时,任何一个等待锁的线程都有机会获得锁。ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数改用公平锁。
      • 锁绑定多个条件:一个ReentrantLock对象可以通过多次调用newCondition()同时绑定多个Condition对象。而在synchronized中,锁对象wait()和notify()或notifyAl()只能实现一个隐含的条件,若要和多于一个的条件关联不得不额外地添加一个锁。
    • Synchronized是悲观锁机制,独占锁。而Locks.ReentrantLock是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
      • ReentrantLock适用场景
      • 某个线程在等待一个锁的控制权的这段时间需要中断
      • 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程,锁可以绑定多个条件。
      • 具有公平锁功能,每个到来的线程都将排队等候。
    • 更多详细参考博客:Lock与synchronized 的区别

5.0.0.7 Volatile和Synchronized各自用途是什么?有哪些不同点?Synchronize在编译时如何实现锁机制?

  • Volatile和Synchronized各自用途是什么?有哪些不同点?
    • 1 粒度不同,前者针对变量 ,后者锁对象和类
    • 2 syn阻塞,volatile线程不阻塞
    • 3 syn保证三大特性,volatile不保证原子性
    • 4 syn编译器优化,volatile不优化 volatile具备两种特性:
      • 1.保证此变量对所有线程的可见性,指一条线程修改了这个变量的值,新值对于其他线程来说是可见的,但并不是多线程安全的。
      • 2.禁止指令重排序优化。
    • Volatile如何保证内存可见性:
      • 1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
      • 2.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
    • 同步:就是一个任务的完成需要依赖另外一个任务,只有等待被依赖的任务完成后,依赖任务才能完成。
    • 异步:不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,只要自己任务完成了就算完成了,被依赖的任务是否完成会通知回来。(异步的特点就是通知)。 打电话和发短信来比喻同步和异步操作。
    • 阻塞:CPU停下来等一个慢的操作完成以后,才会接着完成其他的工作。
    • 非阻塞:非阻塞就是在这个慢的执行时,CPU去做其他工作,等这个慢的完成后,CPU才会接着完成后续的操作。
    • 非阻塞会造成线程切换增加,增加CPU的使用时间能不能补偿系统的切换成本需要考虑。
  • Synchronize在编译时如何实现锁机制?
    • Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

5.0.0.8 wait()和sleep()的区别?各自有哪些使用场景?怎么唤醒一个阻塞的线程?Thread.sleep(0)的作用是啥?

  • sleep来自Thread类,和wait来自Object类
    • 调用sleep()方法的过程中,线程不会释放对象锁。而调用wait方法线程会释放对象锁
    • sleep睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU
    • sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒
  • 通俗解释
    • Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。
  • 怎么唤醒一个阻塞的线程?
    • 如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
    • 技术博客大总结
  • Thread.sleep(0)的作用是啥?
    • 由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

5.0.0.9 同步和非同步、阻塞和非阻塞的概念?分别有哪些使用场景?说说你是如何理解他们之间的区别?

  • 同步和非同步
    • 同步和异步体现的是消息的通知机制:所谓同步,方法A调用方法B后必须等到方法B返回结果才能继续后面的操作;所谓异步,方法A调用方法B后可让方法B在调用结束后通过回调等方式通知方法A
  • 阻塞和非阻塞
    • 阻塞和非阻塞侧重于等待消息时的状态:所谓阻塞,就是在结果返回之前让当前线程挂起;所谓非阻塞,就是在等待时可做其他事情,通过轮询去询问是否已返回结果

5.0.1.0 线程的有哪些状态?请绘制该状态的流程图?讲一下线程的执行生命周期流程?线程如果出现了运行时异常会怎么样?

  • 在任意一个时间点,一个线程只能有且只有其中的一种状态
    • 新建(New):线程创建后尚未启动
    • 技术博客大总结
    • 运行(Runable):包括正在执行(Running)和等待着CPU为它分配执行时间(Ready)两种 无限期等待(Waiting):该线程不会被分配CPU执行时间,要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期等待状态:
    没有设置Timeout参数的Object.wait()
    没有设置Timeout参数的Thread.join()
    LockSupport.park()
    
    • 限期等待(Timed Waiting):该线程不会被分配CPU执行时间,但在一定时间后会被系统自动唤醒。以下方法会让线程进入限期等待状态:
    Thread.sleep()
    设置了Timeout参数的Object.wai()
    设置了Timeout参数的Thread.join()
    LockSupport.parkNanos()
    LockSupport.parkUntil()
    
    • 阻塞(Blocked):线程被阻塞。和等待状态不同的是,阻塞状态表示在等待获取到一个排他锁,在另外一个线程放弃这个锁的时候发生;而等待状态表示在等待一段时间或者唤醒动作的发生,在程序等待进入同步区域的时候发生。
    • 结束(Terminated):线程已经结束执行
  • 绘制该状态的流程图
  • 线程如果出现了运行时异常会怎么样?
    • 如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

5.0.1.1 synchronized锁什么?synchronized同步代码块还有同步方法本质上锁住的是谁?为什么?

  • synchronized锁什么
    • 对于普通同步方法,锁是当前实例对象;
    • 对于静态同步方法,锁是当前类的Class对象;
    • 对于同步方法块,锁是括号中配置的对象;
    • 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。synchronized用的锁是存在Java对象头里的MarkWord,通常是32bit或者64bit,其中最后2bit表示锁标志位。
  • 本质上锁住的是对象。
    • 在java虚拟机中,每个对象和类在逻辑上都和一个监视器相关联,synchronized本质上是对一个对象监视器的获取。当执行同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器,才能进入同步代码块或同步方法;而没有获取到的线程将会进入阻塞队列,直到成功获取对象监视器的线程执行结束并释放锁后,才会唤醒阻塞队列的线程,使其重新尝试对对象监视器的获取。

5.0.1.3 CAS原理是什么?CAS实现原子操作会出现什么问题?对于多个共享变量CAS可以保证原子性吗?

  • CAS原理是什么
    • CAS即compare and swap的缩写,中文翻译成比较并交换。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。自旋就是不断尝试CAS操作直到成功为止。
  • CAS实现原子操作会出现什么问题
    • ABA问题。因为CAS需要在操作之的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成,有变成A,那么使用CAS进行检查时会发现它的值没有发生变化,但实际上发生了变化。ABA问题可以通过添加版本号来解决。Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
    • 循环时间长开销大。pause指令优化。
    • 只能保证一个共享变量的原子操作。可以合并成一个对象进行CAS操作。

5.0.1.4 假如有n个网络线程,需要当n个网络线程完成之后,再去做数据处理,你会怎么解决?

  • 多线程同步的问题。这种情况可以可以使用thread.join();join方法会阻塞直到thread线程终止才返回。更复杂一点的情况也可以使用CountDownLatch,CountDownLatch的构造接收一个int参数作为计数器,每次调用countDown方法计数器减一。做数据处理的线程调用await方法阻塞直到计数器为0时。

5.0.1.5 Runnable接口和Callable接口的区别?Callable中是如何处理线程异常的情况?如何监测runnable异常?

  • Runnable接口和Callable接口的区别
    • Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
    • 这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

5.0.1.6 如果提交任务时,线程池队列已满,这时会发生什么?线程调度算法是什么?

  • 如果提交任务时,线程池队列已满,这时会发生什么?
    • 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
    • 技术博客大总结
    • 如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
  • 线程调度算法是什么?
    • 抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

5.0.1.7 什么是乐观锁和悲观锁?悲观锁机制存在哪些问题?乐观锁是如何实现冲突检测和数据更新?

  • 什么是乐观锁和悲观锁?
    • 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
    • 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,直接上了锁就操作资源。

5.0.1.8 线程类的构造方法、静态块是被哪个线程调用的?同步方法和同步块,哪个是更好的选择?同步的范围越少越好吗?

  • 线程类的构造方法、静态块是被哪个线程调用的?
    • 线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
  • 举个例子
    • 假设Thread2中new了Thread1,main函数中new了Thread2,那么:
      • Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
      • Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
  • 同步方法和同步块,哪个是更好的选择?
    • 同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
    • 技术博客大总结
  • 同步的范围越少越好吗?
    • 是的。虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。

5.0.1.9 synchonized(this)和synchonized(object)区别?Synchronize作用于方法和静态方法区别?

  • synchonized(this)和synchonized(object)区别技术博客大总结
    • 其实并没有很大的区别,synchonized(object)本身就包含synchonized(this)这种情况,使用的场景都是对一个代码块进行加锁,效率比直接在方法名上加synchonized高一些(下面分析),唯一的区别就是对象的不同。
    • 对synchronized(this)的一些理解
      • 一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
      • 二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
      • 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
      • 四、当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
  • Synchronize作用于方法和静态方法区别
    • 测试代码如下所示
    private void test() {
        final TestSynchronized test1 = new TestSynchronized();
        final TestSynchronized test2 = new TestSynchronized();
        Thread t1 = new Thread(new Runnable() {
    
            @Override
            public void run() {
                test1.method01("a");
                //test1.method02("a");
            }
        });
        Thread t2 = new Thread(new Runnable() {
    
            @Override
            public void run() {
                test2.method01("b");
                //test2.method02("a");
            }
        });
        t1.start();
        t2.start();
    }
    
    private static class TestSynchronized{
        private int num1;
        public synchronized void method01(String arg) {
            try {
                if("a".equals(arg)){
                    num1 = 100;
                    System.out.println("tag a set number over");
                    Thread.sleep(1000);
                }else{
                    num1 = 200;
                    System.out.println("tag b set number over");
                }
                System.out.println("tag = "+ arg + ";num ="+ num1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private static int  num2;
        public static synchronized void method02(String arg) {
            try {
                if("a".equals(arg)){
                    num2 = 100;
                    System.out.println("tag a set number over");
                    Thread.sleep(1000);
                }else{
                    num2 = 200;
                    System.out.println("tag b set number over");
                }
                System.out.println("tag = "+ arg + ";num ="+ num2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    //调用method01方法打印日志【普通方法】
    tag a set number over
    tag b set number over
    tag = b;num =200
    tag = a;num =100
    
    
    //调用method02方法打印日志【static静态方法】
    tag a set number over
    tag = a;num =100
    tag b set number over
    tag = b;num =200
    
    • 在static方法前加synchronized:静态方法属于类方法,它属于这个类,获取到的锁,是属于类的锁。
    • 在普通方法前加synchronized:非static方法获取到的锁,是属于当前对象的锁。 技术博客大总结
    • 结论:类锁和对象锁不同,synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

5.0.2.0 volatile是什么?volatile的用途是什么?线程在工作内存进行操作后何时会写到主内存中?

  • volatile是什么?
    • 轻量级锁。synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。
  • volatile的用途是什么?
    • 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
  • 线程在工作内存进行操作后何时会写到主内存中?
    • Java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。
    • 这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。
  • 被volatile修饰变量生成汇编代码有何特点?
    • 在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令
    • 这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:
      • 1.将当前处理器缓存行的数据写回系统内存;
      • 2.这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

5.0.2.1 被volatile修饰变量在多线程下如何获最新值?理解volatile的happens-before关系?多线程下执行volatile读写后的内存状态?

  • 被volatile修饰变量在多线程下如何获取最新值?
    • 如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
    • 因此,经过分析我们可以得出如下结论:
      • 1.Lock前缀的指令会引起处理器缓存写回内存;
      • 2.一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
      • 3.当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
    • 这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。即满足数据的“可见性”。
  • 如何理解volatile的happens-before关系?
  • 先来看两个核心之一:volatile的happens-before关系。
    • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。下面我们结合具体的代码,我们利用这条规则推导下:
    private void test3() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                new VolatileExample().writer();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                new VolatileExample().reader();
            }
        });
        thread1.start();
        thread2.start();
    }
    
    
    public class VolatileExample {
        private int a = 0;
        private volatile boolean flag = false;
        public void writer(){
            a = 1;          //1
            LogUtils.e("测试volatile数据1--"+a);
            flag = true;   //2
            LogUtils.e("测试volatile数据2--"+flag);
        }
        public void reader(){
            LogUtils.e("测试volatile数据3--"+flag);
            if(flag){      //3
                int i = a; //4
                LogUtils.e("测试volatile数据4--"+i);
            }
        }
    }
    
    • 打印日志如下所示
    //第一种情况
    2019-03-07 17:17:30.294 25764-25882/com.ycbjie.other E/TestFirstActivity: │ 测试volatile数据3--false
    2019-03-07 17:17:30.294 25764-25881/com.ycbjie.other E/TestFirstActivity: │ 测试volatile数据1--1
    2019-03-07 17:17:30.295 25764-25881/com.ycbjie.other E/TestFirstActivity: │ 测试volatile数据2--true
    
    //第二种情况
    2019-03-07 17:18:01.965 25764-25901/com.ycbjie.other E/TestFirstActivity: │ 测试volatile数据1--1
    2019-03-07 17:18:01.965 25764-25902/com.ycbjie.other E/TestFirstActivity: │ 测试volatile数据3--false
    2019-03-07 17:18:01.966 25764-25901/com.ycbjie.other E/TestFirstActivity: │ 测试volatile数据2--true
    
    • 上面的实例代码对应的happens-before关系如下图所示:
      • VolatileExample的happens-before关系推导
    • 分析上面代码执行过程
      • 加锁线程A先执行writer方法,然后线程B执行reader方法图中每一个箭头两个节点就代码一个happens-before关系,黑色的代表根据程序顺序规则推导出来,红色的是根据volatile变量的写happens-before 于任意后续对volatile变量的读,而蓝色的就是根据传递性规则推导出来的。
      • 这里的2 happen-before 3,同样根据happens-before规则定义:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序,我们可以知道操作2执行结果对操作3来说是可见的,也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知。
  • 多线程下执行volatile读写后的内存状态?
    • 还是按照两个核心的分析方式,分析完happens-before关系后我们现在就来进一步分析volatile的内存语义。还是以上面的代码为例,假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。
      • 线程A执行volatile写后的内存状态图
      • image
    • 当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。
      • 线程B读volatile后的内存状态图
      • image
    • 结果分析
      • 从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。既然是旧的了,那线程B该怎么办了?自然而然就只能去主内存去取啦。

5.0.2.2 Volatile实现原理?一个int变量,用volatile修饰,多线程去操作++,线程安全吗?那如何才能保证i++线程安全?

  • volatile的作用和原理
    • Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行。
    • volatile是轻量级的synchronized(volatile不会引起线程上下文的切换和调度),它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
    • 由于内存访问速度远不及CPU处理速度,为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后在进行操作,但操作完不知道何时会写到内存。普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。如果对声明了volatile的变量进行写操作,JVM就会想处理器发送一条Lock前缀的指令,表示将当前处理器缓存行的数据写回到系统内存。
  • 一个int变量,用volatile修饰,多线程去操作++,线程安全吗
    • 技术博客大总结
    • 不安全
    • 案例代码,至于打印结果就不展示呢
    • volatile只能保证可见性,并不能保证原子性。
    • i++实际上会被分成多步完成:
      • 1)获取i的值;
      • 2)执行i+1;
      • 3)将结果赋值给i。
    • volatile只能保证这3步不被重排序,多线程情况下,可能两个线程同时获取i,执行i+1,然后都赋值结果2,实际上应该进行两次+1操作。
    private volatile int a = 0;
    for (int x=0 ; x<=100 ; x++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                a++;
                Log.e("小杨逗比Thread-------------",""+a);
            }
        }).start();
    }
    
  • 如何才能保证i++线程安全
    • 可以使用java.util.concurrent.atomic包下的原子类,如AtomicInteger。其实现原理是采用CAS自旋操作更新值。
    for (int x=0 ; x<=100 ; x++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                AtomicInteger atomicInteger = new AtomicInteger(a++);
                int i = atomicInteger.get();
                Log.e("小杨逗比Thread-------------",""+i);
            }
        }).start();
    }
    

5.0.2.3 在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?

  • 原子性(Atomicity):一个操作要么都执行要么都不执行。
    • 可直接保证的原子性变量操作有:read、load、assign、use、store和write,因此可认为基本数据类型的访问读写是具备原子性的。
    • 若需要保证更大范围的原子性,可通过更高层次的字节码指令monitorenter和monitorexit来隐式地使用lock和unlock这两个操作,反映到Java代码中就是同步代码块synchronized关键字。
  • 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
    • 通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现。
    • 提供三个关键字保证可见性:volatile能保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;synchronized对一个变量执行unlock操作之前可以先把此变量同步回主内存中;被final修饰的字段在构造器中一旦初始化完成且构造器没有把this的引用传递出去,就可以在其他线程中就能看见final字段的值。
  • 有序性(Ordering):程序代码按照指令顺序执行。
    • 如果在本线程内观察,所有的操作都是有序的,指“线程内表现为串行的语义”;如果在一个线程中观察另一个线程,所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
    • 提供两个关键字保证有序性:volatile 本身就包含了禁止指令重排序的语义;synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。

其他介绍

01.关于博客汇总链接

02.关于我的博客