Java线程高级篇JUC

JUC(Java.util.concurrent)


volatile

当不同线程操作相同共享数据时,这个共享数据是彼此不可见的,所以出现了内存可见性问题。同步锁(synchronized)可以解决该问题,但是效率较差。因此,Java 提供了一种稍弱的同步机制:volatile。

volatile 关键字修饰变量,用来确保将变量的更新操作通知到其他线程,保证内存可见性。(注意,volatile修饰的变量无法进行重排序)

可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

  • 对于多线程,不是一种互斥关系
  • 不能保证变量状态的原子性操作

写:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存

读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量


原子变量

举例:i++的原子性问题,i++的操作实际上分为三步:“读-改-写”:

int i = 10;
i = i++;//此时的i = 10

//上述两行代码在JMM中执行分以下三步:
int temp = i;
i = i + 1;
i = temp;
//如果某一线程执行了读-改操作,在执行写操作之前,有一个其他线程也执行力读-改-写操作,那么就会出现三个语句没有全部执行的原子性问题。

java.util.concurrent.atomic 包下提供了一些原子操作的常用类:

  • AtomicBoolean、AtomicInteger 、AtomicLong 、 AtomicReference
  • AtomicIntegerArray 、AtomicLongArray
  • AtomicMarkableReference
  • AtomicReferenceArray
  • AtomicStampedReference

这些类使用volatile关键字保证内存可见性,同时,使用CAS算法保证数据的原子性。


CAS算法

CAS算法是硬件对于并发操作共享数据的支持,是一种无锁的非阻塞算法的实现。

CAS 算法包含了三个操作数:内存值V、预估值A、更新值B,当且仅当 V = A 时,才会将B的值赋给A,否则什么操作都不做。


ConcurrentHashMap

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

其余用于多线程上下文中的 Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、 CopyOnWriteArrayList 和CopyOnWriteArraySet。

  • 当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的HashMap, ConcurrentSkipListMap 通常优于同步的 TreeMap
  • 当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

分段锁技术:ConcurrentHashMap使用Segment(分段锁)技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。

在这里插入图片描述

JDK1.8之后,ConcurrentHashMap 采用 CAS 算法 + synchronized代替了分段锁机制。


CountDownLatch 闭锁

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

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

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

应用场景举例:计算所有并发线程执行完毕的时间花费。


实现线程的四种方式

之前的博文写过:Thread、Runnable、Callable、线程池

https://blog.csdn.net/qq_40585800/article/details/106449260


Lock 同步锁

在 Java 5.0 之前,协调共享对象的访问时可以使用的机制只有 synchronized 和 volatile 。Java 5.0 后增加了一些新的机制,但并不是一种替代内置锁的方法,而是当内置锁不适用时,作为一种可选择的高级功能。

ReentrantLock 实现了 Lock 接口,并提供了与 synchronized 相同的互斥性和内存可见性。但相较于 synchronized 提供了更高的处理锁的灵活性,手动加锁和释放锁。

class MyThread2 implements Runnable{
    
    
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
    
    
        try{
    
    
            //加锁
            lock.lock();
            //相关操作
			...   
        }finally {
    
    
            //释放锁
            lock.unlock();
        }
    }
}

Condition 控制线程通信

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

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

举例,在生产者消费者问题中,需要引入同步锁机制(Lock)来控制同步,此时就需要其对应的Condition对象来进行wait、notify操作。

ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
        
condition.await();
...
condition.signalAll();

线程按序交替

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

class AlternateDemo {
    
    

    private int number = 1; //当前正在执行线程的标记

    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    /**
     * @param totalLoop : 循环第几轮
     */
    public void loopA(int totalLoop) {
    
    
        lock.lock();

        try {
    
    
            //1. 判断
            if (number != 1) {
    
    
                condition1.await();
            }
            //2. 打印
            for (int i = 1; i <= 1; i++) {
    
    
                System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
            }
            //3. 唤醒
            number = 2;
            condition2.signal();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void loopB(int totalLoop) {
    
    
        lock.lock();
        try {
    
    
            //1. 判断
            if (number != 2) {
    
    
                condition2.await();
            }
            //2. 打印
            for (int i = 1; i <= 1; i++) {
    
    
                System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
            }
            //3. 唤醒
            number = 3;
            condition3.signal();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void loopC(int totalLoop) {
    
    
        lock.lock();
        try {
    
    
            //1. 判断
            if (number != 3) {
    
    
                condition3.await();
            }
            //2. 打印
            for (int i = 1; i <= 1; i++) {
    
    
                System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);
            }
            //3. 唤醒
            number = 1;
            condition1.signal();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }

}

ReadWriteLock 读写锁

写写/读写:需要互斥;读读:不需要互斥。

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

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

class ReadWriteLockDemo{
    
    
   
   private int number = 0;
   private ReadWriteLock lock = new ReentrantReadWriteLock();
   
   //读
   public void get(){
    
    
      lock.readLock().lock(); //上读锁,不互斥
      try{
    
    
         System.out.println(Thread.currentThread().getName() + " : " + number);
      }finally{
    
    
         lock.readLock().unlock(); //释放读锁
      }
   }
   
   //写
   public void set(int number){
    
    
      lock.writeLock().lock();//上写锁,互斥
      try{
    
    
         System.out.println(Thread.currentThread().getName());
         this.number = number;
      }finally{
    
    
         lock.writeLock().unlock();//释放写锁
      }
   }
}

线程八锁总结

  • 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待
  • 所有的非静态同步方法用的都是同一把锁——实例对象本身(但不同的对象之间不会有竞态条件)
  • 所有的静态同步方法用的也是同一把锁——类对象本身(所有静态方法存在竞态条件,静态方法和非静态方法不存在竞态条件)
  • 某一个时刻内,只能有一个线程持有锁,无论几个方法

线程池

提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。

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

工具类 : Executors :

  • ExecutorService newFixedThreadPool() : 创建固定大小的线程池
  • ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
  • ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程
  • ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
//创建线程
ExecutorService pool = Executors.newFixedThreadPool(5);

//ThreadPoolDemo是实现了Runnable接口的类
ThreadPoolDemo tpd = new ThreadPoolDemo();

//为线程池中的线程分配任务
//pool.execute(tpd);//适合Runnable
pool.submit(tpd);//适合Callable

//关闭线程池
pool.shutdown();

ScheduledExecutorService线程调度

一个 ExecutorService,可安排在给定的延迟后运行或定期执行的命令。

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;
      }
      
   }, 1, TimeUnit.SECONDS);//(线程,延迟时间,延迟时间的单位)
   
   System.out.println(result.get());
}

pool.shutdown();

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

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

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

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

public class TestForkJoinPool {
    
    
	public static void main(String[] args) {
    
    
		Instant start = Instant.now();
		ForkJoinPool pool = new ForkJoinPool();
		ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
		Long sum = pool.invoke(task);
		System.out.println(sum);
		Instant end = Instant.now();
		System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//166-1996-10590
	}
}

class ForkJoinSumCalculate extends RecursiveTask<Long>{
    
    
   private static final long serialVersionUID = -259195479995561737L;
   private long start;
   private long end;
   private static final long THURSHOLD = 10000L;  //临界值
   
   public ForkJoinSumCalculate(long start, long end) {
    
    
      this.start = start;
      this.end = end;
   }

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

猜你喜欢

转载自blog.csdn.net/qq_40585800/article/details/106772242