• 多线程总结


    https://www.cnblogs.com/128-cdy/p/12454855.html

    进程和线程:指的是一段正在运行的程序,一个程序支持很多任务执行,任务又称为线程。是资源分配的最小单位。 线程是程序执行(CPU调度)的最小单位。

    线程的创建:

    • 继承Thread类,重写run方法
    • 实现Runnable接口,重写run方法
    • 实现Callable接口 ,重写call方法,存在返回值,需要抛异常,FutureTask对象可以获取返回值(get()方法)(采用了适配器模式,FutureTask实现了Runnable接口,构造函数是Callable),异步计算的结果。
    • 线程池

    守护线程:具备自动结束生命周期的特点,当么有一个非守护线程时,则JVM会在某一程序结束的时候自动退出;线程.setDeamon(true)将其设置为守护线程(后台程序可以设置为守护线程)。

    进程间的通信方式:

    1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系(无名管道)。
    2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
    3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
    5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
    6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
    7. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

    线程六状态的转换

            通过new创建一个Thread对象进入新建状态调用start()方法后,进入Runnable(就绪状态),当处于就绪状态的线程得到CPU的使用权,也就是被调度,将会进入Running(运行状态),当此时的线程调用wait(),join()方法,便会进Waiting(等待阻塞状态),此时的线程会释放CPU的使用权,并且会释放资源(锁),调用wait()方法后,在调用notify(),或者notifyAll()方法后便会进入到Blocking(阻塞状态),此时线程获取到监视器锁会再次进入到Runnable(就绪状态);在Running(运行状态),线程等待监视器锁等待用户输入,也会进入到Blocking(阻塞状态),此时线程获取到监视器锁并且用户输入完成会进入到Runnable(就绪状态);在Running(运行状态),调用sleep(time)方法,会进入到Timed_Waiting(睡眠阻塞状态),睡眠时间到了后会进入到Runnable(就绪状态);在Running(运行状态),调用yeid()方法,会释放一次CPU的使用权,进入到就绪状态;在Running(运行状态)的run()方法执行完毕后,线程就会进入到Terminated(终止状态)。

     注意:

    •  wait()方法会释放CPU使用权以及锁;
    • 但是sleep()方法仅仅只会释放掉CPU的使用前并不会释放锁(抱着锁睡觉),线程被放入超时等待队列,与yield()相比,他使得线程较长时间得不到运行;
    • yield()方法放弃一次CPU的使用权,让给谁是由操作系统决定的,会使当前的线程由运行态进入到就绪态,是使用在同步方法或者同步代码块中,不会释放当前的监视器锁,并且线程很快会执行;
    • wait()方法需要和notify()/notifyAll()方法用在同步方法和同步块中;
    • yield()是启发式的方法,提醒调度器愿意放弃当前的CPU资源,当CPU不紧张时会忽略这种请求。

    java线程的生命周期?

    • new    当前线程并不存在。
    • Runnable    调用start()方法启动之后进入该状态,线程并不是一启动就会得到执行,需要等到调度器,得到CPU的使用权,该状态的线程存放在可运行的线程的线程池中。(Running 状态表示当前线程正在执行)。
    • Blocked     
    • Waiting   
    • Time_Waiting   
    • Terminated

    start()方法的调用和run()方法的区别?

             在start()方法里调用了一次start0()方法,该方法使用了native关键字进行定义,native指的是调用本机的原生系统函数。调用start方法会告诉JVM去分配本机系统的资源,才能实现多线程,最终让run()方法在开启的线程中执行,执行完毕后会将started标识置为true。但是调用run()只是普通方法的调用,无法启动一个线程。

    sleep()方法和yield()方法的区别?

             sleep()方法使程序进入到阻塞状态,并且可以设置时间,能够使的线程较长时间得不到运行,但是yield()是启发式方法,提醒调度器愿意放弃当前的CPU资源,当CPU不紧张时会忽略这种请求,并且其放弃一次CPU使用权以后会使得线程进入到就绪状态。

    sleep()方法与wait()方法的区别? 

            sleep()是属于Thread类中的,用来控制线程,而wait()是属于Object中的方法,作用于线程间通信的;sleep()方法使用在同步代码块或者同步方法中,不会释放当前持有的监视器锁,但是wait()方法会释放掉监视器锁以及CPU的使用权;调用wait()方法会使得线程进入到等待池中。       (sleep()方法只能传毫秒,TimeUnit枚举类可以设置不同单位的睡眠时间)。

    interrupted()和isInterrupted方法的区别?

           首先看看 interrupt()方法,该方法用于中断线程,将中断位设置为true,线程中断仅仅是设置线程的中断位状态,不会停止线程,需要用户自己去监视线程的状态并且做处理,支持线程中断的方法<wait(),jion(),sleep()>(也就是线程中断会抛出interruptedException的异常的方法)就是在监视线程的中断状态,一旦线程被设置为“中断状态”,就会抛出中断异常。interrupted()查询当前线程的中断状态,并且清除原状态,如果一个线程被中断了,第一次调用该方法返回true,第二次和后面的就返回false。isInterrupted仅仅是查询当前线程的中断状态。

    并发编程的三大特性:

    • 原子性     即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    • 可见性    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    • 有序性     即程序执行的顺序按照代码的先后顺序执行;它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

    临界区:访问临界资源的代码段(特点:是一段供线程独占式访问的代码)。

    临界资源:某一时刻只能由一个线程访问的资源。

    synchronized关键字(可重入锁):

         synchronized 获取监视器锁,使得当前的某一段代码只能被独占式访问。

    synchronized同步代码块和同步方法的底层原理:

         synchronized同步方法,修饰一个普通方法,获取的是当前类对象的(this引用)的锁;修饰的静态(static)方法,获取的就是当前类的锁。

         静态常量池多了一个ACC_SYNCHRONIZED标识符,标识当前是一个同步方法,获取锁以及释放锁也是使用了monitorenter与monitorexit的字节码指令。

         synchronized同步代码块,Object obj = new Object();   synchronized(obj){}通过反编译可以看出,访问同步代码块会获取到monitorenter,出了同步代码块的右括号,就会有一个monitorexit字节码指令。

    monitor  lock

    每一个Java对象天生都带有一把锁(monitor  lock),一个monitor lock只能被一个线程在某一时刻内获得,通过synchronized关键字就是去获取这一把锁,在一个线程尝试获取与对象关联的monitor lock的时候会有如下情况:

    1. monitorenter
    • 如果monitor的计数器为0,表示当前的monitor  lock么有被任何线程获取,被某个线程获取该锁以后,会对计数器进行加1操作,表示当前的线程是这个monitor  lock的所有者,这个已经拥有monitor  lock的线程想要再次拥有该锁,便会对monitor的计数器累加;
    • 如果monitor的计数器不为零,表示锁已经被其他线程所拥有,当前线程想要获取monitor  lock,会被阻塞,直到计数器为零,然后再次尝试获取monitor  lock的使用权;

             2.monitorexit

    • 释放对monitor  lock的所用权,将计数器减1,如果计数器为0,表示当前的线程不在拥有该锁的所有权。

    锁的升级过程?

            JDK 1.5以后对synchronized做了优化,对象的对象头的Mark Word的不同的锁状态下存储的内容不同:

    无锁:普通的对象,Mark Word记录的是对象的hashCode值,锁的标志位为01,是否偏向位为0;

    偏向锁:当对象获取到同步锁,Mark Word记录的是当前线程的ID,锁的标志位为01,是否偏向位为1,表示进入到偏向锁模式,此时若有线程再去尝试获取锁,JVM发现当前锁是偏向模式,Mark Word记录的  不是此线程的ID,便会采用CAS操作尝试去获取当前这把锁,有可能成功有可能失败,失败的话表示抢锁失败,竞争比较激烈,此时就会升级到轻量级锁;

    CAS是一个原子操作(不可中断),其中含有三个值(内存位置值V,期望的值A,要修改的值B),首先判断内存V所保存的值与所期望的值A是否相等,如果相等,则可以修改。

    轻量级锁:JVM会在当前线程的线程栈中开辟一 块内存空间lock record,里面保存指向要获取的锁的Mark Word的引用,同时在要获取锁的Mark  Word保存指向lock record,这两个保存操作(CAS操作)成        功,代表线程抢到了轻量级锁,如果失败,则会进行自旋操作循环一定次数,如果还是失败,表示竞争更加激烈,继续锁升级;

    volatile关键字:保证有序性,可见性(修饰共享资源的共享变量),不能保证原子性;在Java的内存模型(JMM)中,每个线程都有自己的工作内存,当操作一个变量时,需要在主内存拷贝到自己的工作内存中,操作完成后再写入到主内存中,在并发情况下,该过程并不能保证可见性(即每个线程内部的修改对其他线程不可见),所以就需要volatile。要保证线程安全,需要保证并发的三个条件(volatile+synchronized / volatile+CAS)。

          i++的操作不是原子操作,他是分为三步的获取 i,执行 i+1操作,赋值给 i。当多个线程同时操作时,会出现写覆盖的问题。要实现 i++操作的线程安全,当然可以加上synchronize锁,但是这是一个重量级操作消耗性能,就可以用JUC下的atomicInteger类。

    volatile关键字修饰的变量

    • 写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量刷回到主内存。
    • 读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

    被volatile关键字修饰的变量会有一个lock前缀,即内存屏障。

    • 确保指令重排序不会将后面的代码排在内存屏障的前面;
    • 确保指令重排序不会将前面的代码排在内存屏障的后面;
    • 强制的将工作内存中的值刷新到主内存中;
    • 如果是写操作,则会导致其他工作内存中缓存的数据失效。

    悲观锁和乐观锁?

    • 悲观锁    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
    • 乐观锁   总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    公平锁和非公平锁

    • 公平锁  是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
    • 非公平锁   是指多个线程获取锁的顺序并不是按照申请锁的顺字,有可能后申请的线程比先申请的线程优先获取锁在高并发的情况下,有可能会造成优先级反转或者饥饿现象。非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

    可重入锁(又叫递归锁):线程可以进入任何一个它已经拥有的锁的同步着的代码块。也就是同一个线程在外层方法获取锁的时候,在进入该线程的内层方法会自动获取锁(前提是锁对象是同一个对象),不会因为之前已经获取而么有释放而阻塞<同一个线程可以多次获取同一个锁对象>。(ReentrantLock/Synchornized默认都是非公平可重入的)。

    import java.util.concurrent.TimeUnit;
    
    public class Test02 {
        public synchronized void init(){
            System.out.println(Thread.currentThread().getName()+"代码块1");
            init1();
        }
    
        private synchronized void init1() {
            System.out.println(Thread.currentThread().getName()+"代码块2");
        }
    
        public static void main(String[] args) {
            Test02 test02 = new Test02();
            new Thread(()->{
                test02.init();    //可以进入到另一个同步代码块
            },"A").start();
    
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(()->{
                test02.init();
            },"B").start();
        }
    }

    自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    
    public class Test02 {
    
        //原子引用类
        AtomicReference<Thread> atomicReference = new AtomicReference();
        public void myLock(){
            Thread thread = Thread.currentThread();
            System.out.println(Thread.currentThread().getName()+"进来了...");
            //自旋的CAS操作,期望值是null,说明还么有线程进来获取到(首次一定是null)
            while (!atomicReference.compareAndSet(null,thread)){   //加锁操作
    
            }
            //能出while循环,说明cas操作成功
            System.out.println(Thread.currentThread().getName()+"获取锁");
        }
        public void myUnLock(){
            Thread thread = Thread.currentThread();
            atomicReference.compareAndSet(thread,null);   //释放锁
            System.out.println(Thread.currentThread().getName()+"释放锁...");
        }
        public static void main(String[] args) {
            Test02 test02 = new Test02();
            new Thread(()->{
                test02.myLock();   //获取锁
                try {
                    TimeUnit.SECONDS.sleep(5);  //睡眠5秒钟
                } catch (InterruptedException e) {
    
                }
                test02.myUnLock();   //释放锁
            },"A").start();
    
            try {
                TimeUnit.SECONDS.sleep(1);  //主线程睡眠,让后面的线程先执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(()->{
                test02.myLock();   //获取锁
                test02.myUnLock();
            },"B").start();
        }
    
    }

    独占锁与共享锁

    • 独占锁:指该锁一次只能被一个线程所持有。对ReentrantL ock和Synchronized而言都是独占锁
    • 共享锁:指该锁可被多个线程所持有。对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
    CAS的ABA问题?
            假如线程1要修改主内存的值A,但是在这之前已经有线程2将主内存的值改为B,这时又来了线程3将主内存的值改为了A,但是线程1并不会发现这些问题,在这之前已经做出了修改,需要版本号来解决。

    CAS的问题?

    • ABA;
    • 自旋CAS如果长时间不成功,会给CPU带来很大的开销,居高不下;
    • 只能保证一个共享变量的原子操作。

    ReentrantLock

          Lock接口下实现类,提供了一种可定时的,可轮询的以及可中断的锁获取操作。默认的是非公平锁,构造函数传入true是公平性锁。

    • Lock() 加锁  unLock() 解锁。Lock接口中的方法lock() /unlock()/ trylock() /lockInterruptily() /trylock(long time,TimeUnit unit) 方法。

    LockSupport

         是一个类,用于创建锁和其他同步类的基本线程阻塞原语。线程等待唤醒机制wait/notify的优化。该类中的park(),unpark()分别是线程阻塞和解除线程阻塞的

    • LockSupport类中使用了一种名为Permit (许可)的概念来做到阻塞和唤醒线程的功能,每 个线程都有一个许可(permit),permit只有两个值1和零默认是零。可以把许可看成是一种(0—1)信 号量( Semaphore),但与Semaphore不同的是,许可的累加上限是1。
    • permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。
    • 调用unpark(thread)方法后,就会将thread线程的许可证permit设置为1(注意多次调用unpark方法,permit的值仍是1,不会累加)会自动唤醒thread线程,即之前阻塞的LockSupport.park()方法会立即返回。
    • 可以先调用unpark(),然后在调用park(),也会被唤醒。
    • 相较于Synchronized里面的wait(),notify()都需要在同步代码块或者同步方法中,否则会抛出IllegalMonitorStateException的异常,并且必须是先阻塞才能被唤醒,(如果在两个线程中,先执行notify()唤醒操作,在执行wait()阻塞方法,阻塞的线程无法被唤醒,程序无法正常执行),ReentrantLock中的awiati(),signal()方法必须在lock(),unLock()的块中,否则也会抛出IllegalMonitorStateException的异常,(如果在两个线程中,先执行signal()唤醒操作,在执行await()阻塞方法,阻塞的线程无法被唤醒,程序无法正常执行)。

    AQS

    https://www.cnblogs.com/128-cdy/p/13964827.html

    CountDownLatch   

            让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。.

    假如每次放学都需要值日生最后关闭教室门,每次关门都需要等其他同学都出教室才能关门:
    import java.util.concurrent.CountDownLatch;
    
    public class CountDown {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(7);  //七个同学
            for (int i = 0; i < 7; i++) {
                new Thread(()->{
                    System.out.println("同学"+Thread.currentThread().getName()+"走出教室");
                    countDownLatch.countDown();
                },String.valueOf(i)).start();
            }
            countDownLatch.await();  //只有当计数器为零,才会结束掉await方法,其他的线程执行
            System.out.println("值日生"+Thread.currentThread().getName()+"锁门");
        }
    }

    CyclicBarrier    与上面的相反,是从0开始做加法到指定的数据。

    Semaphore   信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。(抢车位)

    自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在底层实现好了。自定义同步器实现的时候主要实现下面几种方法:

    • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
    • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
    • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
    • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
    • ReentrantLock为例,(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
    • 注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。

    Synchornized和Reentrantlock区别?

    • 直观上来看synchornized是一个关键字,而Reentrantlock是一个类;
    • synchornized获取的是内置的monitor锁,不需要手动释放锁,而Reentrantlock加锁时需要new Reentrantlock()的对象,通过对象.lock()获取到独占锁,并且lock()和unlock()要成对出现;
    • Reentrantlock相比于synchornized更加的灵活,等待可中断(调用tryLock(long  timeout,TimeUnit uni)设置超时时间、通过lock.lockinterruptibly()调用interrupt()方法可中断),只有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,相当于synchornized来说可以避免出现死锁的情况。通过lock.lockinterruptibly()来实现这个机制。
    • synchornized锁非公平锁,ReentrantLock默认的构造函数是创建非公平锁,可以通过参数true设为公平锁,但非公平锁的性能更好。一个ReentrantLock对象可以同时绑定多个条件Condition,用来实现分组唤醒需要唤醒的线程,做到精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

    Reentrantlock获取锁和释放锁的过程:

    获取锁:

    • 非公平性锁中,当前线程会通过CAS操作来抢占锁,抢占成功修改锁的状态state为1 ,并且将线程的信息记录到锁当中( setExclusiveOwnerThread(Thread.currentThread()) );
    • 抢占不成功,则进入队列acquire(1),tryAcquire()尝试着去获取锁,如果成功则返回,否则以当前线程作为一个节点插入到一个AQS的队列的尾部;
    • 公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。所以,在第一部尝试获得锁的时候需要去判断有没有队列,而不是直接去抢占。

    释放锁:

            获取锁的状态值,判断当前释放锁的线程与锁中记录的线程信息是否一致,不一致就会抛出异常,如果一致,则判断锁的状态位是否为0,如果为0,则表示锁不在被占用,将锁中的信息清除掉。

    什么是死锁?

            在并发条件下,因为抢夺资源而造成的一种相互等待的现象,若无外力作用下,程序无法继续推进的情况。

    class Demo implements Runnable{
        private String lock1;
        private String lock2;
        public Demo(String lock1,String lock2){
            this.lock1 = lock1;
            this.lock2 = lock2;
        }
    
        @Override
        public void run() {
            synchronized (lock1){
                System.out.println(Thread.currentThread().getName()+"获取到"+lock1+"   尝试获取"+lock2);
                synchronized (lock2){
    
                }
            }
        }
    }
    public class DieSync {
        public static void main(String[] args) {
             String lock1 = "lock1";
             String lock2 = "lock2";
    //两个线程
             new Thread(new Demo(lock1,lock2),"A").start();
            new Thread(new Demo(lock2,lock1),"B").start();
        }
    }

    jps -l命令查看发生死锁的进程,然后通过  jstack 进程号  查看其堆栈信息。

    死锁的四个必要条件:

    • 互斥条件一个资源只能被一个进程占用。
    • 请求与保持    一个进程因为请求资源而阻塞时,对已获得的资源保持不放。
    • 不可剥夺   进程已经获得的资源在没有使用完之前,不能强行剥夺。
    • 循环等待    若干进程之间形成一种头尾相接的循环等待资源关系。

    常见死锁:

    • 交叉锁可能导致死锁的发生       threadA: A获取R1的锁等待获取R2的锁    threadB: B获取R2的锁等 待获取R1的锁
    • 内存不足    并发的请求系统可用的内存资源,系统资源不足可导致死锁
    • 一问一答的交互

    死锁的避免:

    • 打破互斥条件    运行多个线程同时访问某些资源
    • 打破请求和保持条件    资源的预分配策略(银行家算法)
    • 打破不可抢占    允许线程强行从资源占有者手中去剥夺某些资源
    • 打破循环等待     实现资源有序分配策略(银行家算法)

    死锁的检测与恢复:

    • 剥夺资源    剥夺某些现成的资源
    • 撤销线程    撤销代价最小的线程

    线程间的通信:

    • synchronized加锁的线程0bject类 中wait()/notify()/notifyAll()
    • ReentrantLock类加锁的线程Condition类中的await()/signal()/signalAll()

    为什么wait/notify/notifyAll是Object中的方法而不是Thread里的?

           这与他们使用的场景有关,他们使用在synchronized的同步块中,获取的是对象的monitor,是要用对象操作,而不是用线程去操作。

    wait/notify/notifyAll方法的作用?

           wait()方法会使得线程进入到等待阻塞状态,并且释放掉synchronized的锁,进入到对象的等待池中;notify()会唤醒一个调用wait()方法的等待线程,在使用前,线程必须要去拥有调用该方法的对象的锁,如果未获取到该对象的锁,则会进入到锁池中;notifyAll()是唤醒所有的调用wait()方法的等待线程。

    锁池与等待池:

    锁池: 假设thread A已经拥有了某个对象的锁,而其他的线程想要获取当前的对象的锁(进入当前对象的synchornized方法或者synchronzied代码块中),这些线程就进入到当前这个对象的锁池。

    等待池:假设thread A调用某个对象的wait ()方法,thread A就会释放该对象的锁,进入到该对象的等待池中。

    notify/notifyAll方法的区别?

          notify方法会唤醒一个等待线程,但是notifyAll会唤醒所有的等待线程;唤醒一个线程就不会导致等待池中的线程去竞争锁,而唤醒所有线程会导致线程竞争锁,会导致线程流入到锁池中。

     生产者消费者模型(代码)

    public class ProductConsumerTest {
        public static int count = 0;
        private  static final int Max = 5;
        private static String lock= "Lock";
        public static void main(String[] args) {
            new Thread(new product()).start();
            new Thread(new consumer()).start();
        }
        static class product implements Runnable{
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    synchronized (lock){
                        while (count == Max){
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        count++;
                        System.out.println("生产者"+Thread.currentThread().getName()+"生产"+count);
                        lock.notifyAll();
                    }
                }
            }
        }
    
        static  class consumer implements Runnable{
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    synchronized(lock){
                        while (count == 0){
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("消费者"+Thread.currentThread().getName()+"消费"+count);
                        count--;
                        lock.notifyAll();
                    }
                }
            }
        }
    }

    Condition

    Condition是一个接口,基本的方法await()/signal()/signalAll(),并且Condition依赖于Lock接口,采用Lock . newCondition () 生成一个Condition的对象。

    await()方法为什么要绑定到Condition对象上,一个ReentrantLock可以绑定多个condition对象有什么作用?

            wait()在synchronized同步的对象中使用一个队列;但await()在Lock子类以newCondition方法创建多个等待队列,一个锁有多个等待队列,可以提高效率,这也就是ReentrantLock较于synchronized效率高的原因,绑定condition对象用于线程间通信。在多个线程中想要唤醒某个指定的线程,便需要多个condition对象。

    阻塞队列 (当阻塞队列是时,从队列中获取元素的操作将会被阻塞。当阻塞队列是时,往队列里添加元素的操作将会被阻塞)

    Java中常用的阻塞队列

    • ArrayBlockingQueue     数组构成的有界阻塞队列,生产者消费者模型中,共用同一个锁对象。
    • LinkedBlockingQueue    链表构成的有界(但大小默认值为Integer.MAX VALUE)阻塞队列,生产者消费者模型中,生产者和消费者使用了两个锁,可以做到并行的操作数据。
    • SynchrousQueue    只作为中转者,不存放对象,天然实现生产者消费者模型。
    • DelayQueue            使用优先级队列实现的延迟无界阻塞队列,元素只有当指定的延迟时间到了,才能够从队列中获取元素。
    • PriorityBlockingQueue    基于优先级的无界阻塞队列,优先级的判断需要通过构造函数传入的Compartor对象决定,其不会阻塞生产者线程,而只会在没有数据可消费时,阻塞消费者线程。

    线程池本质是:实现了对线程的复用

    为什么需要线程池?

        多线程的缺点:

    1. 线程是不能无限创建了,当线程创建数量比较多的时候,会影响系统的性能;
    2. 并且线程的创建和销毁是会消耗一定的时间;
    3. 线程需要占用内存资源,大量的线程会导致OOM;
    4. 大量的线程回收会给GC带来很大的压力;
    5. 线程抢占资源会导致线程的上下文切换,上下文切换是很耗时的;

    线程池的优势:

    1. 线程池是指实现创建若干个可执行的线程放入一个池子里,当前有任务需要执行的时候,便从池子中获取空闲的线程来执行任务,任务执行完后再放回池子中,可以减少线程创建和销毁的开销,降低资源的消耗,提高响应速度,统一的管理线程;
    2. 实现了线程的复用。

    线程池的继承关系:

     对于工厂类Executors提供一些静态方法创建线程。

    两种方式提交任务:

    1. Executor接口下的,使用void execute(Runnable command)方法提交任务execute方法返回类型为void,所以没有办法判断任务是否被线程池执行成功。
    2. submit(Runnable command) / submit(Runnable command,T result) / submit(Callable<T> task),可以看出submit()方法既可以提交Runnable线程,也可以提交Callable线程,使用submit()方法提交任务可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

    ExecutorService接口shutdown()/ shutdownNow()/isShutDown()

    线程池七大参数

    1. corePoolSize 线程池的常驻核心线程数
    2. maximumPoolSize  线程池所能容纳的最大线程数
    3. keepAliveTime  多于空闲线程的存活时间,当前线程池数量超过corePoolsize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
    4. unit  keepAliveTime的时间单位
    5. workQueue  任务队列,被提交但是未被执行的任务
    6. threadFactory  表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
    7. handler  拒绝策略

    合理配置线程池

    首先通过Runtime.getRuntime().availableProcessors()查看当前服务器的CPU核数。

    • CPU密集型    CPU密集的意思是该任务需要大量的运算,而没有阻寒,CPU一直全速运行。应该尽可能少的线程(一般公式:线程数=CPU核数+1个线程)。
    • IO密集型     IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2,或者(参考公式:线程数=CPU核数/(1-阻塞系数)   -> 阻塞系数为0.8~0.9 )。

    线程池任务提交执行的步骤:

    1. 提交一个任务,线程池里存活的核心线程数小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务
    2. 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
    3. 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没满,创建非核心线程并执行提交的任务。
    4. 如果当前的线程数达到了maxiPoolSize,还有新的任务过来的话,直接采用拒绝策略处理,抛出RejectedExecutionException异常。

    线程池种类

    •  newFixedThreadPool 可以生成固定大小的线程池,核心线程数等于最大线程数,阻塞队列为无界队列LinkedBlockingQueue,可能会导致OOM的异常;适用于处理CPU密集型的任务,适用执行长期的任务;
    •  newCachedThreadPool 可以生成一个无界、可以自动回收的线程池,核心线程数是0,最大线程数为int的最大值,回收线程时间为60s;阻塞队列是SynchronousQueue,如果放入队列后没有线程来取就放不进去,可能导致线程过多,cpu负担太大;适用于并发执行大量短期的小任务
    •  newSingleThreadExecutor 可以生成一个单个线程的线程池,核心和最大线程数都是1;阻塞队列是LinkedBlockingQueue;适用于串行执行任务的场景,一个任务一个任务地执行,顺序执行
    •  newScheduledThreadPool 还可以生成支持周期任务的线程池。阻塞队列是DelayedWorkQueue;周期性执行任务的场景,需要限制线程数量的场景

    任务拒绝策略

    当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

    1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认)
    2. ThreadPoolExecutor.DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的方案。
    3. ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
    4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务"调用者运行"一种调节机制,该策略既不会拋弃任务,也不会抛出异常,而是将某些任务回退到调用者。

    线程池的状态

    • Running       可以被切换到ShutDown或者Stop状态
    • ShutDown    不接受新的任务,但是能够处理已经添加的任务     调用shutDown()方法
    • Stop             不接受新任务,同时也不能够处理已经添加的任务     调用shutDownNow()方法
    • Tidying          当所有的任务都已经终止,ctI所记录的任务数量为0     调用terminated()方法
    • Teerminated     线程池终止
  • 相关阅读:
    日报10.29
    日报10.28
    日报10.27
    周进度总结-6
    日报10.25
    日报10.23
    日报10.22
    日报10.21
    日报10.18
    STL bitset
  • 原文地址:https://www.cnblogs.com/128-cdy/p/13545121.html
Copyright © 2020-2023  润新知