个人吐血系列-总结Java多线程(2)

849 阅读43分钟

这是承接上部分“个人吐血系列-总结Java多线程(1)”的文章哦,这是第二个部分,关于大纲图在第一部分。

并发集合容器

为什么说ArrayList线程不安全?

看add方法的源码:

public boolean add(E e) {  
  
    /**  
     * 添加一个元素时,做了如下两步操作  
     * 1.判断列表的capacity容量是否足够,是否需要扩容  
     * 2.真正将元素放在列表的元素数组里面  
     */
  
    ensureCapacityInternal(size + 1);  // Increments modCount!! // 可能因为该操作,导致下一步发生数组越界  
    elementData[size++] = e; // 可能null值  
    return true;  
}  

数组越界:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
  5. 线程B也发现需求大小为10,也可以容纳,返回。
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

null值情况:

elementData[size++] = e不是一个原子操作

  1. elementData[size] = e;
  2. size = size + 1;

逻辑

  1. 列表大小为0,即size=0
  2. 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
  3. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
  4. 线程A开始将size的值增加为1
  5. 线程B开始将size的值增加为2

「这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。」

解决非安全集合的并发都有哪些?

ArrayList->Vector->SynchronizedList->CopyOnWriteArrayList

ArraySet->SynchronizedSet->CopyOnWriteArraySet

HashMap->SynchronizedMap->ConcurrentHashMap

并发同步容器

AQS原理

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性  

状态信息通过 protected 类型的getStatesetStatecompareAndSetState进行操作

//返回同步状态的当前值  
protected final int getState() {  
        return state;  
}  
 // 设置同步状态的值  
protected final void setState(int newState) {  
        state = newState;  
}  
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)  
protected final boolean compareAndSetState(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);  
}  

AQS 定义两种资源共享方式:

  1. Exclusive(独占)只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁。

    总结:公平锁和非公平锁只有两处不同:

    1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
    2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
  2. Share(共享)多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、 CyclicBarrier、ReadWriteLock 。

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。  
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。  
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。  
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。  
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。  

CountDownLatch

CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown方法时,其实使用了tryReleaseShared方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。

三种用法

  1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown(),当计数器的值变为 0 时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
  3. 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
public class CountDownLatchDemo {  
  
    public static void main(String[] args) throws InterruptedException {  
        countDownLatchTest();  
//        general();  
    }  
  
    public static void general() {  
        for (int i = 0; i < 6; i++) {  
            new Thread(() -> {  
                System.out.println(Thread.currentThread().getName() + " 上完自习,离开教师");  
            }, "Thread --> " + i).start();  
        }  
        while (Thread.activeCount() > 2) {  
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }  
            System.out.println(Thread.currentThread().getName() + " ====班长最后走人");  
        }  
    }  
  
    public static void countDownLatchTest() throws InterruptedException {  
        CountDownLatch countDownLatch = new CountDownLatch(6);  
        for (int i = 0; i < 6; i++) {  
            new Thread(() -> {  
                System.out.println(Thread.currentThread().getName() + " 上完自习,离开教师");  
                countDownLatch.countDown();  
            }, "Thread --> " + i).start();  
        }  
        countDownLatch.await();  
        System.out.println(Thread.currentThread().getName() + " ====班长最后走人");  
    }  
}  

CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

public class CyclicBarrierDemo {  
    public static void main(String[] args) {  
        cyclicBarrierTest();  
    }  
  
    public static void cyclicBarrierTest() {  
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {  
            System.out.println("====召唤神龙====");  
        });  
        for (int i = 0; i < 7; i++) {  
            final int tempInt = i;  
            new Thread(() -> {  
                System.out.println(Thread.currentThread().getName() + " 收集到第" + tempInt + "颗龙珠");  
                try {  
                    cyclicBarrier.await();  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                } catch (BrokenBarrierException e) {  
                    e.printStackTrace();  
                }  
            }, "" + i).start();  
        }  
    }  
}  

当调用 CyclicBarrier 对象调用 await() 方法时,实际上调用的是dowait(false, 0L)方法。 await() 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。

 // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。  
    private int count;  
    /**  
     * Main barrier code, covering the various policies.  
     */
  
    private int dowait(boolean timed, long nanos)  
        throws InterruptedException, BrokenBarrierException,  
               TimeoutException 
{  
        final ReentrantLock lock = this.lock;  
        // 锁住  
        lock.lock();  
        try {  
            final Generation g = generation;  
  
            if (g.broken)  
                throw new BrokenBarrierException();  
  
            // 如果线程中断了,抛出异常  
            if (Thread.interrupted()) {  
                breakBarrier();  
                throw new InterruptedException();  
            }  
            // cout减1  
            int index = --count;  
            // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件  
            if (index == 0) {  // tripped  
                boolean ranAction = false;  
                try {  
                    final Runnable command = barrierCommand;  
                    if (command != null)  
                        command.run();  
                    ranAction = true;  
                    // 将 count 重置为 parties 属性的初始化值  
                    // 唤醒之前等待的线程  
                    // 下一波执行开始  
                    nextGeneration();  
                    return 0;  
                } finally {  
                    if (!ranAction)  
                        breakBarrier();  
                }  
            }  
  
            // loop until tripped, broken, interrupted, or timed out  
            for (;;) {  
                try {  
                    if (!timed)  
                        trip.await();  
                    else if (nanos > 0L)  
                        nanos = trip.awaitNanos(nanos);  
                } catch (InterruptedException ie) {  
                    if (g == generation && ! g.broken) {  
                        breakBarrier();  
                        throw ie;  
                    } else {  
                        // We're about to finish waiting even if we had not  
                        // been interrupted, so this interrupt is deemed to  
                        // "belong" to subsequent execution.  
                        Thread.currentThread().interrupt();  
                    }  
                }  
  
                if (g.broken)  
                    throw new BrokenBarrierException();  
  
                if (g != generation)  
                    return index;  
  
                if (timed && nanos <= 0L) {  
                    breakBarrier();  
                    throw new TimeoutException();  
                }  
            }  
        } finally {  
            lock.unlock();  
        }  
    }

总结CyclicBarrier 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

Semaphore

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源

public class SemaphoreDemo {  
    public static void main(String[] args) {  
        Semaphore semaphore = new Semaphore(3);// 模拟三个停车位  
        for (int i = 0; i < 6; i++) { // 模拟6部汽车  
            new Thread(() -> {  
                try {  
                    semaphore.acquire();  
                    System.out.println(Thread.currentThread().getName() + " 抢到车位");  
                    // 停车3s  
                    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }  
                    System.out.println(Thread.currentThread().getName() + " 停车3s后离开车位");  
                } catch (Exception e) {  
                    e.printStackTrace();  
                } finally {  
                    semaphore.release();  
                }  
            }, "Car " + i).start();  
        }  
    }  
}  

阻塞队列

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列.
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列.
  • SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列.
  • LinkedBlockingDuque:由链表结构组成的双向阻塞队列.
  • 抛出异常方法:add remove
  • 不抛异常:offer poll
  • 阻塞 put take
  • 带时间 offer poll

生产者消费者

synchronized版本的生产者和消费者,比较繁琐

public class ProdConsumerSynchronized {  
  
    private final LinkedList<String> lists = new LinkedList<>();  
  
    public synchronized void put(String s) {  
        while (lists.size() != 0) { // 用while怕有存在虚拟唤醒线程  
            // 满了, 不生产了  
            try {  
                this.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        lists.add(s);  
        System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst());  
        this.notifyAll(); // 这里可是通知所有被挂起的线程,包括其他的生产者线程  
    }  
  
    public synchronized void get() {  
        while (lists.size() == 0) {  
            try {  
                this.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst());  
        this.notifyAll(); // 通知所有被wait挂起的线程  用notify可能就死锁了。  
    }  
  
    public static void main(String[] args) {  
        ProdConsumerSynchronized prodConsumerSynchronized = new ProdConsumerSynchronized();  
  
        // 启动消费者线程  
        for (int i = 0; i < 5; i++) {  
            new Thread(prodConsumerSynchronized::get, "ConsA" + i).start();  
        }  
  
        // 启动生产者线程  
        for (int i = 0; i < 5; i++) {  
            int tempI = i;  
            new Thread(() -> {  
                prodConsumerSynchronized.put("" + tempI);  
            }, "ProdA" + i).start();  
        }  
    }  
}  

ReentrantLock

public class ProdConsumerReentrantLock {  
  
    private LinkedList<String> lists = new LinkedList<>();  
  
    private Lock lock = new ReentrantLock();  
  
    private Condition prod = lock.newCondition();  
  
    private Condition cons = lock.newCondition();  
  
    public void put(String s) {  
        lock.lock();  
        try {  
            // 1. 判断  
            while (lists.size() != 0) {  
                // 等待不能生产  
                prod.await();  
            }  
            // 2.干活  
            lists.add(s);  
            System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst());  
            // 3. 通知  
            cons.signalAll();  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock();  
        }  
    }  
  
    public void get() {  
        lock.lock();  
        try {  
            // 1. 判断  
            while (lists.size() == 0) {  
                // 等待不能消费  
                cons.await();  
            }  
            // 2.干活  
            System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst());  
            // 3. 通知  
            prod.signalAll();  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock();  
        }  
    }  
  
    public static void main(String[] args) {  
        ProdConsumerReentrantLock prodConsumerReentrantLock = new ProdConsumerReentrantLock();  
        for (int i = 0; i < 5; i++) {  
            int tempI = i;  
            new Thread(() -> {  
                prodConsumerReentrantLock.put(tempI + "");  
            }, "ProdA" + i).start();  
        }  
        for (int i = 0; i < 5; i++) {  
            new Thread(prodConsumerReentrantLock::get, "ConsA" + i).start();  
        }  
    }  
}  

BlockingQueue

public class ProdConsumerBlockingQueue {  
  
    private volatile boolean flag = true;  
  
    private AtomicInteger atomicInteger = new AtomicInteger();  
  
    BlockingQueue<String> blockingQueue = null;  
  
    public ProdConsumerBlockingQueue(BlockingQueue<String> blockingQueue) {  
        this.blockingQueue = blockingQueue;  
    }  
  
    public void myProd() throws Exception {  
        String data = null;  
        boolean retValue;  
        while (flag) {  
            data = atomicInteger.incrementAndGet() + "";  
            retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);  
            if (retValue) {  
                System.out.println(Thread.currentThread().getName() + " 插入队列" + data + " 成功");  
            } else {  
                System.out.println(Thread.currentThread().getName() + " 插入队列" + data + " 失败");  
            }  
            TimeUnit.SECONDS.sleep(1);  
        }  
        System.out.println(Thread.currentThread().getName() + " 大老板叫停了,flag=false,生产结束");  
    }  
  
    public void myConsumer() throws Exception {  
        String result = null;  
        while (flag) {  
            result = blockingQueue.poll(2, TimeUnit.SECONDS);  
            if (null == result || result.equalsIgnoreCase("")) {  
                flag = false;  
                System.out.println(Thread.currentThread().getName() + " 超过2s没有取到蛋糕,消费退出");  
                return;  
            }  
            System.out.println(Thread.currentThread().getName() + " 消费队列" + result + "成功");  
        }  
    }  
  
    public void stop() {  
        flag = false;  
    }  
  
    public static void main(String[] args) {  
        ProdConsumerBlockingQueue prodConsumerBlockingQueue = new ProdConsumerBlockingQueue(new ArrayBlockingQueue<>(10));  
        new Thread(() -> {  
            System.out.println(Thread.currentThread().getName() + " 生产线程启动");  
            try {  
                prodConsumerBlockingQueue.myProd();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }, "Prod").start();  
  
        new Thread(() -> {  
            System.out.println(Thread.currentThread().getName() + " 消费线程启动");  
            try {  
                prodConsumerBlockingQueue.myConsumer();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }, "Consumer").start();  
  
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }  
        System.out.println("5s后main叫停,线程结束");  
        prodConsumerBlockingQueue.stop();  
    }  
}  

线程池

线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

FixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现:

 /**  
  * 创建一个可重用固定数量线程的线程池  
  */
  
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {  
        return new ThreadPoolExecutor(nThreads, nThreads,  
                                      0L, TimeUnit.MILLISECONDS,  
                                      new LinkedBlockingQueue<Runnable>(),  
                                      threadFactory);  
    }

从上面源代码可以看出新创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的

  1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
  2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue
  3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

不推荐使用FixedThreadPool使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响:

  1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
  2. 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 被设置为同一个值。
  3. 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
  4. 运行中的 FixedThreadPool(未执行 shutdown()shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。

SingleThreadExecutor

 /**  
 *返回只有一个线程的线程池  
 */
  
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {  
        return new FinalizableDelegatedExecutorService  
            (new ThreadPoolExecutor(11,  
                                    0L, TimeUnit.MILLISECONDS,  
                                    new LinkedBlockingQueue<Runnable>(),  
                                    threadFactory));  
    }

和上面一个差不多,只不过core和max都被设置为1

CachedThreadPool

 /**  
 * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。  
 */
  
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {  
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                      60L, TimeUnit.SECONDS,  
                                      new SynchronousQueue<Runnable>(),  
                                      threadFactory);  
    }

CachedThreadPoolcorePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

  1. 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
  2. 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;

ScheduledThreadPoolExecutor省略,基本不会用

ThreadPoolExecutor(重点)

 /**  
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。  
 */
  
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量  
                              int maximumPoolSize,//线程池的最大线程数  
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间  
                              TimeUnit unit,//时间单位  
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列  
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可  
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务  
                               )
 
{  
        if (corePoolSize < 0 ||  
            maximumPoolSize <= 0 ||  
            maximumPoolSize < corePoolSize ||  
            keepAliveTime < 0)  
            throw new IllegalArgumentException();  
        if (workQueue == null || threadFactory == null || handler == null)  
            throw new NullPointerException();  
        this.corePoolSize = corePoolSize;  
        this.maximumPoolSize = maximumPoolSize;  
        this.workQueue = workQueue;  
        this.keepAliveTime = unit.toNanos(keepAliveTime);  
        this.threadFactory = threadFactory;  
        this.handler = handler;  
    }

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit: keepAliveTime 参数的时间单位。
  • threadFactory** :executor 创建新线程的时候会用到。
  • handler:饱和策略。关于饱和策略下面单独介绍一下.
线程池各个参数的关系
线程池各个参数的关系

线程池各个参数的关系:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)

Executors 返回线程池对象的弊端如下

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPoolScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
public class ThreadPoolExecutorDemo {  
    public static void main(String[] args) {  
        ExecutorService threadpools = new ThreadPoolExecutor(  
                3,   
                5,   
                1l,  
                TimeUnit.SECONDS,  
                new LinkedBlockingDeque<>(3),  
                Executors.defaultThreadFactory(),  
                new ThreadPoolExecutor.AbortPolicy());  
//new ThreadPoolExecutor.AbortPolicy();  
//new ThreadPoolExecutor.CallerRunsPolicy();  
//new ThreadPoolExecutor.DiscardOldestPolicy();  
//new ThreadPoolExecutor.DiscardPolicy();  
        try {  
            for (int i = 0; i < 8; i++) {  
                threadpools.execute(() -> {  
                    System.out.println(Thread.currentThread().getName() + "\t办理业务");  
                });  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            threadpools.shutdown();  
        }  
    }  
}  

Java锁机制

公平锁/非公平锁

公平锁指多个线程按照申请锁的顺序来获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象(很长时间都没获取到锁-非洲人...),ReentrantLock,了解一下

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下

synchronized void setA() throws Exception {  
   Thread.sleep(1000);  
   setB(); // 因为获取了setA()的锁,此时调用setB()将会自动获取setB()的锁,如果不自动获取的话方法B将不会执行  
}  
synchronized void setB() throws Exception {  
   Thread.sleep(1000);  
}  

独享锁/共享锁

  • 独享锁:是指该锁一次只能被一个线程所持有。
  • 共享锁:是该锁可被多个线程所持有。

互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是其具体的实现

乐观锁/悲观锁

  1. 乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待兵法同步的角度。
  2. 悲观锁认为对于同一个人数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出现问题
  3. 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作时没有事情的
  4. 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁带来大量的性能提升
  5. 悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子类操作的更新。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁

分段锁

  1. 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作
  2. 以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)
  3. 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计
  4. 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作

偏向锁/轻量级锁/重量级锁

  1. 这三种锁是锁的状态,并且是针对Synchronized。在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价
  2. 偏向锁的适用场景:始终只有一个线程在执行代码块,在它没有执行完释放锁之前,没有其它线程去执行同步快,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;在有锁竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用
  3. 轻量级锁是指当锁是偏向锁的时候,被另一个线程锁访问,偏向锁就会升级为轻量级锁,其他线程会通过自选的形式尝试获取锁,不会阻塞,提高性能
  4. 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

自旋锁

  1. 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
  2. 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消
  3. 自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。
  4. 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适用使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费

Java锁总结

Java锁机制可归为Sychornized锁和Lock锁两类。Synchronized是基于JVM来保证数据同步的,而Lock则是硬件层面,依赖特殊的CPU指令来实现数据同步的

  • Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。
  • ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。
  • ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。

说明一下,由于掘进限制了文章的长度或者字数,因此,我将这篇文章拆分了两部分,这是第二部分;关注我获取第一部分哦

创作不易哇,觉得有帮助的话,给个小小的star呗。github地址😁😁😁