线程安全问题

共有哪3类线程安全问题?

        发布和初始化导致线程安全问题

public class WrongInit {
    private Map students;

    public WrongInit() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                students = new HashMap<>();
                students.put(1, "赵");
                students.put(2, "钱");
                students.put(3, "孙");
                students.put(4, "李");
            }
        }).start();
    }

    public Map getStudents() {
        return students;
    }

    public static void main(String[] args) {
        WrongInit multiThreadsError6 = new WrongInit();
        System.out.println(multiThreadsError6.getStudents().get(1));
    }
}

该程序会出现.NullPointerException

因为students这个成员变量是在构造函数中新建的线程中进行的初始化和赋值操作,而线程的启动需要一定的时间,但是我们的main函数并没有进行等待就直接获取数据,导致getStudents获取的结果为null。

        运行结果错误

public class WrongResult {
    static int i;

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 100; j++) {
                    i++;
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        thread1.run();
        Thread thread2 = new Thread(runnable);
        thread2.run();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}

        该程序可能最后输出的结果不是200 。视处理器性能

        比如++操作,表面上看只是一行代码,但实际上它并不是一个原子操作,它的执行步骤主要分为三步,而且在每步操作之间都有可能被打断。第一个步骤是读取,第二个步骤是增加,第三个步骤是保存。


        活跃性问题

死锁

public class MayDeadLock {
    Object o1 = new Object();
    Object o2 = new Object();

    public void thread1() throws InterruptedException {
        synchronized (o1) {
            Thread.sleep(500);
            synchronized (o2) {
                System.out.println("线程1成功那到两把锁");
            }
        }
    }

    public void thread2() throws InterruptedException {
        synchronized (o2) {
            Thread.sleep(500);
            synchronized (o1) {
                System.out.println("线程2成功那到两把锁");
            }
        }
    }

    public static void main(String[] args) {
        MayDeadLock mayDeadLock = new MayDeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mayDeadLock.thread1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mayDeadLock.thread2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

活锁

        假设有一个消息队列,队列里放着各种各样需要被处理的消息,而某个消息由于自身被写错了导致不能被正确处理,执行时会报错,可是队列的重试机制会重新把它放在队列头进行优先重试处理,但这个消息本身无论被执行多少次,都无法被正确处理,每次报错后又会被放到队列头进行重试,周而复始,最终导致线程一直处于忙碌状态,但程序始终得不到结果,便发生了活锁问题。

饥饿

        在Java中有线程优先级的概念,Java中优先级分为1到10,1最低,10最高。如果我们把某个线程的优先级设置为1,这是最低的优先级,在这种情况下,这个线程就有可能始终分配不到CPU资源,而导致长时间无法运行。或者是某个线程始终持有某个文件的锁,而其他线程想要修改文件就必须先获取锁,这样想要修改文件的线程就会陷入饥饿,长时间不能运行。

哪些场景需要额外注意线程安全问题?

        访问共享变量或资源 例子为  运行结果错误i++的操作

        典型的场景有访问共享对象的属性,访问static静态变量,访问共享的缓存,等等因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题 。

 
        依赖时序的操作

if (map.containsKey(key)){
    map.remove(obj)

        随后一个线程率先把oj给删除了,而另外一个线程它刚已经检查过存在ky对应的元素,if条件成立,所以它也会继续执行删除obj的操作,但实际上,集合中的obj已经被前面的线程删除了这种情况下就可能导致线程安全问题。


        不同数据之间存在绑定关系

        有时候我们的不同数据之间是成组出现的,存在着相互对应或绑定的关系,最典型的就是P和端口号有时候我们更换了P,往往需要同时更换端口号,如果没有把这两个操作绑定在一起,就有可能出现单独更换了P或端口号的情况,而此时信息如果已经对外发布,信息获取方就有可能获取一个错误的P与端口绑定情况,这时就发生了线程安全问题在这种情况下,我们也同样需要保障操作的原子性。 


        对方没有声明自己是线程安全的

        在我们使用其他类时,如果对方没有声明自己是线程安全的,那么这种情况下对其他类进行多线程的并发操作,就有可能会发生线程安全问题比如我们定义了ArrayList.,它本身并不是线程安全的,如果此时多个线程同时对ArrayList进行并发读/写,那么就有可能会产生线程安全问题,造成数据出错,而这个责任并不在ArrayList因为它本身并不是并发安全的 。

为什么多线程会带来性能问题

        1什么是性能问题

        服务器的响应慢、吞吐量低、内存占用过多。多线程之间则需要调度以及合作,调度与合作就会带来性能开销从而产生性能问题 。

        2为什么多线程会带来性能问题

        3.调度开销

        4.协作开销

使用线程弛比手动创建线程好在哪里?

 
        1.为什么要使用线程池

        如果每个任务都创建一个线程会带来哪些问题:
1.反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么就
有可能导致创建和销毁线程消耗的资源比线程执行任务本身消耗的资源还要大
2.过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。

        首先,针对反复创建线程开销大的问题,线程池用一些固定的线程一直保持工作状态并反复执行任务。其次,针对过多线程占用太多内存资源的问题,解决思路更直接,线程池会根据需要创建线程,控制线程的总数量,避免占用过多内存资源。


        2.线程池解决问题思路


        3.如何使用线程池


        4.使用线程池的好处

线程池中各个参数的含义


        1.线程池的参数

线程安全问题_第1张图片


2.线程创建的时机

线程安全问题_第2张图片

corePoolSize->workQueue->maxPoolSize->Handler

        corePoolSize指的是核心线程数,线程池初始化时线程数默认为0,当有新的任务提交后,会创建新线程执行任务,如果不做特殊设置,此后线程数通常不会再小于corePoolSize,因为它们是核心线程,即便未来可能没有可执行的任务也不会被销毁
        随着任务量的增加,在任务队列满了之后,线程池会进一步创建新线程,最多可以达到max PoolSize来应对任务多的场景,如果未来线程有空闲,大于corePoolSize的线程会被keepAliveTime规定的时间被收回合理回收所以正常情况下,线程池中的线程数量会处在corePoolSize与maxPoolSize的闭区间内。 

线程安全问题_第3张图片 
        ThreadFactory        

        ThreadFactory可以选择使用默认的线程工厂,创建的线程都会在同一个线程组并拥有一样的优先级,且都不是守护线程我们也可以选择自己定制线程工厂,以方便给线程自定义命名不同的线程池内的线程通常会根据具体业务来定制不同的线程名


        workQueue和Handler

线程池有哪4种拒绝策略

        1.拒绝时机

        第一种情况是当我们调用shutdown等方法关闭线程池后,即便此时可能线程池内部依然有没执行完的任务正在执行,但是由于线程池已经关闭,此时如果再向线程池内提交任务,就会遭到拒绝。        

        第二种情况是线程池没有能力继续处理新提交的任务,也就是工作已经非常饱和的时候 。

 拒绝策略。

线程安全问题_第4张图片

         2.AbortPolicy

        这种拒绝策略在拒绝任务时,会直接抛出一个类型为RejectedExecutionException的Runtime Exception让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。 

线程安全问题_第5张图片

        3.DiscardPolicy

        当新任务被提交后直接被丢弃掉,也不会给你任何的通知相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。  线程安全问题_第6张图片

         4.DiscardOldestPolicy

        如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的。

线程安全问题_第7张图片
        

        5.CallerRunsPolicy 

         当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务这样做主要有两点好处:

        第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。

        第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

线程安全问题_第8张图片

有哪6种常见的线程池?什么是Java8的ForkJoinPool

线程安全问题_第9张图片

         1.FixedThreadPool

        核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程。

        特点:线程池中的线程数除了初始阶段需要从0开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了 

        2.CachedThreadPool

         CachedThreadPool,可以称作可缓存线程池

        特点:在于线程数是几乎可以无限增加的(实际最大可以达到Integer.MAX VALUE,为231-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是Synchronous Queue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。

线程安全问题_第10张图片

         3.ScheduledThreadPool

线程安全问题_第11张图片

线程安全问题_第12张图片

        第三种方法scheduleWithFixedDelay与第二种方法类似,也是周期执行任务,区别在于对周期的定义,之前的scheduleAtFixedRate是以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,而不管任务需要花多久执行;而scheduleWithFixedDelay方法以任务结束的时间为下一次循环的时间起点开始计时。 

        4.SingleThreadExecutor(单线程 按照任务提交的顺序依次执行)

         SingleThreadExecutor会使用唯一的线程去执行任务原理和FixedThreadPool是一样的,只不过这里线程只有一个如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务

        5.SingleThreadScheduledExecutor

        SingleThreadScheduledExecutor?实际和第三种ScheduledThreadPool线程池非常相以,它只是ScheduledThreadPool的一个特例,内部只有一个线程。

线程安全问题_第13张图片

        6.ForkJoinPool

线程安全问题_第14张图片

public class Fibonacci extends RecursiveTask {
    int n;
    public Fibonacci(int n) {
        this.n = n;
    }

    /**
     * The main computation performed by this task.
     *
     * @return the result of the computation
     */
    @Override
    protected Integer compute() {
        if (n <= 1) {
            return n;
        }
        Fibonacci f1 = new Fibonacci(n - 1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n - 2);
        // 1.
        f2.fork();
        return f1.join() + f2.join();
        // 2.
//        return f2.compute() + f1.join();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        for (int i = 0; i < 10; i++) {
            ForkJoinTask task = forkJoinPool.submit(new Fibonacci(i));
            System.out.println(task.get());
        }
    }

线程安全问题_第15张图片

ForkJoinPool下的每个线程都有属于自己的任务队列。(子任务分裂出的),每个任务队列的执行(后进先出)。其他线程协助处理任务时候会从该线程的末尾steal task顺序是先进先出。

线程池常见的阻塞队列有哪些

        1.线程池内部结构

线程安全问题_第16张图片

         线程池管理器:主要负责管理线程池的创建、销毁、添加任务等管理操作,是整个线程池的管家。

        工作线程:就是图中的线程t0~t9,这些线程从任务队列中获取任务并执行。

        任务队列作为一种缓冲机制,线程池会把当下没有处理的任务放入任务队列中,由于多线程同时从任务队列中获取任务是并发场景,此时就需要任务队列满足线程安全的要求,所以线程池中任务队列采用BlockingQueue来保障线程安全。

        任务:任务要求实现统一的接口,以便工作线程可以处理和执行。

        2.阻塞队列

 线程安全问题_第17张图片

        FixedThreadPool 和 SingleThreadExector:使用的阻塞队列是容量为Integer.MAX_VALUE的LinkedBlockingQueue,可以认为是无界队列由于FixedThreadPool线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要LinkedBlockingQueue这样一个没有容量限制的阻塞队列来存放任务。

这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。

        CachedThreadPool:SynchronousQueue对应的线程池是CachedThreadPool线程池CachedThreadPool的最大线程数是Integer的最大值,可以理解为线程数是可以无限扩展的CachedThreadPool和上一种线程池FixedThreadPool的情况恰恰相反,FixedThreadPool的情况是阻塞队列的容量是无限的,而这里CachedThreadPool是线程数可以无限扩展所以Cached Thread Pool线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

        ScheduledThreadPool和SingleThreadScheduledExecutor :DelayedWorkQueue对应的线程池分别是ScheduledThreadPool和SingleThreadScheduledExecutor这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务DelayedWorkQueue的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构之所以线程池ScheduledThreadPool和Single Thread Scheduled Executor选择DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

为什么不应该自动创建线程池

        1.FixedThreadPool  2.SingleThreadExecutor

线程安全问题_第18张图片

        如果我们对任务的处理速度比较慢,那么随着请求的增多,队列中堆积的任务也会越来越多,最终大量堆积的任务会占用大量内存,并发生OOM,也就是OutOfMemoryError,这几乎会影响到整个程序,会造成很严重的后果。

        3.CachedThreadPool

线程安全问题_第19张图片

        CachedThreadPool:并不限制线程的数量,当任务数量特别多的时候,就可能会导致创建非常多的线程最终因为超过了操作系统的上限而无法创建新线程,或者是内存不足

        4.ScheduledThreadPool 和 SingleThreadScheduledExecutor

线程安全问题_第20张图片

        最大线程数为Integer.MAX_VALUE,使用的队列为DelayedWorkQueue。

合适的线程数量是多少?CPU核心数和线程数的关系?

         1.CPU密集型任务

        比如:加密、解密、压缩、计算等一系列需要大量耗费CPU资源的任务对于这样的任务最佳的线程数为CPU核心数的1~2倍,如果设置过多的线程数,实际上并不会起到很好的效果。

        2.耗时IO型任务

《Java并发编程实战》的作者Brain Goetz推荐的计算方法:线程数=CPU核心数*(1+平均等待时间/平均工作时间)。【数据库、文件、网络通信】

如何根据实际需求,定制自己的线程池?

 

        1核心线程数

        更好的办法是用不同的线程池执行不同类型的任务,让任务按照类型区分开,而不是混杂在一起这样就可以按照上一课时估算的线程数或经过压测得到的结果来设置合理的线程数了,达到更好的性能。

        2.阻塞队列

        可以选择之前介绍过的LinkedBlockingQueue或者SynchronousQueue或者DelayedWorkQueue。还有一种常用的阻塞队列叫ArrayBlockingQueue,也经常被用于线程池中这种阻塞队列内部是用数组实现的,在新建对象的时候要求传入容量值,且后期不能扩容,所以ArrayBlockingQueue的最大的特点就是容量是有限的。

线程安全问题_第21张图片

        3.线程工厂

线程安全问题_第22张图片

 Google提供的ThreadFactoryBuilder

        4.拒绝策略

        除了AbortPolicy,DiscardPolicy,DiscardOldestPolicy或CallerRunsPolicy还可以通过实现RejectedExecutionHandler接口来实现自己的拒绝策略在接口中我们需要实现rejectedExecution方法,在rejectedExecution方法中,执行例如打印日志、暂存任务、重新执行等自定义的拒绝策略,以便满足业务需求。

线程安全问题_第23张图片

线程安全问题_第24张图片

如何正确关闭线程池?shutdown与shutdownNow的区别?

线程安全问题_第25张图片

 

        1.shutdown()

        它可以安全地关闭一个线程池,调用shutdow()方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用shutdown()方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。但这并不代表shutdown()操作是没有任何效果的调用shutdown()方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。

        2.isShutdown()

        可以返回true或者false来判断线程池是否已经开始了关闭工作也就是是否执行了shutdown或者shutdownNow方法这里需要注意,如果调用isShutdown()方法的返回的结果为tue并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务

        3.isTerminated()

        这个方法可以检测线程池是否真正“终结”了这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了比如此时已经调用了shutdown方法,但是有一个线程依然在执行任务,那么此时调用isShutdown方法返回的是true,而调用isTerminated方法返回的便是false,因为线程池中还有任务正在被执行,线程池并没有真正“终结直到所有任务都执行完毕了,调用isTerminated(0方法才会返回true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。

        4.awaitTermination()

        一种在执行时间内判断线程是否已经"终结"。

        比如我们给awaitTermination方法传入的参数是l0秒,那么它就会陷入l0秒钟的等待直到发生以下三种情况之一:

        1.等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回true

        2.等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回false

        3.等待期间线程被中断,方法会抛出InterruptedException异常。

        5.shutdownNow()

        由于Java中不推荐强行停止线程的机制的限制,即便我们调用了shutdownNow方法,如果被中断的线程对于中断信号不理不睬,那么依然有可能导致任务不会停止。(利用终端信号协同工作)

线程安全问题_第26张图片

 线程池实现“线程复用”的原理

        1.线程复用原理

        线程池可以把线程和任务进行解耦,线程归线程,任务归任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制在线程池中,同一个线程可以从BlockingQueue中不断提取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的run方法,把run方法当作和普通方法一样的地位去调用,相当于把每个任务的run(0方法串联了起来,所以线程数量并不增加。

        2.线程复用源码解析

线程安全问题_第27张图片

public void execute(Runnable command) {
        // 判断Runabl任务是否等于null
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        // 工作线程小于核心线程,创建线程
        if (workerCountOf(c) < corePoolSize) {
            // 见下图1
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 说明当前线程≥核心线程 或者addWorker失败了
        // 检查线程池状态是否为Running 并且工作队列已经添加了Runnable任务
        // workQueue 用于保存任务并移交给工作线程的队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 如果线程池已经不处于Running状态 并且 执行器的内部队列中删除该任务 返回为True
            if (! isRunning(recheck) && remove(command))
                // 执行拒绝策略
                reject(command);
            // 说明线程池状态为Running 为了防止任务被添加进来没有了执行线程的情况发生(比如直线的线程被回收了或者意外终止了)
            else if (workerCountOf(recheck) == 0)
                //这个方法执行时只是创建了一个新的线程,但是没有传入任务,这是因为前面已经将任务添加到队列中了,这样可以防止线程池处于 running 状态,但是没有线程去处理这个任务。
                addWorker(null, false);
        }
        // 说明线程的状态不是Running状态 或者 线程数 ≥ 核心线程 且(任务队列已经满了???哪里看出来的)
        // 创建新的线程提供标识为false- 核心线程之外的最大线程数
        else if (!addWorker(command, false))
            // 添加线程失败了,执行拒绝策略
            reject(command);
    }

线程安全问题_第28张图片

 

线程安全问题_第29张图片

        第一步,通过取Worker的firstTask(第一个任务不需要getTask)或者通过getTask(不是第一个任务)方法从workQueue中获取待执行的任务。

        第二步,直接调用task的run方法来执行具体的任务(而不是新建线程)。

 

你可能感兴趣的:(Thread,java,并发)