程序员

来吧,展示!这份阿里P7大佬给我的JUC知识总结真的写的太详细了

2020-08-19  本文已影响0人  程序员匡胤

前言

在 Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。 提供可调的、灵活的线程池。还提供了设计用于 多线程上下文中的 Collection 实现等

volatile 关键字-内存可见性

JVM为每一个线程提供一个独立的缓存,用于提高效率

内存可见性(Memory Visibility)是指当某个线程正在使用对象状态 而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能实时地看到其他线程写入的值,有时甚至是不可能的事情

我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的volatile变量

Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

public class VolatileTest {
    public static void main(String[] args) {
        ThreadDemo t = new ThreadDemo();
        new Thread(t).start();
        while (true) {
            if(t.isFlag()) {
                System.out.println("------");
                break;
            }
        }
    }
}
class ThreadDemo implements Runnable {
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

mian线程读取到的flag 一直都是false,所以打印结果为 flag = true,然后程序没有结束

解决:

volatile,当多个线程进行操作共享数据时,可以保证内存中的数据可见


public class VolatileTest {
    public static void main(String[] args) {
        ThreadDemo t = new ThreadDemo();
        new Thread(t).start();
        while (true) {
            if(t.isFlag()) {
                System.out.println("------");
                break;
            }
        }
    }
}
class ThreadDemo implements Runnable {
    private volatile boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + isFlag());
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

原子变量-CAS算法

类的小工具包,支持在单个变量上解除锁的线程安全编程。事实上,此包中的类可 将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。

类 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对 相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。

AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操 作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方 面也引人注目,这对于普通数组来说是不受支持的。

核心方法:boolean compareAndSet(expectedValue, updateValue)

java.util.concurrent.atomic 包下提供了一些原子操作的常用类: AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference AtomicIntegerArray 、AtomicLongArray ,AtomicMarkableReference ,AtomicReferenceArray ,AtomicStampedReference
具体的一些方法,可查看API文档

i++ 原子性问题,先读取到i 然后再 ++ ,操作被分开了,有同步安全问题

public class AtomicTest {
    public static void main(String[] args) {
        Atomic a = new Atomic();
        for (int i = 0; i < 10; i++) {
            new Thread(a).start();
        }
    }
}
class Atomic implements Runnable {
    private volatile int serialNumber = 0;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
    }
    public int getSerialNumber() {
        return serialNumber++;
    }
}

原子变量 jdk1.5后 java.util.concurrent.atomic包下提供了常用的原子变量

模拟CAS

public class CompareAndSwapTest {
    public static void main(String[] args) {
        CompareAndSwap cas = new CompareAndSwap();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    int expectedValue = cas.get();
                    boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101));
                    System.out.println(b);
                }
            }).start();
        }
    }
}
class CompareAndSwap {
    private int value;
    // 获取内存值
    public synchronized int get(){
        return value;
    }
    // 比较
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if(oldValue == expectedValue) {
            this.value = newValue;
        }
        return oldValue;
    }
    // 设置
    public synchronized boolean compareAndSet(int expectedValue, int newValue) {
        return expectedValue == compareAndSwap(expectedValue, newValue);
    }
}

解决 i++原子性问题

public class AtomicTest {
    public static void main(String[] args) {
        Atomic a = new Atomic();
        for (int i = 0; i < 10; i++) {
            new Thread(a).start();
        }
    }
}
class Atomic implements Runnable {
    private AtomicInteger serialNumber = new AtomicInteger();
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getSerialNumber());
    }
    public int getSerialNumber() {
        return serialNumber.getAndIncrement();
    }
}

ConcurrentHashMap 锁分段机制

Hashtable效率非常低 复合操作可能线程不安全 一次只能一个线程进行操作

复合操作包括迭代(反复获取元素,直到容器中的最后一个元素)、导航(根据一定顺序查找下一元素)、条件运算(“若不存在则添加”,“若存在则删除”)

Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器 的性能

ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对 与多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段” 机制替代 Hashtable 的独占锁。进而提高性能

此包还提供了设计用于多线程上下文中的 Collection 实现: ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、 CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给 定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap, ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远 大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList

ConcurrentHashMap 的 concurrentLevel 是16


每个段都是独立的锁,当多个线程并发访问时,在不同的段上进行操作,则可做到并行

copyOnWriteArrayList例子

public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        HelloThread ht = new HelloThread();
        for (int i = 0; i < 2; i++) {
            new Thread(ht).start();
        }
    }
}
/**
 * CopyOnWriteArrayList写入并复制,添加操作多时,效率低,因为每次添加时都会进行复制,开销很大
 * 并发迭代操作多时可以选择
 */
class HelloThread implements Runnable {
    // 这种会出现并发修改异常
//    private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    static {
        list.add("AA");
        list.add("BB");
        list.add("CC");
    }
    @Override
    public void run() {
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            System.out.println(it.next());
            list.add("AA");
        }
    }
}

CountDownLatch 闭锁

Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器 的性能。

CountDownLatch 一个同步辅助类,在完成一组正在其他线程中执行的操作 之前,它允许一个或多个线程一直等待。

闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活 动直到其他活动都完成才继续执行:

确保某个计算在其需要的所有资源都被初始化之后才继续执行;
确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
等待直到某个操作所有参与者都准备就绪再继续执行

闭锁:在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才能继续执行

public class CountDownLatchTest {
    public static void main(String[] args) {
        // 5 表示其他线程的数量
        CountDownLatch latch = new CountDownLatch(5);
        LatchDemo ld = new LatchDemo(latch);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            new Thread(ld).start();
        }
        try {
            // 此处要一直等到 latch的值为0 ,就能往下执行了
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("消耗时间为:" + (end - start));
    }
}
class LatchDemo implements Runnable {
    private CountDownLatch latch;
    public LatchDemo(CountDownLatch latch) {
        this.latch = latch;
    }
    @Override
    public void run() {
        synchronized(this) {
            try {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(i);
                    }
                }
            } finally {
                latch.countDown();
            }
        }
    }
}

Condition 控制线程通信

Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用 法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的 功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关 联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版 本中的不同

在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是 await、signal 和 signalAll

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 三个方法的使用方法和 wait,notify 和 notifyAll一样

线程按序交替

编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要 求输出的结果必须按顺序显示。 如:ABCABCABC…… 依次递归

public class ABCAlternateTest {
    public static void main(String[] args) {
        AlternateDemo ad = new AlternateDemo();
        new Thread(new Runnable() {
            @Override
            public void run(){
                for (int i = 1; i <= 10; i++) {
                    ad.loopA(i);
                }
            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    ad.loopB(i);
                }
            }
        },"B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    ad.loopC(i);
                }
            }
        },"C").start();
    }
}
class AlternateDemo {
    // 记录当前正在执行的线程的ID
    private int number = 1;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void loopA(int totalLoop) {
        try {
            lock.lock();
            if(number != 1) {
                try {
                    condition1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 打印
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName());
            }
            number = 2;
            condition2.signal();
        } finally {
            lock.unlock();
        }
    }
    public void loopB(int totalLoop) {
        try {
            lock.lock();
            if(number != 2) {
                try {
                    condition2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 打印
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName());
            }
            number = 3;
            condition3.signal();
        } finally {
            lock.unlock();
        }
    }
    public void loopC(int totalLoop) {
        try {
            lock.lock();
            if(number != 3) {
                try {
                    condition3.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 打印
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName());
            }
            number = 1;
            condition1.signal();
            System.out.print(" ");
        } finally {
            lock.unlock();
        }
    }
}

ReadWriteLock 读写锁

ReadWriteLock是一个接口

ReadWriteLock 维护了一对相关的锁,一个用于只读操作, 另一个用于写入操作。只要没有 writer,读取锁可以由 多个 reader 线程同时保持。写入锁是独占的

ReadWriteLock 读取操作通常不会改变共享资源,但执行 写入操作时,必须独占方式来获取锁。 对于读取操作占 多数的数据结构。ReadWriteLock 能提供比独占锁更高 的并发性。而对于只读的数据结构,其中包含的不变性 可以完全不需要考虑加锁操作

public class ReadWriteLockTest {
    public static void main(String[] args) {
        ReadWriteLockDemo rw = new ReadWriteLockDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                rw.set((int)(Math.random() * 101));
            }
        },"write").start();
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rw.get();
                }
            }).start();
        }
    }
}
class ReadWriteLockDemo {
    private int number = 0;
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 读
    public void get() {
        try {
            readWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + ":" + number);
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
    // 写
    public void set(int number) {
        try {
            readWriteLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName());
            this.number = number;
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

线程八锁

静态同步方法与非静态同步方法之间是不会有竞态条件的,非静态同步方法,看他们的锁是不是同一个number,是同一个number则,一个拿到,另一个要等待;否则,另一个不用等待

* 线程八锁的关键: 1.非静态方法的锁 this, 静态方法的锁 对应的Class实例
 *               2.某一个时刻内,只能由一个线程持有锁,无论几个方法
 * 1.两个同步方法,两个线程,打印 one  two
 * 2.新增Thread.sleep()给getOne 打印 one two
 * 3.新增普通方法getThree,打印  three one two
 * 4.注释getThree,number2.getTwo,打印 two one
 * 5.修改getOne为静态同步方法,改为number.getTwo,打印 two one
 * 6.两个方法都为静态同步方法,一个number对象,打印 one two
 * 7.getOne为静态同步方法,getTwo为同步方法,改为number2.getTwo,打印two one
 * 8.两个静态同步方法,两个number对象,打印 one two
 */
public class Thread8MonitorTest {
    public static void main(String[] args) {
        Number number = new Number();
        // 4
//        Number number2 = new Number();
        // 7
        Number number2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 1,2,3
//                number.getTwo();
                // 4
//                number2.getTwo();
                // 5
//                number.getTwo();
                // 7
                number2.getTwo();
            }
        }).start();
        // 3
        /*new Thread(new Runnable() {
            @Override
            public void run() {
                number.getThree();
            }
        }).start();*/

    }
}
class Number{
    public static synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("one");
    }
    public static synchronized void getTwo() {
        System.out.println("two");
    }
    public void getThree() {
        System.out.println("three");
    }
}

线程池

第四种获取线程的方法:线程池,一个 ExecutorService,它使用可能的几个池线程之
一执行每个提交的任务,通常使用 Executors 工厂方法配置。

线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在
执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行
任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数
据,如完成的任务数。

为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但
是,强烈建议程序员使用较为方便的 Executors 工厂方法 :

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。

好处: 1. 提高响应速度(减少了创建新线程的时间)

2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)

3.便于线程管理 corePoolSize:核心池的大小 ,/maximumPoolSize:最大线程数,keepAliveTime:线程没有任务时最多保持多长时间后会终止 线程池:提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建于销毁的额外开销,提高了响应的速度

java.util.concurrent.Executor :负责线程的使用与调度的根接口
–ExecutorService 子接口:线程池的主要接口,继承Executor
–ThreadPoolExecutor 线程池的实现类
–ScheduledExecutorService 子接口:负责线程的调度,继承ExecutorService
–ScheduledThreadPoolExecutor 继承ThreadPoolExecutor 实现ScheduledExecutorService

public class ThreadPoolTest {
    public static void main(String[] args) {

        // 1.创建线程池 5个线程
        ExecutorService pool = Executors.newFixedThreadPool(5);
        ThreadPoolDemo tpd = new ThreadPoolDemo();
        /*// 2.为线程池中的线程分配任务
        for (int i = 0; i < 5; i++) {
            pool.submit(tpd);
        }
        // 3.关闭线程池,保证所有线程的任务完成才会关闭
        // shutdownNow() 立即关闭,不管任务做完没有
        pool.shutdown();*/
        List<Future<Integer>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
        // Future得到Callable的返回值
            Future<Integer> future = pool.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 0;
                    for (int j = 0; j <= 100; j++) {
                        sum += j;
                    }
                    return sum;
                }
            });
            list.add(future);
        }
        pool.shutdown();
        for(Future<Integer> future : list) {
            try {
                System.out.println(future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}
class ThreadPoolDemo implements Runnable{
    private int i = 0;
    @Override
    public void run() {
        while(i <= 100){
            System.out.println(Thread.currentThread().getName() + " : " + i++);
        }
    }
}

线程调度

ScheduledExecutorService newScheduledThreadPool() 创建固定大小的线程,可以延迟或定时的执行任务

public class ScheduledThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
        for (int i = 0; i < 5; i++) {
            Future<Integer> result = pool.schedule(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int num = new Random().nextInt(100);
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    return num;
                }
            }, 3, TimeUnit.SECONDS);// 延迟3s执行
            try {
                System.out.println(result.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        pool.shutdown();
    }
}

TimeUnit的用法

ForkJoinPool 分支/合并框架 工作窃取

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成 若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进 行 join 汇总

采用 “工作窃取”模式(work-stealing): 当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加 到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队 列中

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务 的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些 原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中, 如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理 该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了 线程的等待时间,提高了性能

Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换

public class ForkJoinPoolTest {
    public static void main(String[] args) {
        Instant start = Instant.now();
        ForkJoinPool pool = new ForkJoinPool();
        // RecursiveTask<V> extends ForkJoinTask<V>
        ForkJoinTask<Long> task = new ForkJoinTest(0L, 50000000000L);
        Long sum = pool.invoke(task);
        System.out.println(sum);
        Instant end = Instant.now();
        System.out.println(Duration.between(start, end).toMillis());
    }
    @Test
    public void test2() { // 18256
        Instant start = Instant.now();
        long sum = 0;
        for (long i = 0; i <= 50000000000L; i++) {
            sum += i;
        }
        Instant end = Instant.now();
        System.out.println(Duration.between(start, end).toMillis());
    }
    @Test
    public void test3() { // 14444
        //  对ForkJoin的改进
        Instant start = Instant.now();
        // rangeClosed 生成连续的数
        long sum1 = LongStream.rangeClosed(0, 50000000000L)
                .parallel()
                .reduce(0, Long::sum); // 第二个参数 是函数式接口LongBinaryOperator
        Instant end = Instant.now();
        System.out.println(Duration.between(start, end).toMillis());
    }
}
class ForkJoinTest extends RecursiveTask<Long> {// Recursive 递归
    // RecursiveAction 没有返回值
    // RecursiveTask 有返回值
    private long start;
    private long end;

    public ForkJoinTest(long start, long end) {
        this.start = start;
        this.end = end;
    }
    private static final long THRESHOLD = 10000;

    @Override
    protected Long compute() {
        long length = end - start;
        if(length <= THRESHOLD){
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            long middle = (start + end) / 2;
            ForkJoinTest left = new ForkJoinTest(start, middle);
            left.fork(); // 拆分子任务,同时压入线程队列
            ForkJoinTest right = new ForkJoinTest(middle + 1, end);
            right.fork();
            return left.join() + right.join();
        }
    }
}

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

上一篇下一篇

猜你喜欢

热点阅读