深入JUC(高并发编程)

文章目录

  • JUC背景
  • JUC的结构
  • Lock锁
    • 线程之间的通信问题
    • synchronized实现
    • JUC实现
  • 并发集合
    • 第一代并发
    • 第二代并发
    • 第三代并发
  • Callable接口
  • 常见工具类
    • CountDownLatch
    • CyclicBarrier
    • Semaphore
  • 读写锁(ReadWriteLock)
  • 阻塞队列
  • 线程池
    • 三大方法
      • CachedThreadPool
      • SingleThreadPool
      • FixedThreadPool
    • 七大参数
    • 四种拒绝策略
      • 1.AbortPolicy
      • 2.CallerRunsPolicy
      • 3.DiscardOldestPolicy
      • 4.DiscardPolicy
      • 自定义拒绝策略
    • 关闭线程池
    • 优先级
      • 提交优先级
      • 执行优先级
      • 场景:
    • 线程池线程复用原理
    • 线程池的调优
      • CPU密集型
      • IO密集型
  • ForkJoin
    • 前言
    • Java并发编程的发展
    • 并发与并行
      • 并发
      • 并行
    • 分治法
      • 基本思想
      • 步骤
      • 典型应用
    • ForkJoin并行处理框架
      • ForkJoin框架概述
      • ForkJoin框架原理
      • 工作窃取算法
      • ForkJoin框架的实现
    • ForkJoin示例程序
  • 并行流(Stream)
    • 前言
    • 并行流工作
    • Stream的一些常规操作
    • parallelStream
    • 总结
  • 异步回调
  • volatile
    • volatile三大特性
    • 指令重排
      • 指令的基本概念
      • 反编译获取指令
      • 指令的基本概念
      • 指令重排序
      • As-If-Serial语义
      • Happens-Before原则
      • 具体规则
    • JMM
    • volatile关键字的场景
  • CAS
    • 背景
    • 什么是CAS
    • CAS存在的问题
      • ABA问题
      • 循环时间长开销大
      • 只能保证一个共享变量的原子操作
    • concurrent包的实现
    • 公平锁/非公平锁
    • 可重入锁
    • 自旋锁
    • 死锁
      • 模拟死锁
      • 排查死锁

JUC背景

JUC(java.util.concurrent的简称)是一个工具包,通常在并发编程中有着很大的用处!

  • 以前我们创建线程使用的是 (Thread),在JUC提供了另一种创建线程的方法Callable
  • 以前使用synchronized进行加锁同步,在JUC中提供了一种锁机制——Lock锁
  • 以前我们的集合想使用线程安全的集合需使用Vector,HashTable等集合,在JUC中提供了CopyOnWriteArrayList,ConcurrentHashMap等方式创建安全集合

当然不止如上描述,下面将仔细描述JUC的强大之处!
深入JUC(高并发编程)_第1张图片

JUC的结构

1,tools(工具类):又叫信号量三组工具类,包含有

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

  • CyclicBarrier(栅栏) 之所以叫barrier,是因为是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 ,并且在释放等待线程后可以重用。

  • Semaphore(信号量) 是一个计数信号量,它的本质是一个“共享锁“。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。

2,executor(执行者):是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService,里面包含的类有:

  • ScheduledExecutorService 解决那些需要任务重复执行的问题

  • ScheduledThreadPoolExecutor 周期性任务调度的类实现

  • atomic(原子性包):是JDK提供的一组原子操作类,包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。

4,locks(锁包):是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁包含的实现类有:

  • ReentrantLock 它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。

  • ReentrantReadWriteLock 它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。

  • LockSupport 它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。

5,collections(集合类):主要是提供线程安全的集合, 比如:

  • ArrayList对应的高并发类是CopyOnWriteArrayList,

  • HashSet对应的高并发类是 CopyOnWriteArraySet,

  • HashMap对应的高并发类是ConcurrentHashMap等等

Lock锁

传统使用的是synchronized对线程枷锁,但是JUC中引用了Lock锁

Runable接口:没有返回值,且效率相比于Callable低(Callable位于java.util.concurrent包下的一个类)

在真正的多线程开发中,公司的开发,降低耦合性。

线程就是一个单独的资源类,没有任何附属的操作,因此

多线程编写三步骤:

  1. 编写资源类
  2. 创建Thread
  3. 将资源类传入Thread中

synchronized会引发的问题

  • 一个线程持有锁会导致其他所所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁当值比较多的上下文切换和调度延时,引发性能问题
  • 一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引发性能问题
    深入JUC(高并发编程)_第2张图片

两者区别:

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

  3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

  4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去(尝试获取锁lock.tryLock()),如果尝试获取不到锁,线程可以不用一直等待就结束了;

  5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可),自由度高

  6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

线程之间的通信问题

生产者和消费者问题(线程交体执行)

线程A B 操作同一个变量num

A:想使num+1

B:想使num-1

因此需要等待唤醒,通知唤醒

步骤:判断等待 、业务、通知

synchronized实现

public class Test{
    public static void main(String[] args){
        //创建资源类
        Data data = new Data();
        //创建Thread并放入资源类
        new Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.increment();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.decrement();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

//资源类
class Data{
    private int num = 0;
    public synchronized void increment() throws InterruptedException{
        //判断等待
        if(number != 0){
            this.wait(); //库存不为0,无需进库,等待
        }
        //业务
        number++;
        //通知
        this.notifyAll();
    }
    public synchronized void decrement() throws InterruptedException{
        //判断等待
        if(number == 0){
            this.wait();   //库存为0,无法再出库,等待
        }
        //业务
        number--;
        //通知
        this.notifyAll();
    }
}

存在问题,只能基于两个线程,当存在C、D线程时将会出现问题,比如存在2,3,4等数据。

在wait()文档中有说明:当前的线程必须拥有该对象的显示器, 该线程释放此监视器的所有权,并等待另一个线程通知等待该对象监视器的线程通过调用notify方法或notifyAll方法notifyAll ,然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。 像在一个参数中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用

由此,得知wait()应该使用在循环中可解决上述问题

if该成while循环解决问题:

public class Test{
    public static void main(String[] args){
        //创建资源类
        Data data = new Data();
        //创建Thread并放入资源类
        new Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.increment();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.decrement();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"B").start();
        ew Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.decrement();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"C").start();
        ew Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.decrement();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"D").start();
        
    }
}

//资源类
class Data{
    private int num = 0;
    public synchronized void increment() throws InterruptedException{
        //判断等待
        while(number != 0){
            this.wait(); //库存不为0,无需进库,等待
        }
        //业务
        number++;
        //通知
        this.notifyAll();
    }
    public synchronized void decrement() throws InterruptedException{
        //判断等待
        while(number == 0){
            this.wait();   //库存为0,无法再出库,等待
        }
        //业务
        number--;
        //通知
        this.notifyAll();
    }
}

JUC实现

由上可知,使用synchronized实现需要wait()方法和notifyAll(),那使用Lock,又需要什么呢?

  • Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。

通过Lock可以找寻到Condition,使用Condition中的await和signal代替wait()方法和notifyAll()

public class Test{
    public static void main(String[] args){
        //创建资源类
        Data data = new Data();
        //创建Thread并放入资源类
        new Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.increment();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for(int i = 0;i<10;i++){
                try{
                    data.decrement();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

//资源类
class Data{
    private int num = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    //condition.await() 等待   condition.signalAll() 
    
    public  void increment() throws InterruptedException{
        lock.lock();  //加锁
        try{
             //判断等待
        while(number != 0){
            condition.await(); //库存不为0,无需进库,等待
        }
        //业务
        number++;
        //通知
        condition.signalAll();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
       
    }
    public synchronized void decrement() throws InterruptedException{
        lock.lock();  //加锁
        try{
             //判断等待
        while(number == 0){
            condition.await(); //库存不为0,无需进库,等待
        }
        //业务
        number--;
        //通知
        condition.signalAll();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
}

实现和synchronized实现没区别,那使用Condition的意义何在?

我们能不能实现精准通知唤醒,A执行完B执行再执行C最后执行D?

synchronized无能为力,而新技术Condition则可以实现!

//实现三个线程精准通知,A通知B,B通知C
public class Test{
    public static vvoid main(String[] args){
        new Thread(()->{
            for(int i=0;i<10;i++){
                printA();
            }
        },"A").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                printB();
            }
        },"B").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                printC();
            }
        },"C").start();
    }
    
}
//资源类
class Data{
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition;
    private Condition condition2 = lock.newCondition;
    private Condition condition3 = lock.newCondition;
    public void printA(){
        lock.lock();
        try{
            while(num != 1){
                //
                condition1.await();
            }
            num = 2;
            condition2.signal();
            
        }catch(Exception e){
           e.printStackTrace();   
        }
    }
    public void printB(){
         lock.lock();
        try{
            while(num != 2){
                //
                condition2.await();
            }
            num = 3;
            condition3.signal();
            
        }catch(Exception e){
           e.printStackTrace();   
        }
    }
    public void printC(){
         lock.lock();
        try{
            while(num != 1){
                //
                condition3.await();
            }
            num = 1;
            condition1.signal();
            
        }catch(Exception e){
           e.printStackTrace();   
        }
    }
}

就是关于锁的问题

  1. synchronized锁的对象是方法的调用者,两个方法用的时同一个锁,谁先拿到谁执行

并发集合

我们常用的是ArrayList,HashMap集合,这些集合当比与Vector,HashTable集合性能高,但是非安全集合,在并发(多线程)开发下使用非安全的集合会报ConcurrentModificationException并发修改异常,如下

public class ListTest{
    public static void main(String[] args){
        List<String> list = new List<String>();
        for(int i=1;i<=10;i++){
            new Thread(()->{
                list.add(UUID.randomUUID.toString().substring(0,5));
                System.out.println(list);
            },String.valueof(i)).start();
        }
    }
}
//报java.util.ConcurrentModificationException异常

解决问题:使用并发集合!

第一代并发

​ 使用安全的集合Vector,HashTable(不推荐),但是Vector,HashTable效率低(所有方法都被synchronized修饰,性能低);

第二代并发

  • Collections.synchronizesList(new ArrayList<>())
  • 将不安全的集合转成安全的,

底层也使用synchronized代码块锁。虽然也是锁住了所有代码,但是锁方法里面,性能可以理解为稍微有提高,毕竟方法本身就要分配资源的

优势:

  • SynchronizedList有很好的扩展和兼容功,他可以将所有的List的子类转成线程安全的类。

  • 使用SynchronizedList的时候,进行遍历时要手动进行同步处理。

  • SynchronizedList可以指定锁定的对象。

缺点:由于底层任然使用Synchronized实现,相比于第三类并发集合性能低

public class ListTest{
    public static void main(String[] args){
        List<String> list = Collections.synchronizesList(new ArrayList<>());
        for(int i=1;i<=10;i++){
            new Thread(()->{
                list.add(UUID.randomUUID.toString().substring(0,5));
                System.out.println(list);
            },String.valueof(i)).start();
        }
    }
}

第三代并发

使用安全的ArrayList:new CopyOnWriteArrayList<>()

使用安全的Set:new CopyOnWriteArraySet<>()

使用安全的Map:ConcurrentHashMap ,ConcurrentSkipListMap

使用安全的Queue :ArrayBlockingQueue

CopyWrite写入时复制, COW 计算机程序设计领域的一种优化策略,多个线程调用的时候,list,读取的时候是固定的,写入(覆盖)

在写入的时候避免覆盖,造成数据问题(即写入时先复制之后重新插入的操作),读写分离

底层大部分采用的是lock锁(1.8的concurrentHashMap不适用Lock锁),保证安全的同时,性能也很高

public class ListTest{
    public static void main(String[] args){
        List<String> list = new CopyWriteArrayList<>();
        for(int i=1;i<=10;i++){
            new Thread(()->{
                list.add(UUID.randomUUID.toString().substring(0,5));
                System.out.println(list);
            },String.valueof(i)).start();
        }
    }
}
public class MapTest{
    public static void main(String[] args){
        Map<String,String> map = new ConcurrentHashMap<>();
        for(int i=1;i<=10;i++){
            new Thread(()->{
                list.put(UUID.randomUUID.toString().substring(0,5));
                System.out.println(map);
            },String.valueof(i)).start();
        }
    }
}

Callable接口

Callable接口类似于Runnable,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,Runnable不返回结果,也不能抛出被检查的异常。

该Executors类包含的实用方法,从其他普通形式转换为Callable类。

即Callable存在以下特点:

  • 拥有返回值
  • 可以抛出异常
  • 执行方法不同,run()变为了call()

创建步骤

  1. 创建类实现Callable接口
  2. 创建FutureTask适配类(Callabel实现类无法直接与Thread挂钩,而启动线程的唯一方法是thread.start(),因此需要一个桥梁FutureTask),并将Callable的实现类作为参数传入
  3. 创建Thread类并将futureTask作为参数传入
public class CallableTest{
    public static void main(String[] args) throw ExecutionException,InterruptedException{
        MyThread thread = new MyThread();
        FutureTask futureTask = new FutureTask(thread); 
        new Thread(futureTask,'A').start();
        new Thread(futureTask,'B').start();
        //获取callable的返回值
        //String str = (String)futureTask.get();
        //此时创建了两个线程,会返回两个128吗?答案只返回一个128,因为结果会被缓存,这样提高效率
        Integer num = (Integer)futureTask.get();
        //get方法可能会发生阻塞,比如在call方法中进行延时操作,因此一般放到最后,或者使用异步通信来处理
        System.out.println(num);
    }
}
class MyThread implements Callabe<Integer>{
    //Callable中的T是与返回值类型一致
    public Integer call(){
        System.out.println("hello call");
        return 128;
    }
}

常见工具类

CountDownLatch

倒计时锁存器,允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

简单来说就是一个倒计时计数器,线程相当于时间,当线程数减到0时,释放所有等待线程。

作用:同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

public CountDownLatch(int count)
//构造一个以给定计数CountDownLatch。 
//  参数 
// count -的次数 countDown()必须调用之前线程可以通过await() 
//如果 count 为负数 ,报异常IllegalArgumentException 

流程

  1. 创建CountDownLatch对象并指定一组任务所需的线程数
  2. 执行任务组
  3. 调用CountDownLatch的countDown方法,对该组线程完成相应任务进行倒计时计数
  4. 调用CountDownLatch的await方法,计数为0,释放所有线程,向下执行其他线程任务

eg

public class CountDownLatchDemo{
    public static void main(String[] args) throws InterruptedException{
        //假设需要8条线程完成某一些工作
        CountDownLatch conutDownLatch = new CountDownLatch(8);
        for(int i=1;i<=8;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"该线程完成任务!");
                countDownLatch.countDown();  //计数器数量减一
            },String.valueOf(i),start());
        }
        countDownLatch.await(); //等待计数器为0,然后向下执行,否则下面的线程全部等待
        System.out.println("该组线程已完成任务1,其他线程请完成任务2!");
        
    }
}

方法介绍:

countDown()

​ 减少当前锁存器的计数,如果计数达到零,释放所有等待的线程,如果当前计数大于零,则将递减。如果新计数为零,则所有等待的线程都将被重新启动以进行线程调度,如果当前计数等于零,那么没有任何反应

await()

​ 导致当前等待,直到寄存器计数到零,除非线程是interrupted,如果当前计数为零,则此方法立即返回,如果当计数大于零,则当前线程将被禁用以进行线程调度,并处于休眠状态,直至发生两件事情之一

  • 由于countDown()方法的调用,计数达到零
  • 一些其他线程interrupts当前线程

如果当前线程

  • 在进入该方法时设置了中断状态
  • 是interrupted等待

然后InterruptedException被关上,当前线程的中断状态清除

总之:该方法等待计数器为零,然后才向下执行

**示例用法:**这是一组类,其中一组工作线程使用两个倒计时锁存器:

  • 第一个是启动信号,防止任何工作人员进入,直到驾驶员准备好继续前进;
  • 第二个是完成信号,允许司机等到所有的工作人员完成。
class Driver {
   // 
       public static void main() throws InterruptedException { 
           //创建CountDownLatch对象,计数为1,相当于启动信号
           CountDownLatch startSignal = new CountDownLatch(1); 
           //创建CountDownLatch对象,计数为N,相当于所有人员
           CountDownLatch doneSignal = new CountDownLatch(N); 
           for (int i = 0; i < N; ++i)
               // create and start threads 
               new Thread(new Worker(startSignal, doneSignal)).start();  	                        doSomethingElse(); 
           // don't let run yet 
               startSignal.countDown(); 
           // let all threads proceed 
           doSomethingElse(); 
           doneSignal.await(); 
           // wait for all to finish 
       } 
} 
class Worker implements Runnable {
    private final CountDownLatch startSignal; 
    private final CountDownLatch doneSignal; 
    Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { 
        this.startSignal = startSignal; 
        this.doneSignal = doneSignal; 
    } public void run() { 
        try { 
            startSignal.await(); 
            doWork(); 
            doneSignal.countDown(); 
        } catch (InterruptedException ex) {
            
        } //
        return; 
    } void doWork() { ... } } 

另一个典型的用法是将问题划分为N个部分,用一个Runnable来描述每个部分,该Runnable执行该部分并在锁存器上倒计时,并将所有Runnables排队到执行器。 当所有子部分完成时,协调线程将能够通过等待。 (当线程必须以这种方式反复倒数时,请改用)

class Driver2 { 
    // 
    public static void main() throws InterruptedException { 
        CountDownLatch doneSignal = new CountDownLatch(N); 
        Executor e = ... for (int i = 0; i < N; ++i) 
            // create and start threads 
            e.execute(new WorkerRunnable(doneSignal, i)); 
        doneSignal.await(); 
        // wait for all to finish 
    } 
} 
class WorkerRunnable implements Runnable { 
    private final CountDownLatch doneSignal;
    private final int i; 
    WorkerRunnable(CountDownLatch doneSignal, int i) { 
        this.doneSignal = doneSignal; this.i = i; 
    } public void run() {
        try { 
            doWork(i); doneSignal.countDown(); 
        } catch (InterruptedException ex) {
            
        } 
        // 
        return; 
    } 
    void doWork() { ... } } 

内存一致性效果:直到计数调用之前达到零,在一个线程操作countDown() happen-before以下由相应的成功返回行动await()在另一个线程。

缺点

​ CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

CyclicBarrier

和CountDownLatch类似,也是类似计数器。

​ 现实生活中经常会遇到一些特定场景,在进行某个活动前需要等待人全部都齐了才开始。例如吃饭时要等全部家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。因此,在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作,如下述动态图

img

官方解释:

​ 允许一组线程全部等待彼此达到共同屏障点的同步辅助。循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。屏障被称为循环,因为它可以在等待的线程被释放之后重新使用。

CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。

深入JUC(高并发编程)_第3张图片

**示例用法:**以下是在并行分解设计中使用障碍的示例:

 class Solver { 
     final int N; 
     final float[][] data; 
     final CyclicBarrier barrier; 
     class Worker implements Runnable { 
         int myRow; 
         Worker(int row) { 
             myRow = row; 
         } public void run() {
             while (!done()) { 
                 processRow(myRow); 
                 try { 
                     barrier.await(); 
                 } catch (InterruptedException ex) {
                     return;
                 } catch (BrokenBarrierException ex) { 
                     return; 
                 } 
             } 
         } 
     } public Solver(float[][] matrix) {
         data = matrix;
         N = matrix.length; 
         Runnable barrierAction = new Runnable() {
             public void run() {
                 mergeRows(...); 
             }
         }; 
         barrier = new CyclicBarrier(N, barrierAction);
         List<Thread> threads = new ArrayList<Thread>(N);
         for (int i = 0; i < N; i++) {
             Thread thread = new Thread(new Worker(i)); 
             threads.add(thread); thread.start();
         } 
         // wait until done 
         for (Thread thread : threads) thread.join();
     } 
 } 

这里,每个工作线程处理矩阵的一行,然后等待屏障,直到所有行都被处理。当处理所有行时,执行提供的Runnable屏障操作并合并行。如果合并确定已经找到解决方案,那么done()将返回true ,并且每个工作人员将终止。

如果屏障操作不依赖于执行方暂停的各方,那么该方可以在释放任何线程时执行该操作。 为了方便这一点,每次调用await()返回该线程在屏障上的到达索引。 然后,您可以选择哪个线程应该执行屏障操作,例如:

   if (barrier.await() == 0) { // log the completion of this iteration } 

CyclicBarrier对失败的同步尝试使用all-or-none断裂模型:如果线程由于中断,故障或超时而过早离开障碍点,那么在该障碍点等待的所有其他线程也将通过BrokenBarrierException (或InterruptedException异常离开如果他们也在同一时间被打断)。

内存一致性效果:线程中调用的行动之前,await() happen-before行动是屏障操作的一部分,进而*发生,之前的动作之后,从相应的成功返回await()其他线程 。

CyclicBarrier与CountDownLatch的区别

这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。

区别:

  • CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制
  • 在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
  • CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能
  • CyclicBarrier提供了:resert()、getNumberWaiting()、isBroken()等比较有用的方法。

(本节参考:https://blog.csdn.net/qq_39241239/article/details/87030142)

Semaphore

​ Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

​ Semaphore表示一个计数信号量。在概念上,信号量维持一组许可证。如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。每个release()添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象;Semaphore只保留可用数量的计数,并相应地执行。信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。

场景

某个餐馆最多能坐15人(限流),后续顾客需要等待就餐的顾客离开才能有座位,此时就需要semaphore进行限流!

两个重要方法

acquire:获得,假设如果已经达到最大数,等待,直到被释放为止

release:释放,会将当前的信号量释放,然后唤醒等待的线程

作用

多个共享资源互斥的使用,并发限流,控制最大的线程数!

实例

public class CustomerDemo{
    public static void mian(String[] args){
        Semaphore semaphore = new Semaphore(15);
        //限流15人,来了40人
        for(int i=1;i<=40;i++){
            new Thread(()->{
                try{
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"进入座位就餐!");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"吃完离开座位!");
                }catch(InterruptedException e){
                }finally{
                    semaphore.release(); //释放位置锁
                }
            },String.valueOf(i)).start();
        }
        
    }
}

读写锁(ReadWriteLock)

ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。readlock可以由多个阅读器线程同时进行。writelock是独家的。

写锁(独占锁)一次只能被一个线程占用,读锁(共享锁)可以多个线程占用

  • 所有ReadWriteLock实现必须保证的存储器同步效应writeLock操作(如在指定Lock接口)也保持相对于所述相关联的readLock。 也就是说,一个线程成功获取读锁定将会看到在之前发布的写锁定所做的所有更新。

  • 读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。 它利用了这样一个事实:一次只有一个线程( 写入线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(因此读取器线程)。 从理论上讲,通过使用读写锁允许的并发性增加将导致性能改进超过使用互斥锁。 实际上,并发性的增加只能在多处理器上完全实现,然后只有在共享数据的访问模式是合适的时才可以。

  • 读写锁是否会提高使用互斥锁的性能取决于数据被读取的频率与被修改的频率相比,读取和写入操作的持续时间以及数据的争用 ,即是,将尝试同时读取或写入数据的线程数。 例如,最初填充数据的集合,然后经常被修改的频繁搜索(例如某种目录)是使用读写锁的理想候选。 然而,如果更新变得频繁,那么数据的大部分时间将被专门锁定,并且并发性增加很少。 此外,如果读取操作太短,则读写锁定实现(其本身比互斥锁更复杂)的开销可以支配执行成本,特别是因为许多读写锁定实现仍将序列化所有线程通过小部分代码。 最终,只有剖析和测量将确定使用读写锁是否适合您的应用程序。

模拟缓存

public class ReadWriteLockDemo{
    public static void main(String[] args){
        MyCache cache = new MyCache();
        //读写分离
        //写入
        for(int i=1;i<=10;i++){
            final int temp = i;
            new Thread(()->{
                cache.put(temp+"",tem+"");
            },String.valueOd(i).start());
        }
        //读取
        for(int i=1;i<=10;i++){
            final int temp = i;
            new Thread(()->{
                cache.get(tem+"");
            },String.valueOf(i)).start();
        }
    }
}
class MyCache{
    private volatile Map<String,Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//创建读写锁
    //lock的创建对比 Lock lock = new ReentrantLock();
    //存操作(写入)的时候,只允许一个线程执行
    public void put(String key,Object value){
        readWriteLock.writeLock().lock(); //开启写入锁
        //lock的开启对比 lock.lock(); 
        try{
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"已写入"+key);
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            readWriteLock.writeLock().unlock(); //关闭锁
            //lock的关闭对比 lock.unlock(); 
        }
        public void get(String key){
        readWriteLock.readLock().lock(); //开启写入锁
        //lock的开启对比 lock.lock(); 
        try{
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            map.get(key);
            System.out.println(Thread.currentThread().getName()+"已读取"+key);
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            readWriteLock.readLock().unlock(); //关闭锁
            //lock的关闭对比 lock.unlock(); 
        }
    }
}

虽然读写锁的基本操作是直接的,但是执行必须做出许多策略决策,这可能会影响给定应用程序中读写锁定的有效性。 这些政策的例子包括:

  • 在写入器释放写入锁定时,确定在读取器和写入器都在等待时是否授予读取锁定或写入锁定。作家偏好是常见的,因为写作预计会很短,很少见。 读者喜好不常见,因为如果读者经常和长期的预期,写作可能导致漫长的延迟。 公平的或“按顺序”的实现也是可能的。
  • 确定在读卡器处于活动状态并且写入器正在等待时请求读取锁定的读取器是否被授予读取锁定。 读者的偏好可以无限期地拖延作者,而对作者的偏好可以减少并发的潜力。
  • 确定锁是否可重入:一个具有写锁的线程是否可以重新获取? 持有写锁可以获取读锁吗? 读锁本身是否可重入?
  • 写入锁可以降级到读锁,而不允许插入写者? 读锁可以升级到写锁,优先于其他等待读者或作者吗?

在评估应用程序的给定实现的适用性时,应考虑所有这些问题。

阻塞队列

意义:使得线程安全

BlockingQueue阻塞队列,线程安全的

注:synchronousQueue是同步队列,不存储元素

队列出现的阻塞

  • 队列满插入数据,队列阻塞
  • 队列空取出数据,队列阻塞

BlockingQueue的操作有四种形式,这四种形式对阻塞的处理方式不同

  • 第一种是抛出异常
  • 第二种是返回一个特殊值(null或者false,具体取决于操作)
  • 第三种是在操作可以成功前,无限期地阻塞当前线程;
  • 第四种是在放弃前 只在给定的最大时间限制内阻塞
抛出异常 有返回值,不抛出异常 阻塞等待 超时等待
插入 add(e) offer(e) put(e) offer(e,time,unit)
移出 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用
BlockingQueue<String> blockingQueue = new BlockingQueue<String>(3);  //只能存储三个数据

线程池

线程是调度cpu的最小单元,也叫轻量级进程LMP(Light Weight Process)

线程的创建存在的问题

  • 线程频繁创建和销毁(性能开销)
  • 线程创建的数量控制(上下文切换)

因此引入了线程池!!!

​ java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。

  1. 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  2. 思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。
  3. 优点
    • 提高响应速度,减少了创建新线程的时间降低资源消耗
    • 重复利用线程池中线程,不需要每次都创建
    • 便于线程管理

线程池:三大方法,七大参数,四种拒绝策略

阿里巴巴开发手册规定,线程池不允许使用Executors去创建,而是通过ThreadPoolExcutor的方式,这样自定义线程池可以规避资源耗尽的风险

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

  • FixedThreadPool 和 SingleThreadPool:

​ 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

  • CachedThreadPool:

​ 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

三大方法

三大方法,指的是juc中的Excutors工具类中创建线程池的三种方法

CachedThreadPool

Executors.newCachedThreadPool()

  • 当时当任务积压过多,不断的创建线程将会给cpu带来巨大负担的同时也会导致OOM

SingleThreadPool

Executors.newSingleThreadExecutor()

  • 会出现内存溢出现象(oom),当任务积压过多超过LinkedBlockingQueue队列最大值

FixedThreadPool

Executors.newFixedThreadPool(int nThreads)

  • 会出现内存溢出现象(oom),当任务积压过多超过LinkedBlockingQueue队列最大值

总结:在一线互联网中任务量很大,不允许使用自带的创建线程池的方式,需要手动创建

七大参数

也就是ThreadPoolExecutor()的七个参数,Excutors中创建线程池的三个方法底层就是创建ThreadPoolExecutor对象并设置了不同的参数而已

自定义创建线程池就是自主设置七大参数

ExecutorService threadPool = new TheadPoolExecutor(
    int corePoolSize,     //1.核心线程数
    int maximumPoolSize,  //2.最大核心线程数
    long keepAliveTime,   //3.非核心线程的生存时间(超时时间)
    TimeUnit unit,        //4.非核心线程的生存时间(超时时间)的单位
    BlockingQueue<Runnable> workQueue,  //5.阻塞队列
    ThreadFactory threadFactory,  //6.Executors创建的线程工厂,一般采用默认Executors.defaultThreadFactory()
    RejectedExecutionHandler handler   //7.拒绝策略
)

四种拒绝策略

1.AbortPolicy

  • 抛异常 Java中默认的策略
  • 直接抛出拒绝异常(继承自RuntimeException),会中断调用者的处理过程

2.CallerRunsPolicy

  • 在调用者线程中,运行当前被丢弃的任务
  • 也就是说该任务从哪来回哪去执行,比如回到main线程执行,不抛出异常

3.DiscardOldestPolicy

  • 丢弃队列中最老的任务,然后再次尝试提交新的任务

4.DiscardPolicy

  • 队列满,丢弃无法加载的任务,不抛出异常
new ThreadPoolExecutor.AbortPolicy()
new ThreadPoolExecutor.CallerRunsPolicy()
new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardPolicy()

自定义拒绝策略

  • 当以上都不合适时,可以自定义符合场景的拒绝策略,需要实现RejectedExecutionHandler接口,并将自己的逻辑写在RejectedExecutionHandler方法内
package thread.stu.com;

import java.util.concurrent.*;

public class RejecctedExecutionHandlerDemo {
    public static void main(String[] args) throws Exception {
       // RejectedExecutionHandler reject  = null;
        //线程池的四种拒绝策略
        //reject = new ThreadPoolExecutor.AbortPolicy();         //默认,队列满了爹任务抛出异常
        // reject = new ThreadPoolExecutor.CallerRunsPolicy();    //  如果添加到线程池失败,那么由主线程去执行该任务
        // reject = new ThreadPoolExecutor.DiscardPolicy();       //  队列满了丢任务不异常
         //reject = new ThreadPoolExecutor.DiscardOldestPolicy();  //  将最早进入队列的任务删除,之后再次尝试加入队列

        //自定义拒绝策略
        CustomRejectedExecutionHandler  reject = new CustomRejectedExecutionHandler();
        //自定义线程池
        
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            1,
            1,
            0L,
            TimeUnit.MILLISECONDS, 
            new ArrayBlockingQueue<Runnable>(2),
            reject);
        try{
             for (int i = 1; i <= 1000; i++) {
                 threadPool.execute(new Test(i));
             }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //线程池使用完毕后,关闭线程池!
            threadPoll.shutdown();
        }
       
    }
}
/**
 * 自定义拒绝策略
 */
class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            // 核心改造点,由blockingqueue的offer改成put阻塞方法
            executor.getQueue().put(r);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Test implements  Runnable{
    int i ;
    public Test(int i){
        this.i = i;

    }
    public void run(){
        System.out.println(Thread.currentThread().getName()+"---"+i);
        try{Thread.sleep(1000L);}catch (InterruptedException e){}
    }
}

关闭线程池

上述代码进行了 线程池使用完毕后,关闭线程池的操作,是为了避免资源的浪费

关闭线程池有两个shutdown(),shutdownNow()

shutdown()/shutdownNow() 的区别

  • shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断

  • shutdownNow是将线程池的状态设置为STOP,正在执行的任务则会被停止,没有被执行的则返回

优先级

这里指提交优先级执行优先级

提交优先级:任务先被提交到核心线程执行,任务数大于核心线程,任务提交到阻塞队列,阻塞队列满,任务提交给非核心线程

执行优先级:先执行核心线程的任务,再执行非核心线程的任务,最后执行阻塞队列的任务

提交优先级

源码分析:

  if (workerCountOf(c) < corePoolSize) {
            //判断核心线程池是否已满
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            //线程处于运行态且队列是否已满
            //offer 相当于add,add的底层逻辑使用的就是offer,只不过add有两种返回值,Boolean和异常处理语句,而offer只有Boolean
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
         //核心线程池且队列也满
        else if (!addWorker(command, false))
            reject(command);

线程池的处理流程

  1. 用户提交任务
  2. 判断核心线程池是否已满 否:创建线程池 是:执行步骤3
  3. 判断队列是否已满 否:将任务存储在队列中 是:执行步骤4
  4. 线程池是否已满 否:创建线程执行任务 是:执行步骤5
  5. 按照策略处理无法执行的步骤

执行优先级

流程:

  1. 用户提交任务 ----- execute
  2. 创建Work对象 ------ addWorker
  3. 启动Work对象 ------ runWorker
  4. 获取任务 -------- getTask
  5. 启动执行任务 ---- processWorkerExit

产生差异的核心代码

while (task != null || (task = getTask()) != null)

逻辑或,当task != null满足时就不会执行(task = getTask()) != null

场景:

银行开了2个窗口办业务(核心线程),3个临时窗口(非核心数),候客厅有5个位置(阻塞队列最大容量),银行最大能容纳10个(最大承载=最大线程数+队列)人。
当有陆续来了10个人办理业务,前两个人到窗口办理,后面5个人只能在候客厅等待,此时又有3人来办理业务,银行只能开启3个临时窗口让该3个人办理业务,其他人来办理业务只能拒绝了(采取拒绝策略)

线程池线程复用原理

在执行中,procWorkerExit 调用 addWorker 实现线程循环利用

//源码
if (runStateLessThan(c, STOP)) {
    if (!completedAbruptly) {
        int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
        if (min == 0 && ! workQueue.isEmpty())
            min = 1;
        if (workerCountOf(c) >= min)
            return; // replacement not needed
    }
    addWorker(null, false);
}

线程池的调优

自定义线程池中,先明确我们是对什么参数进行优化的,池的大小我们如何去设置呢?

int corePoolSize,     //1.核心线程数
int maximumPoolSize,  //2.最大核心线程数
long keepAliveTime,   //3.非核心线程的生存时间(超时时间)
TimeUnit unit,        //4.非核心线程的生存时间(超时时间)的单位
BlockingQueue<Runnable> workQueue,  //5.阻塞队列
ThreadFactory threadFactory,  //6.Executors创建的线程工厂,一般采用默认Executors.defaultThreadFactory()
RejectedExecutionHandler handler   //7.拒绝策略

方式有两种:CPU密集型,IO密集型

CPU密集型

cpu密集型就是根据当前服务器(电脑)的cpu是几核就设计几,使CPU利用率达到最大

//获取CPU的核数
Runtime.getRuntime().availableProcessor()
  ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            1,
            Runtime.getRuntime().availableProcessor(),
            0L,
            TimeUnit.MILLISECONDS, 
            new ArrayBlockingQueue<Runnable>(2),
            reject);

IO密集型

因为IO操作非常消耗资源,因此可以判断程序中非常耗IO的线程数,再设置两到三倍的该线程数即为线程池的大小

ForkJoin

前言

在Java7之前想要并行处理大量数据是很困难的,首先把数据拆分成很多个部分,然后把这这些子部分放入到每个线程中去执行计算逻辑,最后在把每个线程返回的计算结果进行合并操作;在Java7中提供了一个处理大数据的fork/join框架,屏蔽掉了线程之间交互的处理,更加专注于数据的处理。

在JDK中,提供了这样一种功能:它能够将复杂的逻辑(任务)拆分成一个个简单的逻辑来并行执行,待每个并行执行的逻辑执行完成后,再将各个结果进行汇总,得出最终的结果数据。有点像Hadoop中的MapReduce。

ForkJoin是由JDK1.7之后提供的多线程并发处理框架。ForkJoin框架的基本思想是分而治之。分而治之就是将一个复杂的计算,按照设定的阈值分解成多个计算,然后将各个计算结果进行汇总。相应的,ForkJoin将复杂的计算当做一个任务,而分解的多个计算则是当做一个个子任务来并行执行。

Java并发编程的发展

对于Java语言来说,生来就支持多线程并发编程,在并发编程领域也是在不断发展的。Java在其发展过程中对并发编程的支持越来越完善也正好印证了这一点。

  • Java 1 支持thread,synchronized。
  • Java 5 引入了 thread pools, blocking queues, concurrent collections,locks, condition queues。
  • Java 7 加入了fork-join库。
  • Java 8 加入了 parallel streams。

并发与并行

有一个很好的例子:一个人在吃饭,此时有一个电话来了,先接电话再回去吃饭,说明支持并发,边吃饭边接电话,说明支持并行,吃完饭再接电话就说明不支持并发也不支持并行

并发

并发指的是在同一时刻,只有一个线程能够获取到CPU执行任务,而多个线程被快速的轮换执行,这就使得在宏观上具有多个线程同时执行的效果,并发不是真正的同时执行,并发可以使用下图表示。

深入JUC(高并发编程)_第4张图片

并行

并行指的是无论何时,多个线程都是在多个CPU核心上同时执行的,是真正的同时执行

深入JUC(高并发编程)_第5张图片

分治法

基本思想

  • 把一个规模大的问题划分为规模较小的子问题,然后分而治之,最后合并子问题的解得到原问题的解。底层维护的是双端队列

步骤

①分割原问题;

②求解子问题;

③合并子问题的解为原问题的解。

我们可以使用如下伪代码来表示这个步骤。

if(任务很小){
    直接计算得到结果
}else{
    分拆成N个子任务
    调用子任务的fork()进行计算
    调用子任务的join()合并计算结果
}

在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。

package org.example.forkJoin;
import java.util.concurrent.*;
import java.util.stream.LongStream;

public class ForkJionDemo {
    public static void main(String[] args ) throws ExecutionException, InterruptedException {
        test1();  //计算结果为:500000000500000000,计算时长为:342
        test2();  //计算结果为:500000000500000000,计算时长为:232
        test3();  //计算结果为:500000000500000000,计算时长为:207

    }
    //普通:直接进行累加
    public static void test1(){
        long start = System.currentTimeMillis();
        long sum = 0L;
        for(long i=1L;i<=10_0000_0000L;i++){
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("计算结果为:"+sum);
        System.out.println("计算时长为:"+(end-start));
    }
    //中级:使用Forkjoin
    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new SumForkJoinDome(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task); //提交任务
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("计算结果为:"+sum);
        System.out.println("计算时长为:"+(end-start));
    }

    //高级:使用Stream并行流,后面会介绍
    public static void test3(){
        long start = System.currentTimeMillis();
        long sum = LongStream.rangeClosed(0L,10_0000_0000L)
                .parallel()
                .reduce(0,Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("计算结果为:"+sum);
        System.out.println("计算时长为:"+(end-start));

    }
}


//任务:对很大的数据进行累加,如:1-100_0000_0000进行累加
class SumForkJoinDome extends RecursiveTask<Long> {
    //RecursiveAction : 递归事件,无返回值
    //RecursiveTask :   递归任务,有返回值

    //步骤1:继承RecursiveAction 或 RecursiveTask
    //步骤2:重写方法,这里重写了compute方法

    private Long start;
    private Long end;

    //步骤3:设置阀门值,当拆分成10000L,停止递归(停止拆分),即任务大于阈值,就分裂成两个子任务计算
    private Long temp = 10000L;
    public SumForkJoinDome(long start,long end){
        this.start=start;
        this.end=end;
    }

    @Override
    protected Long compute() {
        if((end-start)< temp) {
            long sum = 0L;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }else{
            long middle = (start+end)/2; //中间值,便于分割问题
            SumForkJoinDome task1 = new SumForkJoinDome(start,middle);
            task1.fork(); //拆分任务,把任务压入线程队列
            SumForkJoinDome task2 = new SumForkJoinDome(middle+1L,end);
            task2.fork(); //拆分任务,把任务压入线程队列
            return task1.join()+task2.join();
        }
    }
}

典型应用

  • 二分搜索
  • 大整数乘法
  • Strassen矩阵乘法
  • 棋盘覆盖
  • 合并排序
  • 快速排序
  • 线性时间选择
  • 汉诺塔

ForkJoin并行处理框架

ForkJoin框架概述

Java 1.7 引入了一种新的并发框架—— Fork/Join Framework,主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数。

ForkJoin框架的本质是一个用于并行执行任务的框架, 能够把一个大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务的计算结果。在Java中,ForkJoin框架与ThreadPool共存,并不是要替换ThreadPool

其实,在Java 8中引入的并行流计算,内部就是采用的ForkJoinPool来实现的。例如,下面使用并行流实现打印数组元组的程序。

public class SumArray {
    public static void main(String[] args){
        List<Integer> numberList = Arrays.asList(1,2,3,4,5,6,7,8,9);
        numberList.parallelStream().forEach(System.out::println);
    }
}

这段代码的背后就使用到了ForkJoinPool。

说到这里,可能会提出问:可以使用线程池的ThreadPoolExecutor来实现啊?为什么要使用ForkJoinPool啊?ForkJoinPool是个什么鬼啊?! 接下来,我们就来回答这个问题。

ForkJoin框架原理

ForkJoin框架是从jdk1.7中引入的新特性,它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入指定的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

ForkJoinPool主要使用**分治法(Divide-and-Conquer Algorithm)**来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool能够使用相对较少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概200万+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程

无法向任务队列中再添加一个任务并在等待该任务完成之后再继续执行

而使用ForkJoinPool就能够解决这个问题,它就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。

那么使用ThreadPoolExecutor或者ForkJoinPool,性能上会有什么差异呢?

首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,很显然这是不可行的,也是很不合理的!!

工作窃取算法

​ 假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

简单来说:任务完成的线程窃取未完成任务的线程的部分任务执行,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

工作窃取算法的优点:
充分利用线程进行并行计算,并减少了线程间的竞争。

工作窃取算法的缺点:

  • 在某些情况下还是存在竞争,比如双端队列里只有一个任务时,由于工作窃取特性,会竞争最后一个任务。
  • 该算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。

Fork/Join框架局限性:

对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程查找其他未被执行的任务,并开始执行这些未被执行的任务,通过这种方式,线程充分利用它们的运行时间来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性。

(1)任务只能使用Fork和Join操作来进行同步机制,如果使用了其他同步机制,则在同步操作时,工作线程就不能执行其他任务了。比如,在Fork/Join框架中,使任务进行了睡眠,那么,在睡眠期间内,正在执行这个任务的工作线程将不会执行其他任务了。
(2)在Fork/Join框架中,所拆分的任务不应该去执行IO操作,比如:读写数据文件。
(3)任务不能抛出检查异常,必须通过必要的代码来出来这些异常。

ForkJoin框架的实现

ForkJoin框架中一些重要的类如下所示。

深入JUC(高并发编程)_第6张图片

ForkJoinPool 框架中涉及的主要类如下所示。

1.ForkJoinPool类

实现了ForkJoin框架中的线程池,由类图可以看出,ForkJoinPool类实现了线程池的Executor接口。

我们也可以从下图中看出ForkJoinPool的类图关系。

深入JUC(高并发编程)_第7张图片

其中,可以使用Executors.newWorkStealPool()方法创建ForkJoinPool。

ForkJoinPool中提供了如下提交任务的方法。

public void execute(ForkJoinTask<?> task)
public void execute(Runnable task)
public <T> T invoke(ForkJoinTask<T> task)
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) 
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)
public <T> ForkJoinTask<T> submit(Callable<T> task)
public <T> ForkJoinTask<T> submit(Runnable task, T result)
public ForkJoinTask<?> submit(Runnable task)

2.ForkJoinWorkerThread类

实现ForkJoin框架中的线程。

3.ForkJoinTask类

ForkJoinTask封装了数据及其相应的计算,并且支持细粒度的数据并行。ForkJoinTask比线程要轻量,ForkJoinPool中少量工作线程能够运行大量的ForkJoinTask。

ForkJoinTask类中主要包括两个方法fork()和join(),分别实现任务的分拆与合并。

fork()方法类似于Thread.start(),但是它并不立即执行任务,而是将任务放入工作队列中。跟Thread.join()方法不同,ForkJoinTask的join()方法并不简单的阻塞线程,而是利用工作线程运行其他任务,当一个工作线程中调用join(),它将处理其他任务,直到注意到目标子任务已经完成。

我们可以使用下图来表示这个过程。

深入JUC(高并发编程)_第8张图片

ForkJoinTask有3个子类:

深入JUC(高并发编程)_第9张图片

  • RecursiveAction:无返回值的任务。
  • RecursiveTask:有返回值的任务。
  • CountedCompleter:完成任务后将触发其他任务。

4.RecursiveTask 类

有返回结果的ForkJoinTask实现Callable。

5.RecursiveAction类

无返回结果的ForkJoinTask实现Runnable。

6.CountedCompleter 类

在任务完成执行后会触发执行一个自定义的钩子函数。

ForkJoin示例程序

package io.binghe.concurrency.example.aqs;
 
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
@Slf4j
public class ForkJoinTaskExample extends RecursiveTask<Integer> {
    public static final int threshold = 2;
    private int start;
    private int end;
    public ForkJoinTaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Integer compute() {
        int sum = 0;
        //如果任务足够小就计算任务
        boolean canCompute = (end - start) <= threshold;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任务大于阈值,就分裂成两个子任务计算
            int middle = (start + end) / 2;
            ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
            ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
 
            // 执行子任务
            leftTask.fork();
            rightTask.fork();
 
            // 等待任务执行结束合并其结果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
 
            // 合并子任务
            sum = leftResult + rightResult;
        }
        return sum;
    }
    public static void main(String[] args) {
        ForkJoinPool forkjoinPool = new ForkJoinPool();
 
        //生成一个计算任务,计算1+2+3+4
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
 
        //执行一个任务
        Future<Integer> result = forkjoinPool.submit(task);
 
        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}

并发编程需要掌握的核心技能知识图

深入JUC(高并发编程)_第10张图片

参考博客:https://www.cnblogs.com/binghe001/p/12683253.htm

并行流(Stream)

前言

  • Stream API(java.util.stream.*) Stream 是JAVA8中处理集合的关键抽象概念,
  • 它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。
  • 使用Stream API对集合数据进行操作,就类似于使用SQL执行数据查询一样。
  • 可使用StreamAPI做并行操作,

总之,StreamAPI提供了一种高效且易于使用的处理数据的方式。

Stream是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲的是数据,流讲的是计算。即存储交给集合,计算交给stream流

注意:

1 . Stream自己不会存储元素。

2 . Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream

3 . Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

并行流工作

​ 并行流就是把一个内容分成多个数据块,并用不同的线程分成多个数据块,并用不同的线程分别处理每个数据块的流。
JAVA8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过parallel() 与sequential() 在并行流与顺序流之间进行切换。其实JAVA8底层是使用JAVA7新加入的Fork/Join框架:

Fork/Join框架与传统线程池的区别:
采用“工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行.那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能.

并行流与其他方式对比
在ForkJoin一节代码运行结果:

        test1();  //普通for,计算结果为:500000000500000000,计算时长为:342
        test2();  //ForkJoin 计算结果为:500000000500000000,计算时长为:232
        test3();  //并行流Stream,计算结果为:500000000500000000,计算时长为:207

通过对比,Stream和Fork/Join框架在大数据的时候速度还是挺快的,For循环在数据小的时候是最快的

Stream的一些常规操作

package org.example.stream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class StreamDemo {
    /**
     *  任务要求:
     *  现在有5个用户,对5个用户进行相应筛选
     *  1.ID必须是偶数
     *  2.年龄必须大于23岁
     *  3.用户名转为大写字母
     *  4.用户名字母倒着排序
     *  5.只输出一个用户
     *
     */
    public static void main(String[] args) {
        User u1 = new User(1,"a",21);
        User u2 = new User(2,"b",22);
        User u3 = new User(3,"c",23);
        User u4 = new User(4,"d",24);
        User u5 = new User(5,"e",25);
        //存储数据交给集合
        List<User> list = Arrays.asList(u1,u2,u3,u4,u5);
        //计算交给并行流stream
        list.stream()
                .filter(u->{return u.getId()%2==0;})   //筛选ID是偶数 u2 u4
                .filter(u->{return u.getAge()>23;})    //筛选年龄大于23岁
                //.map(u->{return u.getName().toUpperCase(Locale.ROOT);})   // 用户名转为大写字母
                //.sorted((uu1,uu2)->{return uu2.compareTo(uu1);}) // 用户名字母倒着排序
                //.limit(1)  //只输出一个用户
                //筛选条件可叠加
                .forEach(System.out::println);
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
    int id;
    String name;
    int age;
}

parallelStream

parallelStream底层是通过Fork/Join框架来实现的。

常见的使用方式

1.串行流转化成并行流

LongStream.rangeClosed(1,1000)
                .parallel()
                .forEach(System.out::println);

2.直接生成并行流

 List<Integer> values = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            values.add(i);
        }
        values.parallelStream()
                .forEach(System.out::println);
  • 正确的使用parallelStream

我们使用parallelStream来实现上面的累加例子看看效果,代码如下:

public static void main(String[] args) {
    Summer summer = new Summer();
    LongStream.rangeClosed(1, 100000000)
            .parallel()
            .forEach(summer::add);
    System.out.println("result:" + summer.sum);

}

static class Summer {
    public long sum = 0;

    public void add(long value) {
        sum += value;
    }
}

result

运行之后,我们发现运行的结果不正确,并且每次运行的结果都不一样,这是为什么呢?
这里其实就是错用parallelStream常见的情况,parallelStream是非线程安全的,在这个里面中使用多个线程去修改了共享变量sum, 执行了sum += value操作,这个操作本身是非原子性的,所以在使用并行流时应该避免去修改共享变量。

修改上面的例子,正确使用parallelStream来实现,代码如下:

long result = LongStream.rangeClosed(1, 100000000)
        .parallel()
        .reduce(0, Long::sum);
System.out.println("result:" + result);

在前面我们已经说过了fork/join的操作流程是:拆子部分,计算,合并结果;因为parallelStream底层使用的也是fork/join框架,所以这些步骤也是需要做的,但是从上面的代码,我们看到Long::sum做了计算,reduce做了合并结果,我们并没有去做任务的拆分,所以这个过程肯定是parallelStream已经帮我们实现了,这个时候就必须的说说Spliterator

Spliterator是Java8加入的新接口,是为了并行执行任务而设计的。

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);

    Spliterator<T> trySplit();

    long estimateSize();

    int characteristics();
}

tryAdvance: 遍历所有的元素,如果还有可以遍历的就返回ture,否则返回false

trySplit: 对所有的元素进行拆分成小的子部分,如果已经不能拆分就返回null

estimateSize: 当前拆分里面还剩余多少个元素

characteristics: 返回当前Spliterator特性集的编码

总结

  1. 要证明并行处理比顺序处理效率高,只能通过测试,不能靠猜测
  2. 数据量较少,并且计算逻辑简单,通常不建议使用并行流
  3. 需要考虑流的操作时间消耗
  4. 在有些情况下需要自己去实现拆分的逻辑,并行流才能高效

异步回调

场景:某个方法中需要拿到另方法的返回值,但是该方法需要消耗10秒左右才能执行完并返回结果,但是我们不想等待这10秒时间,想继续执行下面的代码,怎么处理?

此时,使用异步回调即可!

我们常用的一些请求都是同步回调的,同步回调是阻塞的,单个的线程需要等待结果的返回才能继续执行。

​ 有的时候,我们不希望程序在某个执行方法上一直阻塞,需要先执行后续的方法,那就是这里的异步回调。我们在调用一个方法时,如果执行时间比较长,我们可以传入一个回调的方法,当方法执行完时,让被调用者执行给定的回调方法。

异步回调有两种:无返回值的异步回调runAsync,有返回值的异步回调

package org.example.future;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

//实现异步回调
public class FutureDemo {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();

        //没有返回值的runAsync异步回调
        CompletableFuture<Void> objectCompletableFuture = CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName()+"runAsync=>void");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName()+"执行");
        long end = System.currentTimeMillis();
        System.out.println("计算时长为:"+(end-start));
    }
}
//按道理应该需要10秒执行完,但是使用了异步回调只耗时5点几秒
//运行结果:
/**
main执行
ForkJoinPool.commonPool-worker-1runAsync=>void
计算时长为:5060
*/

        //有返回值的异步回调

        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"有返回值回调:supplyAsynch");
            return 1024;
        });
        completableFuture2.get();  //获取阻塞执行结果

        //有返回值的异步回调:成功回调/失败回调
        CompletableFuture<Integer> completableFuture3 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"有返回值回调:supplyAsynch");
            int k = 10/0;
            return 1024;
        });
        completableFuture3.whenComplete((t,u)->{
            //成功回调
            System.out.println("t=>"+t);
            System.out.println("u=>"+u);
        }).exceptionally((e)->{
            //失败回调
            // e.printStackTrace();
            System.out.println("失败回调:运算出错");
            System.out.println(e.getMessage());
            return 505;
        }).get();

    }
}

volatile

在多线程编程中,若干个线程为了实现公共资源的操作,往往是复制相应变量的副本,待操作完成后再将此副本变量数据与原始变量进行同步处理,如果开发者不希望通过副本数据进行操作,而是希望可以直接进行原始变量的操作(节约了复制变量副本与同步的时间),则可以在变量声明使使用volatile关键,volatile无法描述同步处理,他只是一种直接内存的处理,避免了副本的操作,而synchronized是实现同步操作的关键字,volatile主要在属性上使用,synchronized在代码块与方法上使用的,

volatile是Java虚拟机提供的轻量级同步机制

volatile三大特性

  • 保证原子可见性

    可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。

  • 不保证原子性

    原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

  • 禁止指令重排

    有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。

volatile很容易被误用,用来进行原子性操作。

如果要深入了解volatile关键字的作用,就必须先来了解一下JVM在运行时候的内存分配过程。

在 JVM一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,

线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存

变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,

在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

深入JUC(高并发编程)_第11张图片

深入JUC(高并发编程)_第12张图片

那么在了解完JVM在运行时候的内存分配过程以后,我们开始真正深入的讨论volatile的具体作用

package org.example.volatileDemo;
public class VolatileTestDemo01 extends Thread {
    boolean flag = false;
    int i = 0;
    public void run() {
        while (!flag) {
            i++;
        }
    }
    public static void main(String[] args) throws Exception {
        VolatileTestDemo01 vt = new VolatileTestDemo01();
        vt.start();
        Thread.sleep(2000);
        vt.flag = true;
        System.out.println("stope" + vt.i);
    }
}

上面的代码是通过标记flag来控制VolatileTest线程while循环退出的例子!

下面用伪代码来描述一下程序

  • 首先创建 VolatileTest vt = new VolatileTest();
  • 然后启动线程 vt.start();
  • 暂停主线程2秒(Main) Thread.sleep(2000);
  • 这时的vt线程已经开始执行,进行i++;
  • 主线程暂停2秒结束以后将 vt.flag = true;
  • 打印语句 System.out.println(“stope” + vt.i); 在此同时由于vt.flag被设置为true,所以vt线程在进行下一次while判断 while (!flag) 返回假 结束循环 vt线程方法结束退出!
  • 主线程结束

上面的叙述看似并没有什么问题,“似乎”完全正确。那就让我们把程序运行起来看看效果吧,执行mian方法。2秒钟以后控制台打印stope-202753974。

可是奇怪的事情发生了 程序并没有退出。vt线程仍然在运行,也就是说我们在主线程设置的 vt.flag = true;没有起作用。

在这里我需要说明一下,有的同学可能在测试上面代码的时候程序可以正常退出。那是因为你的JVM没有优化造成的!在DOC下面输入 java -version 查看 如果显示Java HotSpot™ … Server 则JVM会进行优化。

如果显示Java HotSpot™ … Client 为客户端模式,需要设置成Server模式 设置方法问Google

深入JUC(高并发编程)_第13张图片

问题出现了,为什么我在主线程(main)中设置了vt.flag = true; 而vt线程在进行判断flag的时候拿到的仍然是false?

那么按照我们上面所讲的 “JVM在运行时候的内存分配过程” 就很好解释上面的问题了。

首先 vt线程在运行的时候会把 变量 flag 与 i (代码3,4行)从“主内存” 拷贝到 线程栈内存(上图的线程工作内存)

然后 vt线程开始执行while循环

        while (!flag) {
           i++;
        }

while (!flag)进行判断的flag 是在线程工作内存当中获取,而不是从 “主内存”中获取

i++; 将线程内存中的i++; 加完以后将结果写回至 “主内存”,如此重复。

然后再说说主线程的执行过程。 我只说明关键的地方

vt.flag = true;

主线程将vt.flag的值同样 从主内存中拷贝到自己的线程工作内存 然后修改flag=true. 然后再将新值回到主内存。

这就解释了为什么在主线程(main)中设置了vt.flag = true; 而vt线程在进行判断flag的时候拿到的仍然是false。那就是因为vt线程每次判断flag标记的时候是从它自己的“工作内存中”取值,而并非从主内存中取值!

这也是JVM为了提供性能而做的优化。那我们如何能让vt线程每次判断flag的时候都强制它去主内存中取值呢。这就是volatile关键字的作用。

再次修改我们的代码:

public class VolatileTest extends Thread { 
    volatile boolean flag = false;
    int i = 0; 
    public void run() {
        while (!flag) {
            i++;
        }
    }
    public static void main(String[] args) throws Exception {
        VolatileTest vt = new VolatileTest();
        vt.start();
        Thread.sleep(2000);
        vt.flag = true;
        System.out.println("stope" + vt.i);
    }
}

在flag前面加上volatile关键字,强制线程每次读取该值的时候都去“主内存”中取值。在试试我们的程序吧,已经正常退出了。

指令重排

指令的基本概念

​ 指令是指示计算机执行某种操作的命令,如:数据传送指令、算术运算指令、位运算指令、程序流程控制指令、串操作指令、处理器控制指令。指令不同于我们所写的代码,一行代码按照操作的逻辑可以分成多条指令。

举个例子:int a = 1; 这段代码大致可以分为两条指令:1.加载常量1;2.将常量1赋值给变量a。

反编译获取指令

1.创建一个Test.java文件,并输入内容

2.使用javac命令编译Test.java文件,得到Test.class文件

3.使用Sublime3打开Test.class

4.使用javap命令,反编译Test.class文件

使用命令:$ javap -v Test

如下输出为:

Classfile /Users/soldier/Desktop/Test.class
  Last modified 2018-9-19; size 265 bytes
  MD5 checksum 551399dd9890a81e8f8c079c6c1f364d
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."":()V
   #2 = Fieldref           #3.#16         // Test.m:I
   #3 = Class              #17            // Test
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               Test
  #18 = Utf8               java/lang/Object
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 4: 0
}
SourceFile: "Test.java"

使用命令:$ javap -c Test

输出为:

Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public int inc();
    Code:
       0: aload_0
       1: getfield      #2                  // Field m:I
       4: iconst_1
       5: iadd
       6: ireturn
}

用法摘要

-help 帮助
-l 输出行和变量的表
-public 只输出public方法和域
-protected 只输出public和protected类和成员
-package 只输出包,public和protected类和成员,这是默认的
-p -private 输出所有类和成员
-s 输出内部类型签名
-c 输出分解后的代码,例如,类中每一个方法内,包含java字节码的指令,
-v 输出栈大小,方法参数的个数
-constants 输出静态final常量

指令的基本概念

指令是指示计算机执行某种操作的命令,如:数据传送指令、算术运算指令、位运算指令、程序流程控制指令、串操作指令、处理器控制指令。指令不同于我们所写的代码,一行代码按照操作的逻辑可以分成多条指令。

举个例子:int a = 1; 这段代码大致可以分为两条指令:1.加载常量1;2.将常量1赋值给变量a。

指令重排序

Java语言规范JVM线程内部维持顺序花语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。

源代码到最终执行的指令序列示意图

img

指令重排序主要分为三种:

1.编译器重排序:JVM中进行完成的

2.指令级并行重排序

3.处理器重排序:CPU中进行完成的

As-If-Serial语义

as-if-serial语义的意思是:不管怎么进行指令重排序,单线程内程序的执行结果不能被改变。编译器,处理器进行指令重排序都必须要遵守as-if-serial语义规则。

为了遵守as-if-serial语义,编译器和处理器对存在依赖关系的操作,都不会对其进行重排序,因为这样的重排序很可能会改变执行的结果,但是对不存在依赖关系的操作,就有可能进行重排序。

Happens-Before原则

happens-before可以理解为“先于”,是用来指定两个操作之间的执行顺序,由于这个两个操作可以在一个线程之内,也可以在不同线程之间。因此,JMM可以通过happens-before关系来保证跨线程的内存可见性(如果A线程是对变量进行写操作,而B线程是对变量进行读操作,那么如果A线程是先于B线程的操作,那么A线程写操作之后的数据对B线程也是可见的)

具体的定义:

1.如果一个操作“先于”另一个操作,那么第一操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;

2.两个操作是happens-before的关系,也并不意味着JVM会按照这个关系去顺序执行,因为会存在重排序的可能,但是进行了重排序的执行结果,与此happens-before的关系顺序执行的结果一致的话,那就说明这个重排序是合法的(也就是JVM允许这样的重排序)。

具体规则

1.程序顺序规则:在一个线程内必须保证语义串行性,也就是按照代码顺序执行;

2.监视器(管程)锁规则:无论是单线程还是对线程环境,对于同一个锁来说,解锁操作必须是先于后一个加锁操作之前(如果A线程进行了加锁,还未进行解锁,那么B线程是不可能进行加锁操作的,只有等到A线程进行解锁操作之后,才能再进行加锁操作),而且前者线程解锁之后,对数据的操作对于后者加锁的线程是可见的;

3.volatile规则:volatile变量的写先于变量的读,保证了volatile变量的可见性,简而言之,volatile变量每次别线程访问时,都强迫从主内存中读该变量的值,而当变量的值被修改时,又会强迫将最新的值从工作内存刷回主内存中,任何时刻,不同线程总是能获取到该变量的最新值;

4.线程启动规则:线程的start方法先于此线程run方法中的所有操作(线程一定是执行start方法之后,才会执行真正的run方法逻辑),如果A线程在执行过程中,执行B线程的start方法,那么在A线程执行过程中到B线程start这一段区域中对共享变量的修改,对线程B是可见的;

深入JUC(高并发编程)_第14张图片

5.线程终止规则:线程run方法中的执行操作一定是先于此线程的join方法的,如果B线程修改了共享变量的值,那么在B线程执行join方法之后,主线程一定对此共享变量是可见的;

深入JUC(高并发编程)_第15张图片

6.线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断;

7.传递性:A先于B,B先于C,那么一定可以知道A先于C;

8.对象终结规则:一个对象的初始化完成(构造函数执行完成)一定先于finalize方法(对象被回收时会调用,即垃圾回收时)之前执行,也就是现有对象才能进行对象回收操作。

As-if-Serial 和 Happens-Before原则

\1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变;

2.as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的;

3.as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

符合Happens-before规则的多线程程序才是正确的逻辑,符合As-if-serial语义的单线程程序才是正确的逻辑,这也是保证了程序员对代码编写的逻辑合理性。

JMM

java内存模型,不存在的东西,是一种概念!约定!

  1. 线程解锁前,必须把共享变量立即刷新主存。
  2. 线程加锁前,必须读取主存中的最新值到工作内存中!
  3. 加锁和解锁必须是同一把锁

这些都可以由volatile中的例子很好的说明了这一约定

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Cta9XVl-1647526854438)(C:/Users/wp990/AppData/Roaming/Typora/typora-user-images/image-20220317120524044.png)]

volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

CAS

背景

CAS是compareAndSet 的缩写,译为比较并交换

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁存在引发如下问题。

锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

什么是CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。

我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。

在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。

CAS 操作包含三个操作数

  • 内存位置(V)
  • 预期原值(A)
  • 新值(B),或者也叫交换值

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

​ 利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个JUC都是建立在CAS之上的,因此对于synchronized阻塞算法,JUC在性能上有了很大的提升。

java中代码实现CAS

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo01 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(121);
        //预期是121,则更新为122
        atomicInteger.compareAndSet(121,212);
        System.out.println(atomicInteger.get());  //212
    }
}

CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

代码:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class CAS_ABA {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(111);
        //ABA
        new Thread(()->{
            atomicInteger.compareAndSet(111,112);
            System.out.println("线程一:悄悄已改动了!");
            atomicInteger.compareAndSet(112,111);
        },"线程一").start();
        //线程二根本不知道线程一改动过
        new Thread(()->{
            atomicInteger.compareAndSet(111,114);
        },"线程二").start();
        TimeUnit.SECONDS.sleep(4);
        System.out.println("线程二:"+atomicInteger.get());  //212
    }
}

解决ABA问题(添加版本号)

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CAS_ABA2 {
    public static void main(String[] args) throws InterruptedException {
        AtomicStampedReference<Integer> cas = new AtomicStampedReference<>(111,1);  //第二个参数就是版本号

        new Thread(()->{
            int stamp = cas.getStamp(); //获取版本号
            System.out.println(stamp);
            cas.compareAndSet(111, 112,cas.getStamp(),cas.getStamp()+1);
            System.out.println("偷偷改动");
            cas.compareAndSet(112, 111,cas.getStamp(),cas.getStamp()+1);
        },"线程一").start();

        new Thread(()->{
            int stamp = cas.getStamp(); //获取版本号

            // if(stamp!= cas.getReference()){}
            System.out.println(stamp);
            cas.compareAndSet(111, 112,cas.getStamp(),cas.getStamp()+1);

        },"线程二").start();

        TimeUnit.SECONDS.sleep(2);
        System.out.println("现在版本:"+cas.getStamp());
        System.out.println("现在数值;"+cas.getReference());
    }
}

循环时间长开销大

​ 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作

​ 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

concurrent包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

深入JUC(高并发编程)_第16张图片

公平锁/非公平锁

公平锁:非常公平,不能插队,必须先来后到!

非公平锁:非常不公平,可以插队(默认都是非公平锁),因为存在作业时长问题,长时间作业应该让短作业

Lock lock = new ReentrantLock();   //非公平锁
Lock lock = new ReentrantLock(true);    //公平锁

底层代码

//非公平锁
public ReentrantLock(){
    sysnc = new NonfairSync();
}
//公平锁
public ReentrantLock(boolean fair){
    sysnc = fair? new FairSync():new NonfairSync();
}

可重入锁

什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。例如

package com.test.reen;
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (this) {
					System.out.println("第1次获取锁,这个锁是:" + this);
					int index = 1;
					while (true) {
						synchronized (this) {
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
						}
						if (index == 10) {
							break;
						}
					}
				}
			}
		}).start();
	}
}
package com.test.reen;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

// 演示可重入锁是什么意思
public class WhatReentrant2 {
	public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
         new Thread(new Runnable() {
		@Override
		public void run() {
			try {
				lock.lock();
				System.out.println("第1次获取锁,这个锁是:" + lock);

				int index = 1;
				while (true) {
					try {
						lock.lock();
						System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
						try {
							Thread.sleep(new Random().nextInt(200));
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						
						if (index == 10) {
							break;
						}
					} finally {
						lock.unlock();
					}

				}

			} finally {
				lock.unlock();
			}
		}
	}).start();
}
}

可以发现没发生死锁,可以多次获取相同的锁

可重入锁有

  • synchronized
  • ReentrantLock

使用ReentrantLock的注意点
ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样

以下代码演示,加锁和释放次数不一样导致的死锁

package com.test.reen;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class WhatReentrant3 {
	public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					lock.lock();
					System.out.println("第1次获取锁,这个锁是:" + lock);

					int index = 1;
					while (true) {
						try {
							lock.lock();
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
							
							try {
								Thread.sleep(new Random().nextInt(200));
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							
							if (index == 10) {
								break;
							}
						} finally {
//							lock.unlock();// 这里故意注释,实现加锁次数和释放次数不一样
						}

					}

				} finally {
					lock.unlock();
				}
			}
		}).start();
		
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				try {
					lock.lock();
					
					for (int i = 0; i < 20; i++) {
						System.out.println("threadName:" + Thread.currentThread().getName());
						try {
							Thread.sleep(new Random().nextInt(200));
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				} finally {
					lock.unlock();
				}
			}
		}).start();		
	}
}

由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。

稍微改一下,在外层的finally里头释放9次,让加锁和释放次数一样,就没问题了

try {
	lock.lock();
	System.out.println("第1次获取锁,这个锁是:" + lock);

	int index = 1;
	while (true) {
		try {
			lock.lock();
			System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
			
			... 代码省略节省篇幅...
		} finally {
//							lock.unlock();// 这里故意注释,实现加锁次数和释放次数不一样
		}

	}

} finally {
	lock.unlock();
	
	// 在外层的finally里头释放9次,让加锁和释放次数一样,就没问题了
	for (int i = 0; i < 9; i++) {
		lock.unlock();
		
	}
}

自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

Java如何实现自旋锁?

下面是个简单的例子:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockTest {
    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();
        new Thread(()->{
            spinLock.lock();   //加锁
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                spinLock.unlock();  //4秒后解锁
            }
        },"线程A").start();
        //
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            spinLock.lock();   //加锁
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                spinLock.unlock();  //4秒后解锁
            }
        },"线程B").start();
    }
}
class SpinLock{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    //Thread 默认值 null
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"加锁");
        while(!atomicReference.compareAndSet(null,thread)){
        }
    }
    public void unlock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"解锁");
        atomicReference.compareAndSet(thread,null);
    }
}
//运行结果
/*

线程A加锁
线程B加锁
线程A解锁
线程B解锁

*/

lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

自旋锁的缺点

  • 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

  • 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

  • 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的
  • 不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

可重入的自旋锁和不可重入的自旋锁

开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。

而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

public class ReentrantSpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    private int count;
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
            count++;
            return;
        }
        // 如果没获取到锁,则通过CAS自旋
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
                count--;
            } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
                cas.compareAndSet(cur, null);
            }
        }
    }
}

自旋锁的其他变种

1. TicketLock

TicketLock主要解决的是公平性的问题。

思路:每当有线程获取锁的时候,就给该线程分配一个递增的id,我们称之为排队号,同时,锁对应一个服务号,每当有线程释放锁,服务号就会递增,此时如果服务号与某个线程排队号一致,那么该线程就获得锁,由于排队号是递增的,所以就保证了最先请求获取锁的线程可以最先获取到锁,就实现了公平性。

可以想象成银行办理业务排队,排队的每一个顾客都代表一个需要请求锁的线程,而银行服务窗口表示锁,每当有窗口服务完成就把自己的服务号加一,此时在排队的所有顾客中,只有自己的排队号与服务号一致的才可以得到服务。

实现代码:

public class TicketLock {
    /**
     * 服务号
     */
    private AtomicInteger serviceNum = new AtomicInteger();
    /**
     * 排队号
     */
    private AtomicInteger ticketNum = new AtomicInteger();
    /**
     * lock:获取锁,如果获取成功,返回当前线程的排队号,获取排队号用于释放锁. 
* * @return */
public int lock() { int currentTicketNum = ticketNum.incrementAndGet(); while (currentTicketNum != serviceNum.get()) { // Do nothing } return currentTicketNum; } /** * unlock:释放锁,传入当前持有锁的线程的排队号
* * @param ticketnum */
public void unlock(int ticketnum) { serviceNum.compareAndSet(ticketnum, ticketnum + 1); } }

上面的实现方式是,线程获取锁之后,将它的排队号返回,等该线程释放锁的时候,需要将该排队号传入。但这样是有风险的,因为这个排队号是可以被修改的,一旦排队号被不小心修改了,那么锁将不能被正确释放。一种更好的实现方式如下:

public class TicketLockV2 {
    /**
     * 服务号
     */
    private AtomicInteger serviceNum = new AtomicInteger();
    /**
     * 排队号
     */
    private AtomicInteger ticketNum = new AtomicInteger();
    /**
     * 新增一个ThreadLocal,用于存储每个线程的排队号
     */
    private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>();
    public void lock() {
        int currentTicketNum = ticketNum.incrementAndGet();
        // 获取锁的时候,将当前线程的排队号保存起来
        ticketNumHolder.set(currentTicketNum);
        while (currentTicketNum != serviceNum.get()) {
            // Do nothing
        }
    }
    public void unlock() {
        // 释放锁,从ThreadLocal中获取当前线程的排队号
        Integer currentTickNum = ticketNumHolder.get();
        serviceNum.compareAndSet(currentTickNum, currentTickNum + 1);
    }
}

TicketLock存在的问题

多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

2. CLHLock

CLH锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋,获得锁。

实现代码如下:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
 * CLH的发明人是:Craig,Landin and Hagersten。
 * 代码来源:http://ifeve.com/java_lock_see2/
 */
public class CLHLock {
    /**
     * 定义一个节点,默认的lock状态为true
     */
    public static class CLHNode {
        private volatile boolean isLocked = true;
    }
    /**
     * 尾部节点,只用一个节点即可
     */
    private volatile CLHNode tail;
    private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>();
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class,
            "tail");
    public void lock() {
        // 新建节点并将节点与当前线程保存起来
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        // 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点
        CLHNode preNode = UPDATER.getAndSet(this, node);
        if (preNode != null) {
            // 前驱节点不为null表示当锁被其他线程占用,通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁
            while (preNode.isLocked) {
            }
            preNode = null;
            LOCAL.set(node);
        }
        // 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁
    }
    public void unlock() {
        // 获取当前线程对应的节点
        CLHNode node = LOCAL.get();
        // 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁
        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false;
        }
        node = null;
    }
}

3. MCSLock

MCSLock则是对本地变量的节点进行循环。

/**
 * MCS:发明人名字John Mellor-Crummey和Michael Scott
 * 代码来源:http://ifeve.com/java_lock_see2/
 */
public class MCSLock {
    /**
     * 节点,记录当前节点的锁状态以及后驱节点
  */
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }
    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();
    // 队列
    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    // queue更新器
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class,  "queue");
    public void lock() {
        // 创建节点并保存到ThreadLocal中
        MCSNode currentNode = new MCSNode();
        NODE.set(currentNode);
        // 将queue设置为当前节点,并且返回之前的节点
        MCSNode preNode = UPDATER.getAndSet(this, currentNode);
        if (preNode != null) {
            // 如果之前节点不为null,表示锁已经被其他线程持有
            preNode.next = currentNode;
            // 循环判断,直到当前节点的锁标志位为false
            while (currentNode.isLocked) {
            }
        }
    }
    public void unlock() {
        MCSNode currentNode = NODE.get();
        // next为null表示没有正在等待获取锁的线程
        if (currentNode.next == null) {
            // 更新状态并设置queue为null
            if (UPDATER.compareAndSet(this, currentNode, null)) {
                // 如果成功了,表示queue==currentNode,即当前节点后面没有节点了
                return;
            } else {
                // 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待
                // 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)
                while (currentNode.next == null) {
                }
            }
        } else {
            // 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null
            currentNode.next.isLocked = false;
            currentNode.next = null;

        }
    }
}

4. CLHLock 和 MCSLock

都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。

将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了TicketLock多处理器缓存同步的问题。

自旋锁与互斥锁

自旋锁与互斥锁都是为了实现保护资源共享的机制。

无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。

获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

总结

  • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
  • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
  • 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
  • 自旋锁本身无法保证公平性,同时也无法保证可重入性。
  • 基于自旋锁,可以实现具备公平性和可重入性质的锁。

TicketLock,采用类似银行排号叫好的方式实现自旋锁的公平性,但是由于不停的读取serviceNum,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

CLHLock和MCSLock通过链表的方式避免了减少了处理器缓存同步,极大的提高了性能,区别在于CLHLock是通过轮询其前驱节点的状态,而MCS则是查看当前节点的锁状态。

CLHLock在NUMA架构下使用会存在问题。在没有cache的NUMA系统架构中,由于CLHLock是在当前节点的前一个节点上自旋,NUMA架构中处理器访问本地内存的速度高于通过网络访问其他节点的内存,所以CLHLock在NUMA架构上不是最优的自旋锁。

死锁

模拟死锁

import java.util.concurrent.TimeUnit;

public class DeadLock {
    /*
    死锁条件
    1.两条以上线程
    2.线程各自拥有对方资源(锁)
    3.都不释放自己手中的资源(锁)
    4.都在等待对方释放资源(锁)
     */
    public static void main(String[] args) {
        String lockA="lockA";
        String lockB="lockB";

        new Thread(new myThread(lockA,lockB),"线程A").start();
        new Thread(new myThread(lockB,lockA),"线程B").start();
    }
}
class myThread implements Runnable{
    private String lockA;
    private String lockB;

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

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+": lock"+lockA+"=>get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+": lock"+lockB+"=>get"+lockA);
            }
        }

    }
}

排查死锁

步骤:

1.使用jps定位线程号:jps -l

​ 使用idea的Terminal窗口输入命令 jps -l 定位到当前运行类的线程号 17432

深入JUC(高并发编程)_第17张图片

2.利用线程号,查询死锁信息 :jstack 进程号

深入JUC(高并发编程)_第18张图片

Found 1 deadlock. !!!

你可能感兴趣的:(java,开发语言)