笔记---java多线程

这篇文章的问题和答案是我在准备面试复习时查询资料和代码测试自己整理的,分享给大家,如有不正确的地方,欢迎大家批评指正。

 1、线程池的原理,为什么要创建线程池?创建线程池的方式?
        原理及为什么:
            假设一个服务器完成一项任务所需时间为:T1创建线程时间+T2在线程中执行任务的时间+T3销毁线程时间。线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。
        当我们需要频繁调用系统A的服务时,就会频繁的创建线程和关闭线程。创建线程和回收线程都会占用系统资源,大量创建回收线程都会增加系统负担、降低系统性能。因此,为了提高系统性能,
        我们提前创建一些线程,这些线程由线程池管理,使用的时候就从线程池里选一个,使用完毕就归还给线程池。当然创建线程池肯定是会占用一点内存空间。
        创建方式:
            Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:
            1.ExecutorService fixedExecutor = Executors.newFixedThreadPool(3);
            定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,可控制线程最大并发数,超出的线程会在队列中等待。
            2.ExecutorService cachedExecutor = Executors.newCachedThreadPool();
            可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制。
            3.ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(3);
            定长线程池,可执行周期性的任务。
            4.ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
            单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
            5.ScheduledExecutorService singleScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
            单线程可执行周期性任务的线程池。
 
2、线程的生命周期,什么时候会出现僵死进程?
        生命周期:
            1.New(初始化状态)
            new Thread 在Java层面的线程被创建了,而在操作系统中的线程其实是还没被创建的,所以这个时候是不可能分配CPU执行这个线程的!
            2.Runnable(可运行/运行状态)
            调用start()方法后分配CPU执行,线程就处于这个状态。
            3.Blocked(阻塞状态)
            这个状态下是不能分配CPU执行的,只有一种情况会导致线程阻塞,就是synchronized!被synchronized修饰的方法或者代码块同一时刻只能有一个线程执行,而其他竞争锁的线程就从Runnable到了Blocked状态!
            当某个线程竞争到锁了它就变成了Runnable状态。注意并发包中的Lock,是会让线程属于等待状态而不是阻塞,只有synchronized是阻塞。
            4.Waiting(无时间限制的等待状态)
            这个状态下也是不能分配CPU执行的。有三种情况会使得Runnable状态到waiting状态:
                调用无参的Object.wait()方法。等到notifyAll()或者notify()唤醒就会回到Runnable状态。
                调用无参的Thread.join()方法。也就是比如你在主线程里面建立了一个线程A,调用A.join(),那么你的主线程是得等A执行完了才会继续执行,这时你的主线程就是等待状态。
                调用LockSupport.park()方法。LockSupport是Java6引入的一个工具类Java并发包中的锁都是基于它实现的,再调用LocakSupport.unpark(Thread thread),就会回到Runnable状态。
            5.Timed_Waiting(有时间限制的等待状态)
            这个状态和Waiting就是有没有超时时间的差别,这个状态下也是不能分配CPU执行的。有五种情况会使得Runnable状态到waiting状态:
                Object.wait(long timeout)。
                Thread.join(long millis)。
                Thread.sleep(long millis)。注意 Thread.sleep(long millis, int nanos) 内部调用的其实也是Thread.sleep(long millis)。
                LockSupport.parkNanos(Object blocked,long deadline)。
                LockSupport.parkUntil(long deadline)。
            6.Terminated(终止状态)
            在我们的线程正常run结束之后或者run一半异常了就是终止状态!
            注意有个方法Thread.stop()是让线程终止的,但是这个方法已经被废弃了,不推荐使用,因为比如你这个线程得到了锁,你stop了之后这个锁也随着没了,其它线程就都拿不到这个锁了!所以推荐使用interrupt()方法。
            interrupt()会使得线程Waiting和Timed_Waiting状态的线程抛出 interruptedException异常,使得Runnabled状态的线程如果是在I/O操作会抛出其它异常。
            如果Runnabled状态的线程没有阻塞在I/O状态的话,那只能主动检测自己是不是被中断了,使用isInterrupted()。
        僵死进程:
            一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
        怎么处理僵死进程:
            1、通过信号机制
            子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。调用wait()或者waitpid(),让父进程阻塞等待僵尸进程的出现,处理完在继续运行父进程。
            2、杀死父进程
            当父进程陷入死循环等无法处理僵尸进程时,强制杀死父进程,那么它的子进程,即僵尸进程会变成孤儿进程,由系统来回收。
            3、重启系统
            当系统重启时,所有进程在系统关闭时被停止,包括僵尸进程,开启时init进程会重新加载其他进程。
 
    3、 说说线程安全问题,什么是线程安全,如何实现线程安全?
 
     什么是线程安全:    
        线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
       如何实现线程安全:
            1.无状态的确定性函数。给定一个特定的输入,它总是产生相同的输出。 该方法既不依赖于外部状态,也不维护状态。
            2.当一个类实例的内部状态在构造之后不能被修改时,它就是不可变的。并且他也是线程安全的。
            3.线程本地变量。创建线程安全的类,在此类中创建变量,那么这些变量就不会再线程之间共享,因此也是线程安全的。
            4.同步集合-- Collections.synchronizedCollection(new ArrayList<>());
同步集合的方法都用synchronized修饰(内部锁), 意味着方法在某一时刻只能被一个线程访问,而其他线程将被阻塞,直到该方法被第一个线程解锁。 但是同步的性能在并发量高的情况下会受到影响。
            5.并发集合-- new ConcurrentHashMap<>(); 与同步集合不同,并发集合通过将数据划分为段来实现线程安全性。 例如在 ConcurrentHashMap中,多个线程可以获取不同段上的锁,因此多个线程可以同时访问。也因此 并发集合比同步集合具有更高的性能, 同步和并发集合只使集合本身成为线程安全的,而不是内容
            6.原子对象--Java 提供的一组原子类(包括 AtomicInteger、 AtomicLong、 AtomicBoolean 和 AtomicReference)实现线程安全。原子类允许我们在不使用同步的情况下执行线程安全的原子操作。 原子操作在单个机器级别的操作中执行。
            7.用synchronized关键字修饰方法或代码块&语句 -- 同步锁
            8.用volatile关键字修饰变量 -- 可见性,直接对主存读写
            9.Lock锁机制
                9.1.Reentrantlock 构造函数接受一个可选的布尔参数。 当参数设置为 true 时,并且多个线程试图获取锁时,JVM 将优先考虑等待时间最长的线程,并授予对锁的访问权。
                9.2.读 / 写锁
        private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();                private final Lock readLock = rwLock.readLock(); 
        private final Lock writeLock = rwLock.writeLock();
 
    4、 创建线程池有哪几个核心参数?如何合理配置线程池的大小?
核心参数:
            1.corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
            如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
            2.runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
                1.ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
                2.LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
                3.SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
                4.PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
            3.maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
            4.ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。
            5.RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
                以下是JDK1.5提供的四种策略:
                    1、ThreadPoolExecutor.AbortPolicy:
                        当线程池中的数量等于最大线程数时抛 java.util.concurrent.RejectedExecutionException 异常,涉及到该异常的任务也不会被执行,线程池默认的拒绝策略就是该策略。
                    2、ThreadPoolExecutor.DiscardPolicy():
                        当线程池中的数量等于最大线程数时,默默丢弃不能执行的新加任务,不报任何异常。
                    3、ThreadPoolExecutor.CallerRunsPolicy():
                        当线程池中的数量等于最大线程数时,重试添加当前的任务;它会自动重复调用execute()方法。
                    4、ThreadPoolExecutor.DiscardOldestPolicy():
                        当线程池中的数量等于最大线程数时,抛弃线程池中工作队列头部的任务(即等待时间最久的任务),并执行当前任务。
            6.keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
            7.TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
        线程池的工作原则:
            当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
            当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池中的核心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取任务并处理。
            当 taskQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目达到 maximumPoolSize(最大线程数量设置值)。
            如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任务拒绝处理。
        如何合理配置:
            Little's Law(利特尔法则)
            最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
            线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
            (1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
            (2)并发不高、任务执行时间长的业务要区分开看:
              a)IO密集型的任务,因为IO操作并不占用CPU,可以适当加大线程池中的线程数目,让CPU处理更多的业务
              b)计算密集型任务,线程池中的线程数设置得少一些,减少线程上下文的切换
            (3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。
            最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
 
5、volatile、ThreadLocal的使用场景和原理?
 ThreadLocal:
        ThreadLocal是用于解决多线程共享类的成员变量,原理:在每个线程中都存有一个本地ThreadMap,相当于存了一个对象的副本,key为threadlocal对象本身,value为需要存储的对象值,这样各个线程之间对于某个成员变量都有自己的副本,不会冲突。
 
    volatile:
        Volatile可以看做是一个轻量级的synchronized,它可以在多线程并发的情况下保证变量的“可见性”,可见性就是在一个线程的工作内存中修改了该变量的值,该变量的值立即能回显到主内存中,从而保证所有的线程看到这个变量的值是一致的。
        所以在处理同步问题上它大显作用,而且它的开销比synchronized小、使用成本更低。
        但是volatile不具有操作的原子性,也就是它不适合在对该变量的写操作依赖于变量本身自己。举个最简单的栗子:在进行计数操作时count++,实际是count=count+1;,count最终的值依赖于它本身的值。
        所以使用volatile修饰的变量在进行这么一系列的操作的时候,就有并发的问题。
 
     6、ThreadLocal什么时候会出现OOM的情况?为什么?
    
    出现OOM的情况:
        threadLocal设为null和线程结束这段时间(线程对象不被回收)的情况,容易发生内存泄露
    为什么:
        ThreadLocal里面使用了一个存在弱引用的map,不过弱引用只是针对key,每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, currentthread就不会存在栈中,强引用断开, CurrentThread, Map, value将全部被GC回收.所以,存在着内存泄露.的可能,最好的做法是当threadlocal无用时调用其remove方法。
    补充:什么是弱引用?什么是强引用?
PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
 
   7、 synchronized、volatile区别、synchronized锁粒度、模拟死锁场景、原子性与可见性?
    volatile:
    volatile是变量修饰符,修饰的变量具有可见性,一旦某一个线程修改了被volatile修饰的变量,该变量会立即刷新到主内存,普通的变量操作是线程在寄存器或者CPU缓存上进行的,操作完才会同步到主内存中,但是被volatile修饰的变量是直接读写主内存。
    volatile可以禁止指令重排, 程序执行到volatile修饰变量的读操作或者写操作时,前面的操作已经完成,且结果对后面的操作可见,后面的操作还没有进行。
    synchronized
    synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。
    可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。
    原子性表现在:要么不执行,要么执行到底。
异同:
(1)从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。
 
(2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。
 
但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章