java多线程安全

一、volatile

欢迎访问个人网站

1. volatile是java虚拟机提供的轻量级的同步机制

保证可见性、不保证原子性、禁止指令重排

2. JMM

2.1 可见性
2.2 原子性

不可分割,完整性,也即某个正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。number++在多线程下时非安全的,如何不加synchronized解决使用原子变量AtomicInteger、使用锁

2.3 有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为三种
在这里插入图片描述
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
处理在进行重排序时必须要考虑指令间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测

  • 重排1
    java多线程安全_第1张图片
class  ReSortSeqDemo{
    int a = 0;
    boolean flag = false;
    public void method01(){
        a = 1;
        flag = true;
    }
    //多线程环境中线程交替执行,由于编译器优化重排的存在,
    //两个线程中使用的变量能否保持一致性时无法确定的,结果无法预测
    public void method02(){
        if (flag) {
            a = a+5;
            System.out.println("******retValue" + a);
        }
    }
}
  • 重排小结
    volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
    先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个cpu指令,他的作用有两个:
    一是保证特定操作的执行顺序
    二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
    由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉编译器和cpu,不管什么指令都不能和这条Memory Barrier指令重排序,
    也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种cpu的缓存数据,因此任何cpu上的线程都能读取到这些数据的最新版本
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量赋值刷新到主内存 对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存读取共享变量
java多线程安全_第2张图片 java多线程安全_第3张图片

工作内存与主内存同步延迟现象导致的可见性问题可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其它线程可见。对于指令重排导致的可见性问题和有序性问题,可以利用volatile的另外一个作用就是cpu禁止指令重排

3. 哪些地方用volatile

单例模式DCL(双重检验机制)
代码

class SingleonDemo {
    private static volatile SingleonDemo instance = null;

    private SingleonDemo() {             System.out.println(Thread.currentThread().getName());
    }

    public static SingleonDemo getInstance() {
        //双重校验机制
        if (instance == null) {
            synchronized (SingleonDemo.class) {
                if (instance == null) {
                    instance = new SingleonDemo();
                }
            }
        }
        return instance;
    }
}

等等。。。。

二、CAS

1. 比较并交换

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(5);
    atomicInteger.compareAndSet(5,10);
    atomicInteger.compareAndSet(5,1024);
    System.out.println(atomicInteger.get());
}

####2. CAS底层原理?以及对UnSafe的理解
#####2.1 atomicInteger.getAndIncrement()
atomicInteger.getAndIncrement()方法源码:

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

引出来的一个问题:UnSafe类是什么?

2.2 UnSafe

java多线程安全_第4张图片
a. UnSafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。
Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖与UnSafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务。
b. 变量valueOffset,表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
java多线程安全_第5张图片
c. 变量value用volatile修饰,保证了多线程之间的内存可见性。

2.3 CAS是什么

CAS的全称为Compare-And-Swap,他是一条cpu并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。 CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮助我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的过程,并且原始的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

var1 AtomicInteger对象本身。
var2 该对象值的引用地址。
var4 需要变动的数量。
var5 是用var1、var2找出的内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并返回true,
如果不同,继续取值然后再比较,直到更新完成。
#####2.4 CAS缺点
a. 循环时间长开销大
java多线程安全_第6张图片
我们可以看到getAndAddInt方法执行时,有个do while
如果失败,会一直进行尝试。如果长时间不成功,可能给CPU带来很大开销。
b. 只能保证一个共享变量的原子操作
c. ABA问题(原子引用说明)
####3. 原子类Atomiclnteger的ABA问题,原子更新引用
#####3.1 ABA产生原因
CAS算法实现一个重要前提需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据变化。比如一个线程一从内存位置V中取出A,这时另外一个线程二也从内存中取出A值,并且线程二进行了一些操作将值变成了B,然后线程二又将V位置的值变回为A,这时线程一进行CAS操作发现内存中V的值仍然是A,然后线程一操作成功。尽管线程一的CAS操作成功,但不代表这个过程就没问题。

3.2 原子引用

java多线程安全_第7张图片
java多线程安全_第8张图片

3.3 时间戳的原子引用

java多线程安全_第9张图片
java多线程安全_第10张图片

三、线程安全

3.1 ArrayList线程不安全及其他集合延申
/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/18 15:34
 * @website https://www.zhangguimin.cn
 */
public class CollectionSafety {
    public static void main(String[] args) {
        //List list = new ArrayList<>();
        //List list = new Vector<>();
        //List list = Collections.synchronizedList(new ArrayList<>());
        //List list = new CopyOnWriteArrayList<>();
        Set<String> set = new CopyOnWriteArraySet<>();
        //Map map = new ConcurrentHashMap<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
        /**
         * 1 故障现象
         *      java.util.ConcurrentModificationException
         * 2 导致原因
         *      并发争抢修改导致
         * 3 解决方案
         *      3.1 new Vector<>();
         *      3.2 Collections.synchronizedList(new ArrayList<>());
         *             ...Collections.synchronizedMap(new HashMap<>());
         *             ...Collections.synchronizedSet(new HashSet<>());
         *      3.3 new CopyOnWriteArrayList<>();
         *             ...Set set = new CopyOnWriteArraySet<>();
         *             ...Map map = new ConcurrentHashMap<>();
         * 4 优化建议
         */

    }
}
3.2 公平锁/非公平锁/可重入锁/递归锁/自旋锁
  • 公平和非公平锁
  1. 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队,先来后到。
  2. 非公平锁: 指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
  3. 两者区别
    相同:公平锁/非公平锁,并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认为false非公平锁
    两者区别
    公平锁:每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就加入到等待队列中,会按照FIFO规则从队列中取到自己。
    非公平锁:上来就尝试占有锁,如果尝试失败,就采用类似公平锁方式。
  4. 题外话:Java Reentrantlock而言,通过构造函数指定该锁是否是公平锁,默认为false非公平锁,非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁
  • 可重入锁(递归锁)
  1. 是什么?
    可重入锁(也叫递归锁),指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进去内层方法会自动获取锁,也即,在线程可以进入任何一个它已经拥有的锁所同步着的代码块。
  2. ReentrantLock/Synchronized就是一个典型的可重入锁
  3. 可重入锁最大的作用是避免死锁
  • 自旋锁
  1. 是什么?
    自旋锁(spinlock):是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
    java多线程安全_第11张图片
/**
 * @author Mr. Zhang
 * @description 自旋锁
 * @date 2019/4/19 9:39
 * @website https://www.zhangguimin.cn
 */
public class SpinLockDemo {

    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        //System.out.println(thread.getName() + "\t come in");
        while (!atomicReference.compareAndSet(null, thread)) {
            System.out.println(thread.getName() + "\t get lock fail");
        }
        System.out.println(thread.getName() + "\t get lock success");
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + "\t myUnLock");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock();

        }, "AAA").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();
        }, "BBB").start();
    }

}

  • 独占锁(写锁)/共享锁(读锁)/互斥锁
  1. 是什么?
    独占锁:指该锁一次只能被一个线程锁持有。对ReentrantLock和Synchronized而言都是独占锁。
    共享锁:指该锁可以被多个线程持有。
    对ReentrantReadWriteLock其读锁是共享,其写锁是独占的。
    读锁的共享可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

  2. ReadWriteLockDemo

/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/19 10:46
 * @website https://www.zhangguimin.cn
 *
 * 多个线程同时读一个资源没任何问题,所以为了满足并发量,读取共享资源应该是可以同时进行。
 * 但是
 * 如果一个线程想去修改共享资源,就不应该再有其他线程可以对该资源进行读或写。
 * 小总结:
 *      读-读能共享
 *      读-写不能共享
 *      写-写不能共享
  *      写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
 */
public class ReadWriteLockDemo {
    /**
     * 读写锁
     */
    private volatile Map<Integer, Integer> map = new HashMap<>();
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public void put(Integer key, Integer val) {
        rwl.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始写入");
            map.put(key, val);
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + "写入完成");
        } catch (InterruptedException e) {

        } finally {
            rwl.writeLock().unlock();
        }
    }

    public void get() {
        rwl.readLock().lock();
        try {
            map.entrySet().forEach(p -> {
                System.out.println(Thread.currentThread().getName() + "开始读取:" + p.getValue());
            });
        } finally {
            rwl.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        for (int i = 0; i < 5; i++) {
            final int kv = i;
            new Thread(() -> {
                readWriteLockDemo.put(kv, kv);
            },"AA").start();
        }

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                readWriteLockDemo.get();
            },"BB").start();
        }
    }
3.3 CountDownLatch/CyclicBarrier/Semaphore
  • CountDownLatch
  1. 让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒
  2. CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其它线程调用countDown方法会将计数器减(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因调用await方法被阻塞的线程会被唤醒,继续执行。
  3. CountDownLatchDemo
/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/19 17:39
 * @website https://www.zhangguimin.cn
 */
public class CountDownLatchDemo {

    public static void main(String[] args) throws Exception {

        CountDownLatch latch = new CountDownLatch(6);
        List<Integer> list = Collections.synchronizedList(new ArrayList<>(6));

        for (int i = 0; i < 6; i++) {
            final Integer nu = i;
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(nu);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add(nu);
                latch.countDown();
                System.out.println(Thread.currentThread().getName() + "\t 分布计算:"+nu);
            }).start();

        }
        latch.await();
        System.out.println(Thread.currentThread().getName() + "\t 计算完毕"+list.stream().collect(Collectors.summingInt(Integer::intValue)));
    }
}


/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/19 17:39
 * @website https://www.zhangguimin.cn
 */
public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(6);

        for (int i = 1; i <=6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "国,被灭");
                latch.countDown();
            }, CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
        }
        latch.await();
        System.out.println(Thread.currentThread().getName() + "\t***************秦灭六国,统一华夏");

    }
}

enum CountryEnum {

    ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");

    private Integer retCode;
    private String retMessage;

    CountryEnum(Integer retCode, String retMessage) {
        this.retCode = retCode;
        this.retMessage = retMessage;
    }

    public Integer getRetCode() {
        return retCode;
    }

    public String getRetMessage() {
        return retMessage;
    }

    public static CountryEnum forEach_CountryEnum(int index) {
        CountryEnum[] values = CountryEnum.values();
        for (CountryEnum value : values) {
            if (index == value.getRetCode()) {
                return value;
            }
        }
        return null;
    }
}
  • CyclicBarrier
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/23 10:15
 * @website https://www.zhangguimin.cn
 */
public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("********召唤神龙");
        });
        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "\t收集到第:" + temp + "龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
  • Semaphore
  1. 是什么?
    信号量主要用于两个目的,一个是用于多资源的互斥使用,另一个用于并发线程数控制。
  2. SemaphoreDemo
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/23 10:43
 * @website https://www.zhangguimin.cn
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t抢到车位");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName() + "\t停车3s后离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //释放资源
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}
3.4 阻塞队列
  • 队列+阻塞
    java多线程安全_第12张图片
    当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
    当阻塞队列时满时,往队列里添加元素的操作将会被阻塞。
    试图从控阻塞队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新元素。
    同理
    视图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增。
  • 为什么用?有什么好处?
    多线程领域:所谓阻塞,在某些情况下会挂起(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
    为什么需要BlockingQueue?
    好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQuequ都给你一手包办了,在concurrent包发布前,在多线程环境下,我们每个程序员都必须去处理控制这些细节,由器还要兼顾效率和线程安全,而这会给我们程序带来不小的复杂度。
  • BlockingQueue的核心方法
    java多线程安全_第13张图片
抛出异常 当阻塞队列满时,再往队列里add插入元素会抛出java.lang.IllegalStateException: Queue full异常当阻塞队列空时,再往队列里remove移除元素会抛出NoSuchElementException
特殊值 插入方法,成功ture失败false移除方法,成功放回队列的元素,队列里没有就返回null
一直阻塞 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出,当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超时后生产者线程会退出
  • 架构梳理+种类分析
  1. 架构介绍
  2. 种类分析
    ArrayBlockingQueue:由数组结构组成的有界阻塞队列
    LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
    PriorityBlockingQueue:支持优先级排序的无界阻塞队列
    DelayQueue:使用优先级队列实现的延迟无界阻塞队列
    SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
    LinkedTransferQueue:由链表结构组成的无界阻塞队列
    LinkedBlockingDeque:由链表结构组成的双向阻塞队列
  • 用在哪里
    生产者消费者模式(阻塞队列版)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/24 16:13
 * @website https://www.zhangguimin.cn
 */
public class ProdConsumerBlockQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        MyResource resource = new MyResource(new ArrayBlockingQueue<>(10));

        new Thread(() -> {
            try {
                resource.myProduct();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "prod").start();

        new Thread(() -> {
            try {
                resource.myConsumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Consumer").start();
        TimeUnit.SECONDS.sleep(5);
        resource.stop();
    }
}

class MyResource {
    private volatile Boolean flag = true;
    private AtomicInteger atomicInteger = new AtomicInteger();
    private BlockingQueue<String> blockingQueue = null;

    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    public void myProduct() throws InterruptedException {
        String data = null;
        boolean retValue;
        while (flag) {
            data = atomicInteger.incrementAndGet() + "";
            retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if (retValue) {
                System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t停止生产,flag=" + flag);
    }

    public void myConsumer() throws InterruptedException {
        String result = null;
        while (flag) {
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if (result == null || "".equals(result)) {
                flag = false;
                System.out.println(Thread.currentThread().getName() + "\t超过2s未取到,退出");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t消费队列" + result + "成功");
            TimeUnit.SECONDS.sleep(1);
        }
    }

    public void stop() {
        this.flag = false;
    }
}
3.5 线程池和ThreadPoolExecutor
  • 为什么要用线程池?
    线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
    主要特点:线程复用;控制最大并发数;管理线程。
    第一:降低资源消耗。通过重复利用已经创建的线程降低创建和销毁造成的消耗。
    第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立刻执行。
    第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一的分配,调用和监控
  • 线程池如何使用
  1. 架构说明
    Java中线程池是通过Executor框架实现的,该框架中用到Excutor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
    java多线程安全_第14张图片

  2. 编码实现

    • Executors.newFixedThreadPool(int):执行长期任务,性能好很多
      在这里插入图片描述
      主要特点如下:
      1、创建一个定长线程池,可控制线程最大并发数,超出的线程会再队列中等待。
      2、newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
    • Executors.newSingleThreadExecutor():一个任务一个任务执行的场景
      java多线程安全_第15张图片
      主要特点如下:
      1、创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
      2、newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue;
    • Executors.newCachedThreadPool():适用:执行很多短期异步的小程序或者负载较轻的服务器
      在这里插入图片描述
      主要特点如下:
      1、创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
      2、newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说,来任务就创建线程池运行,当线程空闲超过60秒,就销毁线程。
  • 线程池的七个重要参数
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.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize:线程池中的常驻核心线程数
    1、在创建了线程池后,当有请求任务来了之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
    2、当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
  • maximumPoolSize:线程池能够容纳通知执行的最大线程数,此值必须大于等于1
  • keepAliveTime:多余的空闲线程的存活时间
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被提交但尚未被执行的任务
  • threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
  • handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时拒绝任务
  • 线程池的底层工作原理
    java多线程安全_第16张图片
    重要:
    1、在创建了线程池后,等待提交过来的任务请求
    2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:
    2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    2.2 如果正在运行的线程数量小于或等于corePoolSize,那么将这个任务放入队列
    2.3 如果这时队列满了,且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
    2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
    3、当一个线程完成任务时,它会队列中取下一个任务来执行。
    4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
    如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉。
    所以线程池的所以任务完成后它最终会收缩到corePolSize的大小。
3.6 生产上如何设置线程池的参数
  • 线程池的拒绝策略
    1、是什么
    等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务。这时候我们就需要拒绝者策略机制合理的处理这个问题。JDK内置的拒绝策略。以上内置策略均实现了RejectedExecutionHandle接口。
  • 工作中使用哪个?
    生产上只使用自定义的,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
    1)newFixedThreadPool和newSingleThreadExecutor:
      主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
    2)newCachedThreadPool和newScheduledThreadPool:
      主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
  • 自定义线程池
ThreadPoolExecutor threadPoolExecutor = 
new ThreadPoolExecutor(3,5,5, TimeUnit.SECONDS,
                       new LinkedBlockingQueue<>(3),
                       Executors.defaultThreadFactory(),
                       new ThreadPoolExecutor.AbortPolicy());
  • 合理配置线程池参数
    1、CPU密集型
    CPU密集型的意思是该任务需要大量的运算,而没有阻塞,CPU一致全速运行。
    CPU密集型任务只有在真正的多核CPU上才能得到加速(通过多线程),而单核CPU上无论你开几个模拟的多线程任务都不可能得到加速,因为CPU总的运算能力就哪些。
    CPU密集型任务配置尽可能少的线程数量:
    一般公式:CPU核数+1 个线程的线程池
    2、IO密集型(存在两种配置)
    1)由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
    2)IO密集型,即该任务需要大量的IO,即大量的阻塞。
    在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。IO密集型时,大部分线程都阻塞,故需要多配置线程数:
    参考公式:CPU核数/1-阻塞系数
    阻塞系数在0.8~0.9之间
    比如8核CPU: 8/1-0.9 = 80个线程数
3.6 死锁编码及定位分析

1、是什么?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限资源而陷入死锁。
java多线程安全_第17张图片
2、原产生死锁主要因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当
    3、代码实现死锁
/**
 * @author Mr. Zhang
 * @description
 * @date 2019/4/25 17:34
 * @website https://www.zhangguimin.cn
 *
 * 死锁是指两个或两个以上的进程在执行过程中,
 * 因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,
 */
public class DeadLockDemo {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldLockThread(lockA,lockB),"AAA").start();
        new Thread(new HoldLockThread(lockB,lockA),"BBB").start();
    }
}

class HoldLockThread implements Runnable{
    private String lockA;
    private String lockB;

    public HoldLockThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获得:" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获得" + lockA);
            }
        }

    }
}

4、解决方式
查看 先jps->jstack pid

java多线程安全_第18张图片

  • jps命令定位进程编号
  • jstack找到死锁查看
3.7 synchronized和lock有什么区别(补充)

1、原始构成
synchronized是关键字属于JVM层面,monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只有再同步块或方法中才能调用wait/notify等方法)monitorexit。
lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁。
2、使用方法
synchronized不需要用户手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。
ReentrantLock则需要用户手动释放锁,如果没主动释放锁,可能会造成死锁,需要使用lock()和unLock()方法配合try/finally语句块来完成
3、等待是否可中断
synchronized不可中断,除非抛出异常或正常运行完成。
ReentrantLock 可中断, 1)、设置超时方法 tryLock(long timeout,TimeUnit unit) 2)、lockInterruptibly()放代码块中,调用interrupt()方法可中断
4、加锁是否公平
synchronized非公平锁。
ReentrantLock两者都可以,默认为非公平锁(false)
5、锁绑定多个条件Condition
synchronized没有
ReentrantLock用来实现分组唤醒的线程,可以精确唤醒,而不像synchronized要么随机唤醒要么全部唤醒。

你可能感兴趣的:(java后端,多线程,JUC,java多线程安全,JUC,原子变量,原子重排)