精华内容
下载资源
问答
  • 文章目录多线程部分谈谈你对volatile的理解你谈谈JMM什么是指令重排,并举几个案例你在哪些地方用到过volatile?如果不使用volatile双重检测机制会有什么问题volatile无法保证原子性,应该使用什么保证原子性...



    谈谈你对volatile的理解

      volatile是Java虚拟机提供的轻量级的同步机制。特点如下:

    1. 保证可见性
    2. 不保证原子性
    3. 禁止指令重排


    你谈谈JMM

      JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

      由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间,最大的工作内存就是main线程的),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
    在这里插入图片描述

    JMM关于同步 的规定:

    1. 线程解锁前,必须把共享变量的值刷新回主内存。
    2. 线程加锁前,必须读取主内存的最新值到自己的工作内存。
    3. 加锁解锁是同一把锁。

    JMM的特性:

    1. 原子性:指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰;比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A 给他赋值 1,线程 B 给它赋值为 -1,。那么不管这两个线程以何种方式,何种步调工作,i的值要么是1,要么是-1,线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断

    2. 可见性:指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题 是不存在。因为你在任何一个操作步骤中修改某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。但是这个问题在并行程序中就不见得了。如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。

    3. 有序性:对于一个线程的执行代码而言,我们总是习惯地认为代码的执行时从先往后,依次执行的。这样的理解也不能说完全错误,因为就一个线程而言,确实会这样。但是在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会先执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。


    什么是指令重排,并举几个案例

      指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。即代码写的顺序和代码的执行顺序可能不一样。在保证数据依赖性的情况下看可以写在后面的代码可能会先执行。

    在这里插入图片描述
    在这里插入图片描述

    案例:

    public void mySort(){
        int x=11;//语句1
        int y=12;//语句2
        x=x+5;//语句3
        y=x*x;//语句4
    }
    

    代码正常的执行顺序是:1234
    但是经过指令重排代码的执行顺序可能变为:
    2134
    1324

    问题:
    请问语句4 可以重排后变成第一条码?
    存在数据的依赖性 没办法排到第一个



    你在哪些地方用到过volatile?

    单例模式的双重检测机制

    public class SingletonDemo {
    
        private static volatile SingletonDemo instance=null;
        private SingletonDemo(){
            System.out.println(Thread.currentThread().getName()+"\t 构造方法");
        }
    
        /**
         * 双重检测机制
         * @return
         */
        public static SingletonDemo getInstance(){
            if(instance==null){
                synchronized (SingletonDemo.class){
                    if(instance==null){
                        instance=new SingletonDemo();
                    }
                }
            }
            return instance;
        }
    
        public static void main(String[] args) {
            for (int i = 1; i <=10; i++) {
                new Thread(() ->{
                    SingletonDemo.getInstance();
                },String.valueOf(i)).start();
            }
        }
    }
    


    如果不使用volatile双重检测机制会有什么问题

      如果不使用volatile,DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排

    原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
    instance=new SingletonDem();可以分为以下步骤(伪代码)

    memory=allocate();//1.分配对象内存空间
    instance(memory);//2.初始化对象
    instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 
    

    步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.

    memory=allocate();//1.分配对象内存空间
    instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完。
    instance(memory);//2.初始化对象
    

    但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
    所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题。



    volatile无法保证原子性,应该使用什么保证原子性

    使用原子应用类AtomicReference或者使用原子包装类,例如:AtomicInteger

    AtomicReference使用的演示:

    /**
     * Description:
     *
     * @author veliger@163.com
     * @date 2019-04-12 21:23
     **/
    @Getter@Setter@AllArgsConstructor@ToString
    class User{
        private String name;
        private int age;
    }
    public class AtomicReferenceDemo {
        public static void main(String[] args) {
            User zs = new User("zs", 22);
            User ls = new User("ls", 22);
            AtomicReference<User> userAtomicReference = new AtomicReference<>();
            userAtomicReference.set(zs);
            System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get().toString());
            System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get().toString());
        }
    }
    


    Atomiclnteger是怎么保证原子性

    Atomiclnteger使用了CAS算法和UnSafe保证原子性



    谈一谈CAS和UnSafe

    在这里插入图片描述
    在这里插入图片描述

    1. UnSafe是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个工具,基于该类可以直接操作特额定的内存数据。UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UnSafe类的方法。
      注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
    2. 变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。
    3. 变量value和volatile修饰,保证了多线程之间的可见性。


    详细说一下CAS

    CAS(Compare And Swap) 比较并交换
    比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作
    否则继续比较直到主内存和工作内存中的值一致为止

    CAS应用
    CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
    当且仅当预期值A和内存值V相同时,将内存值V修改为B,重新读取内存中的值,在进行修改,知道预期值和内存中的值相等时将内存值V修改为B。

    用UnSafe中的getAndAddInt进行举例:
    在这里插入图片描述

    假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):

    1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

    2. 线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起。

    3. 线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK。

    4. 这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了。

    5. 线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功。

    CAS优点就是在不加锁的情况下保证原子性



    CAS有没有缺点,是什么

    1. 循环时间长开销大
      在这里插入图片描述

    2. 只能保证一个共享变量的原子性
      当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

    3. 会导致ABA问题



    什么是ABA问题,怎么解决

    在这里插入图片描述

    解决ABA问题可以使用带时间戳(版本号)的原子应用类AtomicStampedReference

    原理就是在每次主内存数据的时候设置一个版本号,当线程要写回数据的时候不仅要判断期望值是否相同,也要判断期望的版本号是否相同。

    代码演示如下:

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * Description: ABA问题的解决
     *
     * @author veliger@163.com
     * @date 2019-04-12 21:30
     **/
    public class ABADemo {
    
        private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
        private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
    
        public static void main(String[] args) {
            
            System.out.println("===以下是ABA问题的产生===");
    
            new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 100);
            }, "t1").start();
    
            new Thread(() -> {
                //先暂停1秒 保证完成ABA
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
            }, "t2").start();
            
    
            //暂停两秒钟,保证上面的线程执行完了
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
    
            System.out.println("===以下是ABA问题的解决===");
    
    
            new Thread(() -> {
                int stamp = stampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
                //暂停1秒钟t3线程,保证t4能拿到一样的初始值
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t 第2次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
                stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t 第3次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
            }, "t3").start();
    
            new Thread(() -> {
                int stamp = stampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
                //保证线程3完成1次ABA
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //第三个参数是期望的版本号,如果与stampedReference.getStamp()当前版本号相同才会执行
                boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + "\t 修改成功否" + result + "\t最新版本号" + stampedReference.getStamp());
                System.out.println("最新的值\t" + stampedReference.getReference());
            }, "t4").start();
        }
    
    }
    


    什么是公平锁和非公平锁

    公平锁

    • 是指多个线程按照申请锁的顺序来获取锁类似队列 先进先出

    非公平锁

    • 是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象

      并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 默认是非公平锁

    两者区别

    公平锁:
      就是很公平,在并发环境中,每个线程在获取锁时会先査看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第个,就占有锁,否则就会加入到等待队列中,以后会按照FFO的规则从队列中取到自己。

    非公平锁:
      非公平锁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

    ReentrantLock而言,通过构造哈数指定该锁是否是公平锁 默认是非公平锁
    非公平锁的优点在于吞吐量比公平锁大。

    对于synchronized而言 也是一种非公平锁



    说一说可重入锁

    在这里插入图片描述

    注意内层和外出必须是同一把锁,否则不会自动获取。

    ReentrantLocksynchronized就是一个典型的可重入锁。

    可重入锁最大的作用就是避免死锁

    验证synchronized 是可重入锁:

    
    class Phone{
        public synchronized void sendSms() throws Exception{
            System.out.println(Thread.currentThread().getName()+"\tsendSms");
            sendEmail();
        }
        public  synchronized void sendEmail() throws Exception{
            // TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName()+"\tsendEmail");
        }
    
    }
    
    public class ReenterLockDemo {
    
        public static void main(String[] args) {
            Phone phone = new Phone();
            new Thread(()->{
                try {
                    phone.sendSms();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },"t1").start();
            new Thread(()->{
                try {
                    phone.sendSms();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },"t2").start();
        }
    }
    

    运行结果:

    t1	sendSms
    t1	sendEmail
    t2	sendSms
    t2	sendEmail
    

    验证Lock是可重入锁:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class Phone2 implements Runnable {
        private Lock lock = new ReentrantLock();
        private Lock lock1 = new ReentrantLock();
    
        @Override
        public void run() {
            get();
        }
    
        private void get() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\tget");
                set();
            } finally {
                lock.unlock();
            }
        }
    
        private void set() {
            lock1.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\tset");
            } finally {
                lock1.unlock();
            }
        }
    }
    
    public class ReenterLockDemo2 {
        public static void main(String[] args) {
            Phone2 phone = new Phone2();
            Thread t3 = new Thread(phone);
            Thread t4 = new Thread(phone);
            t3.start();
            t4.start();
    
        }
    }
    


    什么是自旋锁

    在这里插入图片描述
    CAS算法就是自旋锁的思想。



    什么是读写锁(独占锁和共享锁)

    在这里插入图片描述

    代码演示:

    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 资源类
     */
    class MyCaChe {
        /**
         * 保证可见性
         */
        private volatile Map<String, Object> map = new HashMap<>();
        private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    
        /**
         * 写
         *
         * @param key
         * @param value
         */
        public void put(String key, Object value) {
            reentrantReadWriteLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);
                //模拟网络延时
                try {
                    TimeUnit.MICROSECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.put(key, value);
                System.out.println(Thread.currentThread().getName() + "\t正在完成");
            } finally {
                reentrantReadWriteLock.writeLock().unlock();
            }
        }
    
        /**
         * 读
         *
         * @param key
         */
        public void get(String key) {
            reentrantReadWriteLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t正在读取");
                //模拟网络延时
                try {
                    TimeUnit.MICROSECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Object result = map.get(key);
                System.out.println(Thread.currentThread().getName() + "\t正在完成" + result);
            } finally {
                reentrantReadWriteLock.readLock().unlock();
            }
        }
    
        public void clearCaChe() {
            map.clear();
        }
    
    }
    
    /**
     * Description:
     * 多个线程同时操作 一个资源类没有任何问题 所以为了满足并发量
     * 读取共享资源应该可以同时进行
     * 但是
     * 如果有一个线程想去写共享资源来  就不应该有其他线程可以对资源进行读或写
     * <p>
     * 小总结:
     * 读 读能共存
     * 读 写不能共存
     * 写 写不能共存
     * 写操作 原子+独占 整个过程必须是一个完成的统一整体 中间不允许被分割 被打断
     *
     * @author veliger@163.com
     * @date 2019-04-13 0:45
     **/
    public class ReadWriteLockDemo {
        public static void main(String[] args) {
            MyCaChe myCaChe = new MyCaChe();
            for (int i = 1; i <= 5; i++) {
                final int temp = i;
                new Thread(() -> {
                    myCaChe.put(temp + "", temp);
                }, String.valueOf(i)).start();
            }
            for (int i = 1; i <= 5; i++) {
                int finalI = i;
                new Thread(() -> {
                    myCaChe.get(finalI + "");
                }, String.valueOf(i)).start();
            }
        }
    }
    

    运行结果:

    2	正在写入2
    2	正在完成
    5	正在写入5
    5	正在完成
    3	正在写入3
    3	正在完成
    4	正在写入4
    4	正在完成
    1	正在写入1
    1	正在完成
    2	正在读取
    3	正在读取
    5	正在读取
    4	正在读取
    1	正在读取
    1	正在完成1
    5	正在完成5
    2	正在完成2
    4	正在完成4
    3	正在完成3
    


    synchronized和Look的区别

    1. 原始构成
      synchronized是关键字属于JVM层面
      monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或方法中才能调wait/notify等方法
      Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁

    2. 使用方法
      synchronized 不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。需要Lock( )利unlock()方法配合try/finally语句块来完成。

    3. 等待是否可中断
      synchronized不可中断,除非抛出异常或者正常运行完成
      ReentrantLock可中断
      设置超时方法 tryLock(Long timeout,TimeUnit unit)
      lockInterruptibly()放代码块中,调用interrupt()方法可中断

    4. 加锁是否公平
      synchronized非公平锁
      ReentrantLock两者都可以,默认非公平锁,构造方法可以传入booLean值,true为公平锁,false为非公平锁

    5. 锁绑定多个条件condition
      synchronized没有
      ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。



    死锁的分析和排查

    在这里插入图片描述
    产生死锁的主要原因:

    1. 系统资源不足
    2. 进程运行推进的顺序不合适
    3. 资源分配不当

    代码演示:

    import java.util.concurrent.TimeUnit;
    
    class HoldThread implements Runnable {
    
        private String lockA;
        private String lockB;
    
        public HoldThread(String lockA, String lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }
    
        @Override
        public void run() {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockA + "尝试获得" + lockB);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockB + "尝试获得" + lockA);
                }
            }
        }
    }
    
    /**
     * Description:
     * 死锁是指两个或者以上的进程在执行过程中,
     * 因争夺资源而造成的一种相互等待的现象,
     * 若无外力干涉那他们都将无法推进下去
     *
     * @author veliger@163.com
     * @date 2019-04-14 0:05
     **/
    public class DeadLockDemo {
        public static void main(String[] args) {
            String lockA = "lockA";
            String lockB = "lockB";
            new Thread(new HoldThread(lockA, lockB), "threadAAA").start();
            new Thread(new HoldThread(lockB, lockA), "threadBBB").start();
        }
    }
    

    jps命令定位进程编号:
    在这里插入图片描述

    jstack找到死锁查看:
    在这里插入图片描述



    synchronized锁升级的过程

    一开始是无锁状态,当有线程使用的时候会升级成偏向锁,这时候是单线程状态,一旦有第二个线程竞争锁,将会升级为轻量级锁,其余线程会自旋等待,当自旋到一定次数,或者竞争非常激烈的情况下会升级为重量级锁,这时其余线程进入等待队列,等待调用。

    (注意:jdk 1.6以前synchronized 关键字只表示重量级锁,1.6之后区分为偏向锁、轻量级锁、重量级锁。)

    锁升级的方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
    在这里插入图片描述

    1. 偏向锁
      偏向锁是JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

    2. 轻量级锁
      如果明显存在其它线程申请锁,那么偏向锁将很快升级为轻量级锁。

    3. 自旋锁
      自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

    4. 重量级锁
      指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

    守护线程和用户线程有什么区别呢?
    守护线程和用户线程

    • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。

    • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随JVM 一起结束工作,main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

    比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

    注意事项:

    1. setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常,不能将已经启动的线程设置为守护线程

    2. 在守护线程中产生的新线程也是守护线程

    3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑

    4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行

    在这里插入图片描述

    展开全文
  • java多线程面试题总结

    2020-05-02 00:00:53
    文章目录1,线程和进程的区别2,创建线程的几种方式3,线程的执行的几种状态4,同步代码块和...进程是应用程序,线程是进程中的一个执行序列,一个进程可以有线程。像java的jvm就是一个进程,里面的thread就是一...

    文章目录

    1,线程和进程的区别

    进程是应用程序,线程是进程中的一个执行序列,一个进程可以有多个线程。像java的jvm就是一个进程,里面的thread就是一个个线程。

    2,创建线程的几种方式

    继承Thread类,实现Runnable接口,用Executor创建线程池
    Runnable相对更受欢迎一些,因为其实接口,类可以实现多个接口。但只能继承一个。

    3,线程的执行的几种状态

    1,新建(new),创建一个线程对象
    2,可运行状态(runnable):调用start()方法,但不会立即执行,而是在可运行线程池中等待调度运行。
    3,运行(running):线程获得了cup的时间片,开始执行代码
    4,阻塞(block):线程因某种原因放弃了执行权,让出cpu资源,停止运行。
    阻塞又可细分为以下几种:
    等待阻塞,线程执行了wait()方法,进入到了等待队列。
    同步阻塞,如果线程执行中遇到有同步锁被别的线程持有,则jvm会把该线程放到锁池中。
    其他阻塞,线程遇到sleep或join方法,jvm也会把线程置为阻塞状态。
    当阻塞结束,就由进入2,可运行状态
    5,死亡(dead):线程run(),main()方法执行结束或有异常退出了,则该线程死亡,生命周期结束。

    4,同步代码块和同步方法的区别

    前者的颗粒度更细一点。

    5,监视器内部是如何线程同步的?程序应该做哪种级别的同步?

    代码块级别的同步,监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。

    6,什么是死锁(deadlock)?

    举例说,有两个线程,两个对象,第一个线程先给第一个对象加锁,然后在同步代码块里面又拿第二个对象加锁。第二个对象先给第二个对象加锁,再在同步代码块中给第一个对象加锁。然后运行两个线程,就会出现两个线程互相等待对方释放锁,导致程序进行不下去。
    案例如下:

    package test;
    public class DeadLock {
    	private static Object lock1=new Object();
    	private static Object lock2=new Object();
    	
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		new Thread(){
    			public void run(){
    				synchronized (lock1) {
    					System.out.println("thread1 get lock1;");
    					try{
    						Thread.sleep(100);
    					}catch (Exception e) {
    						// TODO: handle exception
    					}
    					synchronized (lock2) {
    						System.out.println("thread1 get lock2;");
    					}
    				}
    				System.out.println("thread1 end...");
    			}
    		}.start();
    		
    		new Thread(){
    			public void run(){
    				synchronized (lock2) {
    					System.out.println("thread2 get lock2;");
    					try{
    						Thread.sleep(100);
    					}catch (Exception e) {
    						// TODO: handle exception
    					}
    					synchronized (lock1) {
    						System.out.println("thread2 get lock1;");
    					}
    					
    				}
    				System.out.println("thread2 end");
    			}
    		}.start();
    	}
    
    }
    

    7,如何保证N个线程可以访问N个资源同时又不导致死锁?

    指定获取锁的顺序,

    8 start() 和run()方法的区别

    只有通过start调用run方法,才会表现出多线程的特性。如果直接调run方法,还是单线程,同步的。

    9 Runnable接口和callable接口的区别,及callable获取结果的方式,futureTask

    前者接口中的run方法的返回值是void,后者的call方法是由返回值的,且是个泛型,通过和future和futureTask配合可以获取异步执行结果。非常有用的。实例如下:

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    import java.util.concurrent.FutureTask;
    
    public class TestThread {
    
    	public static void main(String[] args) throws Exception {
    		// TODO Auto-generated method stub
    		int taskSize=100;
    		//创建一个线程池
    		ExecutorService pool=Executors.newFixedThreadPool(5);
    		//创建多个有返回值的任务
    		for(int i=0;i<taskSize;i++){
    			Callable callable=new MyCallable(i);
    			//第一种方式   
    			FutureTask<String> f=new FutureTask<>(callable);
    			//FutureTask实现的RunnableFuture,所以用start方法执行call方法
    			new Thread(f).start();
    			//第二种方式,交个线程池管理,submit的返回类型是future
    			Future f2=pool.submit(callable);
    			System.out.println(f.get());
    			System.out.println(f2.get());
    		}
    	}
    
    }
    
    class MyCallable implements Callable<String>{
    	private int taskNum;
    	public MyCallable(int taskNum) {
    		// TODO Auto-generated constructor stub
    		this.taskNum=taskNum;
    	}
    	@Override
    	public String call() throws Exception {
    		// TODO Auto-generated method stub
    		System.out.println(taskNum+" 任务启动");
    		
    		return "第"+taskNum+"线程执行。";
    	}
    	
    }
    

    10,cyclicBarrier和countdownlatch的区别

    二者都在java.util.concurrent下,都可以用来表示代码运行的某一点。
    前者是某个线程运行到某个点后立即停止,直达所有线程都到达这个点所有线程才重新运行,而主线程则不用管,可能早早就结束了。
    后者是所有线程启动后,countdownlatch的会记录线程的总个数,某个运行到某个点后会给countdownlatch个数值减一,该线程还继续运行。等countdownlatch记录的个数值为0的时候,也就是所有线程都要运行完了,再运行主线程。比如LOL等待所有玩家加载完了才进入到游戏一样。
    案例:
    countdownlatch:

    import java.util.concurrent.CountDownLatch;
    
    public class CountDownlatchTest {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(5);
            for(int i=0;i<5;i++){
                new Thread(new readNum(i,countDownLatch)).start();
            }
            countDownLatch.await();
            System.out.println("线程执行结束。。。。");
        }
     
        static class readNum  implements Runnable{
            private int id;
            private CountDownLatch latch;
            public readNum(int id,CountDownLatch latch){
                this.id = id;
                this.latch = latch;
            }
            @Override
            public void run() {
                synchronized (this){
                    System.out.println("id:"+id);
                    latch.countDown();
                    System.out.println(latch.getCount());//4,3,2,1,0  有几个线程就有数值就是多少
                    System.out.println("线程组任务"+id+"结束,其他任务继续");
                }
            }
        }
    }
    
    
    id:0
    4
    线程组任务0结束,其他任务继续
    id:2
    3
    线程组任务2结束,其他任务继续
    id:3
    2
    线程组任务3结束,其他任务继续
    id:1
    1
    线程组任务1结束,其他任务继续
    id:4
    0
    线程组任务4结束,其他任务继续
    线程执行结束。。。。
    
    import java.util.Random;
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class CyclicBarrierTest {
        public static void main(String[] args) {
            CyclicBarrier barrier = new CyclicBarrier(3);
            for(int i = 0; i < barrier.getParties(); i++){
                new Thread(new MyRunnable(barrier), "队友"+i).start();
            }
            System.out.println("main function is finished.");
        }
    
    
        private static class MyRunnable implements Runnable{
            private CyclicBarrier barrier;
    
            public MyRunnable(CyclicBarrier barrier){
                this.barrier = barrier;
            }
    
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    try {
                        Random rand = new Random();
                        int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                        Thread.sleep(randomNum);
                        System.out.println(Thread.currentThread().getName() + ", 通过了第"+i+"个障碍物, 使用了 "+((double)randomNum/1000)+"s");
                        this.barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    
    main function is finished.
    队友1, 通过了第0个障碍物, 使用了 1.151s
    队友2, 通过了第0个障碍物, 使用了 1.458s
    队友0, 通过了第0个障碍物, 使用了 1.993s
    队友2, 通过了第1个障碍物, 使用了 1.27s
    队友1, 通过了第1个障碍物, 使用了 1.995s
    队友0, 通过了第1个障碍物, 使用了 2.564s
    队友1, 通过了第2个障碍物, 使用了 1.833s
    队友2, 通过了第2个障碍物, 使用了 2.125s
    队友0, 通过了第2个障碍物, 使用了 2.911s
    
    

    11 java 内存模型,线程安全问题和volatile 关键字的作用

    1,java 内存模型将内存分为了主内存和工作内存。主内存里面放的是共享变量,工作内存是也就是cpu,cpu的处理速度很快,一个线程获取了cpu时间片,会在这段时间内单独占有这块cpu。另外在工作内存和主内存之间还有个cpu缓存,用来copy共享变量到工作内存中,工作内存用完后再更新到主内存中。再者当前的机器都是多核cpu了,也就是可以同时处理多个线程,那么在处理共享变量时就会带来更为明显的线程安全问题。
    2,线程安全问题,当线程A对共享变量A=1 做加1操作时,是先把共享变量拷贝一份到工作内存中,然后加1,再去更新主内存中的功效变量。如果在这期间有其他线程也做了同样的加1操作,而A不知道,那么结果就造成了本来A应该=3,但是想现在变成2.这就是线程安全问题。
    解决办法之一可以加volatile关键字。
    2.使用volatile关键字修饰的变量保证了多线程间的可见性和有序性,就是每次读取到的volatile变量一定是最新的。
    至于volatile工作原理是启用cpu总线嗅探机制,就是监听主内存中的共享变量在工作内存执行期间是否发生了改变,如果有就重新读取,重新计算,再更新到主内存中。

    11 java 原子操作

    1,read:读取 ,从主内存读取数据
    2,load:载入,将主内存数据读取到的数据写入到工作内存
    3,use:使用,对从主内存读取的数据做计算
    4,assign:赋值,将计算好的数据重新赋值到工作内存中
    5,store:存储,将工作内存数据写入到主内存
    6,write:写入,将存储的数据变量赋值给主内存的变量
    7,lock:锁定,将主内存变量加锁,标识为线程独占状态
    8,unlock:解锁,将主内存变量解锁,解锁后其他线程可以锁定该变量
    上面这些步骤没有先后顺序,对于某些操作也是要根据我们的代码决定是否要执行的。
    一般来说1-5步是顺序进行的,但是如果使用volatile,监听到值发生了改变,就可能中途又要重新冲1,读取再次执行。
    对于加锁解锁,要看在不在同步代码块中了。

    11 volatile为何不保证原子性?

    首先解释原子性,就是一个操作或多个操作要么全部执行,不因任何元素而中断,要么全都不执行。
    下面举个反例,就是10个线程操作一个共性变量,各自加1000次,但最后结果不一定是10000,因为虽然使用了volatile可见性让cpu总线嗅探机制得知了有其他线程修改了主内存数据,于是这个线程要重新读取主内存数据做计算,但是问题是,这个线程已经循环了n次,这点是不同于数据库先给回滚再重新计算。而是在已经循环n基础上解释计算,这就导致这个线程最后的计算结果是1000-n.那么总的数值肯定也不是10000了。

    public class VolatileAtomicTest {
    	
    	public static volatile int num=0;
    	public static void increase(){
    		num++;
    	}
    	public static void main(String[] args) throws InterruptedException {
    		Thread[] threads=new Thread[10];
    		for(int i=0;i<threads.length;i++){
    			threads[i]=new Thread(new Runnable() {
    				@Override
    				public void run() {
    					for(int i=0;i<1000;i++){
    						increase();
    //						num++;
    					}
    				}
    			});
    			threads[i].start();
    		}
    		for(Thread t:threads){
    			t.join();
    		}
    		System.out.println(num);
    	}
    
    }
    
    

    结果也有可能是10000,多跑几次就发现问题了。
    解决办法是给increase方法加synchronize。

    11 java并行编程的三大特性,可见性,原子性,有序性

    1,可见性就是保持读取的共享变量数据是最新的
    2,原子性就是一个或多个操作要么全部执行,不因任何操作而中断。要么全部不执行,这个数据库的原子性是一样的
    3,有序性就是java代码的执行顺序是按照代码依次进行的。

    12 什么是线程安全

    即如果代码在多线程下执行和在单线程下执行的结果是一样的,那我们写的多线程代码就是线程安全的。

    13 线程安全的级别

    1 不可变,像String Ineger Long这些都是final修饰的类,任何线程都改变不了他们的值,可以直接在多线程下使用。其实就是参数传递时值传递的原因
    2绝对线程安全 就是不用做类似加锁之类的操作就是线程安全的类,java中有CopyOnWriteArrayList,copyOnwriteArraySet
    3 相对线程安全 就是我们通常意义上说的线程安全,像Vector,他的add remove方法都是原子操作,不会被打断,而且我们也常说vector是线程安全,但是实际上如果一个线程在遍历Vector,另一个线程同时做add操作,很大情况下会报ConcurrentModificationException。也就是fail-fast机制。
    4 线程非安全,就是我们常说的Arraylist,Linkedlist,HashmAp了,也就是没给这些集合加锁喽。

    14 java 如何获取线程dump文件

    线程dump就是线程堆栈,能够获取jvm此时的堆栈信息。通常用于系统出现内存不够,反应迟钝的情况。
    linux下的获取步骤是:1 ps -ef|grep java 获取java PID端口编号,2 打印线程堆栈,然后通过jstack pid命令获取信息。
    此外,我们写代码是用的.getStackTrace()方法也可以获得线程堆栈信息,只是获取到的是当前某个线程的运行状况。

    15 一个线程如果出现了运行时异常会怎么样?

    如果此异常没有被捕获到的话,这个线程就会停止。此外这个线程下的对象监视器也会被立即释放。

    16 如何在两个线程间共享数据

    通过在线程间共享对象就行了,也就是使两个线程使用的对象指向堆内存中的同一个块内存地址即可。然后通过wait notify/notifyAll 或await/singal/singall 等待和唤醒

    17 sleep 和wait方法的区别

    二者都可以使当前线程放弃一段时间cpu资源,
    不同的是 sleep不会放弃某个对象的监视器,wait会放弃。也就是如果sleep被包裹在某个锁内,其他线程还是进不来。但是不会占用资源

    18 生产者消费者模型的作用是什么?

    1,通过平衡生产者的生成能力和消费者的消费能力来提升整个系统的运行效率。
    2,解耦,减少二者间的关联,不耽误各自的独立发展。

    19 Threadlocal的作用

    就是它为每个线程提供了一个变量的副本,是各自线程操作的变量不是同一个,这样就做到了互不影响。而且也是异步进行的,比较快。但是既然是异步了就不是变量共享了,再者有副本就增大了内存,所以这也就是大家说的“以空间换时间”。
    案例:

    class Student2 {  
        private int age;  
      
        public int getAge() {  
            return age;  
        }  
      
        public void setAge(int age) {  
            this.age = age;  
        }  
    }  
      
    public class TestThreadLocal implements Runnable {  
      
        ThreadLocal studentLocal = new ThreadLocal();  
      
        public static void main(String[] args) {  
        	
            TestThreadLocal t = new TestThreadLocal();  
            new Thread(t, "t1").start();  
            new Thread(t, "t2").start();  
        }  
      
        @Override  
        public void run() {  
        	  
            Student2 s = this.getStudent();  
            Random random = new Random();  
            int age = random.nextInt(100);  
            System.out.println("current thread set age " + Thread.currentThread()  
                    + ":" + age);  
            s.setAge(age);  
            System.out.println("current thread first get age "  
                    + Thread.currentThread() + ":" + s.getAge());  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
            }  
            System.out.println("current thread second get age "  
                    + Thread.currentThread() + ":" + s.getAge());  
        
        }  
      
        public Student2 getStudent() {  
            Student2 s = (Student2) studentLocal.get();  
            if (s == null) {  
                s = new Student2();  
                studentLocal.set(s);  
            }  
            return s;  
        }  
    } 
    
    current thread set age Thread[t1,5,main]:76
    current thread set age Thread[t2,5,main]:39
    current thread first get age Thread[t1,5,main]:76
    current thread second get age Thread[t1,5,main]:76
    current thread first get age Thread[t2,5,main]:39
    current thread second get age Thread[t2,5,main]:39
    
    

    在上面的案例中,student2处于共享变量的角色,但是在输出结果上,两个线程睡眠前后set和get的值各自保持一致。

    20 为什么 wait方法和notify、notifyAll方法要在同步块中调用?

    这是JDK强制的,因为三个都要在调用前先获得对象的锁。

    21 wait方法和notify、notifyAll方法在放弃对象监视器时有什么区别

    wait会立即释放对象监视器
    notify、notifyAll方法会等待线程剩余代码执行完毕后才会放弃对象监视器。

    22 为什么要使用线程池

    1,避免频繁创建和消耗线程,达到线程重用
    2,还可以根据项目灵活地控制并发数目。

    23 怎么检测一个线程是否持有对象监视器

    Thread类提供了一个holdsLock(Object obj) 方法,当且仅当obj的监视器被线程持有时才会返回true。而且他还是个static方法,

    24 synchronize和reentrantlock的区别

    1,前者是关键字,后者是类
    2,reentrantlock既然是类,那就更加灵活,体现在可以设置获取锁的等待时间,避免线程持有锁一直不释放,造成死锁;可以获取各种锁的信息,可以实现多路通知。
    关于reentrantlock的案例:

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.locks.ReentrantLock;
     
     
    public class MyReentrantLock extends Thread {
    	TestReentrantLock lock;
    	private int id;
     
    	public MyReentrantLock(int i, TestReentrantLock test) {
    		this.id = i;
    		this.lock = test;
    	}
     
    	public void run() {
    		lock.print(id);
    	}
     
    	public static void main(String args[]) {
    		ExecutorService service = Executors.newCachedThreadPool();
    		TestReentrantLock lock = new TestReentrantLock();
    		for (int i = 0; i < 5; i++) {
    			service.submit(new MyReentrantLock(i, lock));
    		}
    		service.shutdown();
    		//不用线程池启动
    		MyReentrantLock lock2=new MyReentrantLock(1,lock);
    		MyReentrantLock lock3=new MyReentrantLock(4,lock);
    		lock2.start();
    		lock3.start();
    	}
    }
     
    class TestReentrantLock {
    	private ReentrantLock lock = new ReentrantLock();
     
    	public void print(int str) {
    		try {
    			lock.lock();
    			System.out.println(str + "获得");
    			Thread.sleep((int) (Math.random() * 1000));
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			System.out.println(str + "释放");
    			lock.unlock();
    		}
    	}
    }
    
    
    0获得
    0释放
    1获得
    1释放
    2获得
    2释放
    3获得
    3释放
    4获得
    4释放
    1获得
    1释放
    4获得
    4释放
    
    

    25 cuoncurrentHashMap的并发度是什么

    就是容量的大小,默认16 .
    它相对于hashmap就是线程安全的,相对于hashtable就是在线程安全的基础上又可以并发,但是hashtable是线程同步的,无并发。

    26 ReadWriteLock是什么?

    readwritelock是一个读写锁接口,实现了读写分离,读锁是共享的,写锁时独占的。也就是读与读之间是不互斥的,这是相对于Reentrantlock的优势,因为后者无论是读读还是读写都要一个个来。
    下面案例中的输出可见,写入到的时候是一个一个线程的执行,可以看到顺序,而读的时候线程间就无序了,说明是并行的。

    import	java.util.HashMap;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
     
    class Note{
        private volatile HashMap<Integer,Integer> map = new HashMap<> ();
        private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        private static int a=0;
        public void get(int key){
            readWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"读取开始");
            try { TimeUnit.MILLISECONDS.sleep(400); }catch (InterruptedException e){ e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"读取完成--------"+map.get(key));
            readWriteLock.readLock().unlock();
        }
        public void put(int key, int value){
            readWriteLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"写入开始");
            try { TimeUnit.MILLISECONDS.sleep(400); }catch (InterruptedException e){ e.printStackTrace(); }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+"写入完成");
            readWriteLock.writeLock().unlock();
        }
     
    }
    public class ReadWriteLockTest {
        public static void main(String[] args) {
            Note note = new Note();
            for (int i = 0; i < 5; i++) {
                int finalI = i;
                new Thread(() -> {
                    note.put(finalI,finalI);
                },String.valueOf(i)).start();
            }
            for (int i = 0; i < 5; i++) {
                int finalI = i;
                new Thread(() -> {
                    note.get(finalI);
                },String.valueOf(i)).start();
            }
     
        }
    }
    
    
    0写入开始
    0写入完成
    1写入开始
    1写入完成
    2写入开始
    2写入完成
    3写入开始
    3写入完成
    4写入开始
    4写入完成
    0读取开始
    1读取开始
    2读取开始
    3读取开始
    4读取开始
    0读取完成--------0
    2读取完成--------2
    4读取完成--------4
    3读取完成--------3
    1读取完成--------1
    
    

    27 什么是多线程的上下文切换

    是cpu控制器由一个正在运行的线程切换到另外一个就绪并等待获取cpu执行权的线程的过程

    28 如果提交任务时,线程池队列已满,会发什么什么?

    1,如果使用的是linkedblockingqueen这种容量趋近无限的队列,可以不予考虑
    2,如果是有界队列,比如arrayblockingqueen ,任务会首选被添加到arrayblockingqueen中,如果它也满了,会根据manxinumPoolsize的值增加线程数,如果增加的线程还是来不及处理新进的任务数。那么就会使用拒绝策略,报出溢出异常。

    29 java的线程调度算法是什么?

    抢占式,
    一个线程用完cpu后,操作系统会根据线程优先级,线程的饥饿程度等情况算出一个总的优先级,得出下个该执行的线程。

    30 Thread.sleep(0)的作用

    1.由于java抢占式的线程调度算法,如果某个线程的优先级比较高,到之前其他线程一直得不到执行,就可以执行Thread.sleep(0),可以跳过java的线程调度机制,让操作系统无差别的触发一次分配时间片的操作。这也是平衡cpu的一种手段

    31 什么是自旋

    如果synchronize同步代码块执行时间很短,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户状态和内核态的切换问题,也是比较繁琐的。因此既然同步块的时间很短,不防让等待锁的线程不要阻塞,而是在synchronize的边界做忙循环,这就是自旋。如果多次忙循环还没有活动锁,在阻塞不迟。

    32 什么是CAS

    CAS:compare and swap 比较再交换。它是区别于synchronize悲观锁的一种乐观锁.再者者也是一种编程思想,具体过程还要代码实现。
    由java 内存模型知道,多线程共享主存,各自又占有独有的cpu处理器。当两个线程同时区访问主存中的同一变量时,会把值完全拷贝一份到自己的工作内存中,假设t1线程优先修改了该变量,并同步到了主存中,当然同步前先对比了该变量与预先拷贝到自己工作内存的值是否一样,一样就可以同步了。然后t2线程也处理完了,也要去主存中同步该变量,但是同步前对比拷贝的变量时发现值不一样,也就是共享数据被修改了,那么操作就失败了,因为有其他操作先改变了这个值。那么操作失败了然后怎么办呢?那就要重新读取主存的值,再次做一遍业务逻辑处理,再更新主存数据。
    再者,CAS一定要volatile变量配合,这样才能那大主存中的值。

    33 什么是乐观锁,悲观锁

    乐观锁,对于并发间操作产生的线程安全问题持乐观状态,乐观锁任务竞争不总是发生,因此他不需要持有锁。它使用的安全机制就是CAS 比较再交换。它的缺点是只能保证一个共享变量的原子操作,再者是长期自旋可能导致开销过大。
    悲观锁:就是对于并发操作持悲观状态,认为竞争总会发生,因此对于每次对共享资源的操作就会持有一个独占的线程,比如synchronize.

    34 简单说下什么是AQS

    全称AbstractqueenedSychronize,抽象队列同步器。
    AQS是整个java并发包的核心,在Reentrantlock,conutdownlatch中都用到了AQS。那么是怎么使用的呢?以Reentrantlock为例,被阻塞的线程都会以一个个entry对象的形式放在aqs队列中,至于AQS队列,是个先进先出的双向队列。当前一个线程使用完了Reentrantlock锁,在AQS对列首节点的entry就拿到锁开始运行。
    此外要说的是,AQS同步器的实现方式有两种,一个是独占式,比如Reentrantlock,一个个来。一种是共享式,多个线程一起来,比如conutdownlatch,共享式获取线程同步状态是基于CAS的,这样保证即使多个线程操作也不有脏读的情况。就像火车售票一样。

    35 semaphore有什么作用

    semaphore就是一个信号量,他的作用是限制某段代码块的并发数。它有个构造函数,可以传入一个int整数,表示某段代码块最多有n个线程可以访问,如果超过了就等待。如果n=1那就是个synchronize了。
    在使用时,可在创建时创建设置信号量的个数,说成许可数的个数更合适。semaphore.acquire(); 方法是获取许可权,semaphore.release(); 方法是释放刚才获得的许可,如果不释放的话会造成线程阻塞,进行不下去,比如semaphore.acquire(2);也可以设置许可个数,但是相应的处理完后也要释放semaphore.release(2).否则就线程阻塞。
    案例1:

    package test;
    
    import java.util.concurrent.Semaphore;  
    import java.util.concurrent.locks.Lock;  
    import java.util.concurrent.locks.ReentrantLock;  
     
    public class SemaphoreDemo {  
      
        public static void main (String[] args) {  
            Storge storge = new Storge();  
            for (int i = 0; i < 12; i++) {  
                new MyThread(storge).start();  
            }  
        }  
    }  
      
    class Storge {  
      
        private volatile Boolean[] printer;  
      
        private Semaphore semaphore;  
      
        private Lock lock;  
      
        Storge () {  
            semaphore = new Semaphore(3);  
            printer = new Boolean[3];  
            for (int i = 0; i < printer.length; i++) {  
                printer[i] = true;  
            }  
            lock = new ReentrantLock();  
        }  
      
        public int getFreePrinter () {  
            lock.lock();  
            try {  
                for (int i = 0; i < printer.length; i++) {  
                    if (printer[i]) {  
                        printer[i] = false;  
                        System.out.println(i);
                        return i;  
                    }  
                }  
            } finally {  
                lock.unlock();  
            }  
            return -1;  
        }  
      
        public void setPrinter (int i) {  
            lock.lock();  
            try {  
                printer[i] = true;  
            } finally {  
                lock.unlock();  
            }  
        }  
      
        public void print () {  
            try {  
                //获取信号量  
                semaphore.acquire();  
                System.out.println("等待的队列长度是 "+semaphore.getQueueLength());
                
                int freePrinter = getFreePrinter();  
                System.out.println("Thread " + Thread.currentThread().getName() + " 正在使用"  + freePrinter + "号打印机");  
                Thread.sleep(1000);  
                setPrinter(freePrinter);  
                System.out.println("Thread " + Thread.currentThread().getName() + " 释放"  + freePrinter + "号打印机");  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                semaphore.release();  
            }  
      
        }  
      
      
    }  
      
    class MyThread extends Thread {  
        private Storge storge;  
      
        MyThread (Storge storge) {  
            this.storge = storge;  
        }  
      
        public void run () {  
            storge.print();  
        }  
      
    }  
    
    等待的队列长度是 0
    0
    Thread Thread-1 正在使用0号打印机
    等待的队列长度是 0
    1
    Thread Thread-0 正在使用1号打印机
    等待的队列长度是 0
    2
    Thread Thread-4 正在使用2号打印机
    Thread Thread-1 释放0号打印机
    等待的队列长度是 4
    0
    Thread Thread-3 正在使用0号打印机
    Thread Thread-0 释放1号打印机
    Thread Thread-4 释放2号打印机
    等待的队列长度是 3
    1
    Thread Thread-2 正在使用1号打印机
    等待的队列长度是 2
    2
    Thread Thread-6 正在使用2号打印机
    Thread Thread-3 释放0号打印机
    等待的队列长度是 1
    0
    Thread Thread-5 正在使用0号打印机
    Thread Thread-2 释放1号打印机
    Thread Thread-6 释放2号打印机
    等待的队列长度是 0
    1
    Thread Thread-7 正在使用1号打印机
    Thread Thread-7 释放1号打印机
    Thread Thread-5 释放0号打印机
    
    

    案例二,如果在print ()方法中把许可数设置成4,就会直接运行不下,因为我们初始设置的个数是3,哪来的4各个许可,会造成阻塞。

     //获取信号量  
                semaphore.acquire(4);  
    

    案例三,如果print方法改成如下:

     public void print () {  
            try {  
                //获取信号量  
                semaphore.acquire(3);  
                System.out.println("等待的队列长度是 "+semaphore.getQueueLength());
                
                int freePrinter = getFreePrinter();  
                System.out.println("Thread " + Thread.currentThread().getName() + " 正在使用"  + freePrinter + "号打印机");  
                Thread.sleep(1000);  
                setPrinter(freePrinter);  
                System.out.println("Thread " + Thread.currentThread().getName() + " 释放"  + freePrinter + "号打印机");  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                semaphore.release();  
            }  
      
    

    会输出如下:

    等待的队列长度是 0
    0
    Thread Thread-0 正在使用0号打印机
    Thread Thread-0 释放0号打印机
    
    

    因为获取许可的个数是3,但是只释放了一个,又造成了线程阻塞。

    36 什么是公平锁,什么是非公平锁,synchronize属于哪种?

    公平锁:获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁,也就是先来后到,依次执行。
    非公平锁:获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争。也就是java的抢占式锁机制。
    synchronize是非公平锁,因为java本身设计的就是抢占式的,所以java的应该都是非公平锁。

    什么是可重入锁

    也称为递归锁,就是某个线程可以多次获得某个同样的锁,只是给锁的状态加1,当锁的状态是0时说明此线程彻底释放了锁。synchronize和reentrantlock都是可重入锁。
    可重入锁也会导致某个线程一直持有某个同步块,不能让其他的线程进入。
    案例:

    public class ReentrantlockTest {
        public static void main(String[] args) {
            Phone phone = new Phone();
            // 第一个线程
            new Thread(() -> {
                phone.sendMsg();
            }, "t1").start();
            // 第二个线程
            new Thread(() -> {
                phone.sendMsg();
            }, "t2").start();
        }
    }
    
    class Phone {
        // 发送短信同步方法
        public  void sendMsg() {
        	synchronized (this) {
        		System.out.println(Thread.currentThread().getName() + " called sendMsg()");
        		// 进入另外一个同步着的方法
        		sendEmail();
    		}
        }
        // 发送邮件同步方法
        public  void sendEmail() {
        	synchronized (this) {
        		System.out.println(Thread.currentThread().getName() + " ******called sendEmail()");
        	}
        }
    }
    
    t1 called sendMsg()
    t1 ******called sendEmail()
    t2 called sendMsg()
    t2 ******called sendEmail()
    
    

    37 synchronized与static synchronized 的区别

    二者互不干扰,如果是对同一个对象加锁,那么二者会各自用于不同的锁,即使是同一个对象。这也说明了虽然是同一个对象,但是锁不一样,具体机制不太清楚。
    但是如果是同一个对象下有多个synchronize修饰的普通方法,那就要竞争等待了。
    案例如下:

    public class ReentrantlockTest2 {
        public static void main(String[] args) {
            Phone2 phone = new Phone2();
          //第0个线程
            new Thread(()->{
            	Phone2.sendMsg2();
            },"t0").start();
            // 第一个线程
            new Thread(() -> {
                phone.sendMsg();
            }, "t1").start();
            //第二个线程
            new Thread(()->{
            	phone.sendMsg2();
            },"t2").start();
            // 第三个线程
            new Thread(() -> {
                phone.sendEmail();
            }, "t3").start();
        }
    }
    
    class Phone2 {
        // 发送短信同步方法
        public  synchronized void sendMsg() {
    		System.out.println(Thread.currentThread().getName() + " called sendMsg()");
    		try {
    			Thread.sleep(5000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
        }
     // 发送短信同步方法
        public static synchronized void sendMsg2() {
    		System.out.println(Thread.currentThread().getName() + " called sendMsg2()");
    		try {
    			Thread.sleep(5000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
        }
        // 发送邮件同步方法
        public  synchronized void sendEmail() {
    		System.out.println(Thread.currentThread().getName() + " ******called sendEmail()");
        }
    }
    

    输出

    t0 called sendMsg2()
    t1 called sendMsg()
    t2 called sendMsg2()
    t3 ******called sendEmail()
    说明:前两个是同时进行的,说明带不带static是两个不同的锁,不会同步执行。后面两个要等5s才会执行。
    

    38 hashtable 的size()方法只有一个return count,为什么还要做同步

    其实只要操作的是同一个容器,在多线程情况下,此容器的不同方法不同步的话都有可能造成线程安全问题,前面我们讨论过了,同一对象下,不同的普通方法用的是同一把锁,所以当有线程对hashtable在做put操作时,put完了,但是还没来得及size(),另一个线就已经程执行size方法,就有可能拿到的值不是最新的。所以要加锁。
    再者,虽然只有一条return count,但是在编译成机器码后,就可能是好几行。也是有可能执行大其中某一行就做了cpu时间片切换,执行其他线程了。

    39 线程类的构造对象,静态代码块被哪个线程调用?

    是被new这个线程所在的线程类调用,而run方法里面的代码才是线程自身所调用的。

    40 锁粗化操作

    相对于加锁部分的代码,不加锁的自然就是异步进行的了,那自然就很快。所以,同步的范围就越小越好。但是凡事不是绝对的。如果对一个锁频繁的操作,开锁解锁也是很耗性能的,因为java虚拟机在线程上要反复的在内核态和用户态之间进行切换。
    比如说在for循环里面有个同步块和其他的非同步块代码。如果for循环里面的非同步块代码很快的话,那就可以做锁粗化操作了,把整个for循环都加在锁里面,让多次加锁解锁变成一次。

    41 线程池中的闲置线程是怎么存活等待的?占内存吗?

    1,线程池中的线程是通过take方法去队列中取任务的,如果队列没有任务了,那么线程就会发生阻塞。一旦有新的任务进到队列中,就会唤醒等待线程。这样就保证了核心线程一直存活。
    2,处于阻塞状态的线程是不占内存的。

    42 高并发,任务执行短的业务怎么设置线程个数?并发不高,任务执行长的业务怎么使用线程池?并发高,执行任务长的业务怎么设置线程个数?

    1,高并发,时间短的业务,说明线程执行速度很快,没必要做线程切换,等当前线程执行完了再执行队列中的不迟,此时线程数的个数跟cpu核数+1,保持一致即可。有超线程技术的话可以再*2
    2.并发不高,执行任务长的可以分开看,比如说花费在IO操作上的时间很长的业务,像我做得征信系统,需要调用各种外部数据源,花费的时间很长,就可以把线程数调高一些。然cpu处理更多业务。
    如果是计算操作时间长,也就是单个线程切切实实需要花费较长的cpu时间,那就还是跟cpu的核数保持一致吧,减少上下文的切换
    3,对于并发高,业务时间长的,这类问题线程池就无能为力了。更重要的是重视整体架构了,比如加机器负载均衡,设置缓存,使用中间件进行拆分和解耦等。

    43 java 线程池体系介绍

    1,顶级接口类Executor
    2,真正线程池接口ExecutorService 因为ExeuctorService提供的方法更全一点,而executor只有一个excetor方法。
    3,具体实现类ThreadPoolExecutor,它实现了ExecutorService 接口。应该算得上线程池的核心类了。它的构造函数参数有核心线程数,最大线程数,无任务时多余线程存活时间,keepalive的时间单位,工作队列,创建线程时使用的工厂,拒绝策略。
    4,如果ThreadPoolExecutor的参数过多,使用起来有些麻烦或困难,那么Executors工具类提供了一些我们常见的静态方法,用于生成各有特点的线城池,如下:
    newSingleThreadExecutor:创建一个单线程池,跟new Thread差不多,不过它的优势在于如果这个单线程挂了,会有新线程替代。java给他设定的工作队列是linkedblockingqueen,无界队列,这种队列是当线程池达到核心线程池时就把新任务放到队列中,故而这种如果给线程池设置最大线程池参数也就没什么意义了。
    newFixedThreadPool:创建固定大小线程池,进来一个任务就创建一个,直到达到最大线程数,如果又挂掉也会自动新建。java给他设定的工作队列是linkedblockingqueen,无界队列
    newCachedThreadPool:创建可缓存的线城池。这个看源码可知,java 给我们的设置是,如果线程大小超过了处理任务数会自动销毁多余线程,60s没有处理新任务就会销毁,而不够时又可以智能增加;至于线程数的最大值没有做限制,根据jvm能够创建的线程控制。它使用的工作队列是synchronousqueue,特点是也不存储任务,而是直接提交给线程池,不断的增加线程数。
    newScheduledThreadPool:创建一个大小无限的线城池,它以一个类似模板的形式支持定时和周期性任务。

    ExecutorService service = Executors.newCachedThreadPool();
    

    这些静态方法的返回对象都是ExecutorService接口。

    44 queen的三种排队策略

    1,直接提交,工作队列默认选项是synchronousqueue,它将任务直接提交给线程而不保持他们,不保持是什么意思呢?有人说synchronousqueue是无界线程,我觉着最好还是不用这种说法,容易迷惑人。不保持就是,synchronousqueue内部容器可以理解成只能容纳一个任务,当前一个任务被线程取走了,后面才能进来。所以说他是个同步队列器,在jkd官网也说明了这种策略可以避免在处理可能具有内部依赖性的请求集时出现锁,也就是先进来的线程先执行,同步的,不会混乱。
    再者因为synchronousqueue不保持任务,所以通常与newCachedThreadPool连用,新进的任务用创建新线程处理。
    2,无界队列,如linkedblockingqueue,任务多过线程池核心线程数是就会把任务放到队列中,这种可用于处理访问突发增多的情况,不过放入队列中的访问还是要等待处理的,响应还是会慢。
    3,有界队列,像arrayblockingqueue,有最大线程数,核心线程数,有助于防止资源耗尽。但最大线程数和核心线程数的设置要根据具体情况而定,如io多就多设置点线程,计算逻辑多还是和cpu核数保持一致吧,总之多测试吧

    45 线程池的拒绝策略

    拒绝策略就是线程池当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。
    拒绝策略有四种,不介绍了,工作中常用的是AbortPolicy 默认的拒绝策略,不接受新的请求进来。

    55 synchronized锁的原理,和可重入原理

    被synchronized的代码块编译后会生成monitorenter和monitorexit两个指令,当运行monitorenter指令时会把对象锁的计数器加1,当运行monitorexit指令时会把对象锁的计数器-1,当计数器为0时锁就被释方了。
    此外对于synchronized修饰的静态代码块,则表示锁为此代码块对应的类对象锁,就是在jvm中凡是用到这个类创建的对象都要受到这个同步块的限制。
    再者,是当线程拥有了这个对象的锁,如果这个线程又进入到这个对象的另一个同步块,那么会在把对象锁加1,同步块结束后再减一,所以不妨碍synchronized的可重入性。

    56 synchronized和reentrantlock的区别

    ReentrantLock是lock接口的实现类,能够比synchronized有更细的操作颗粒度,比如:持有锁的线程长期不释放,其它线程可以放弃等待。还有可以设置等待时间,超了就返回无结果。还有判断是否有线程在排队等待获取锁。实现公平锁。
    再者synchronized锁在异常是会自动释放锁,但后者必须要在finally中手动释放。
    最后听说前者在并发底的情况下性能更好些,并发高后者要好些。

    57 Reentrantlock锁的可重入性原理是?

    有个线程的id属性,查看当前请求线程的id和之前持有锁的id是不是同一个即可。

    58 线程池非核心线程的创建时机?

    非核心线程的创建要求是,核心线程满了,并且对列也满了,才会创建非核心线程区处理任务。所以对于使用无界队列的类型,可以不用设置最大线程数。

    59 如何在java线程池中提交线程

    常有两种方式:
    1,execute方法,ExecutroService.execute方法接收一个Runnable实例,执行任务
    2,submit方法:ExecutorService.submit方法返回Future对象,可获得线程返回结果

    60 CopyOnWriteArrayList是线程安全的

    当有新元素添加到copyonwritearraylist中时,先把原先的数组数据拷贝一份,在新的数组中做些操作。如果有多线程些,就加锁。1,如果些操作未完成或写完了引用还未指向新地址,读的还是原来的数组,2,如果引用指向了新数组地址就用新的数据。
    适合多读少些的场景。由于要copy所以占内存。

    展开全文
  • /**创建锁对象并将锁设置为公平锁*/ private Lock lock = new ReentrantLock(true);//默认为false

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,225
精华内容 490
热门标签
关键字:

java多线程面试题总结

java 订阅