当前位置: 首页>后端>正文

9-1、线程 并发(重点关注JUC:java.util.concurrent,并发包)

一、防止任务在共享资源上产生冲突

1、利用以下方式同步代码。

(1) 同步代码块或者同步方法sychronized关键字加锁。
(2) 使用ReentrantLock显示的加锁和释放锁。(JUC下的类之一)

在【JDK5】之后,提供了一个【java.util.concurrent.locks.Lock接口】,这是一个来控制多个线程对共享资源进行访问的工具。常用的实现类有:ReentrantLock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReentrantLock,可以显式的加锁、释放锁

构造器
ReetrantLock rtLock = new ReetrantLock(); 
ReetrantLock rtLock = new ReetrantLock(boolean fair = false); // 默认是非公平锁,公平的含义在于先到来的线程先拿到锁。

1、lock()
该方法上锁。
2、unlock()
该方法解锁。

常常和try..finally搭配使用,显示的上锁和释放锁。
try{
  rtLock.lock();
  ....
}catch(..){
  ...
}finally{
  rtLock.unlock();
  ...
}

2、根除对变量的共享。可以使用【java.lang.ThreadLocal】类来实现。

线程本地存储的创建和管理可以由【java.lang.ThreadLocal】类来实现,JDK 1.2引入。
线程本地存储是一种自动化机制,可以为使用同一个对象的每个不同线程都创建不同的存储

? 注意:
(1) ThreadLocal对象可以为当前线程关联一个数据,通常当作静态域存储
(2) 只能通过get()和set()方法来访问ThreadLocal的内容。
get方法返回与其线程相关联的对象的副本;set方法会将参数插入到为其线程存储的对象中。

ThreadLocal关联Object类型数据:
class myClass{
  public static ThreadLocal<Object> x = new ThreadLocal<>();
  public static void test(){
    x.set(Object obj);
    Object obj = x.get();
  }
}

二、线程的通信

1、常用的方法

1、wait()
调用该方法的线程进入阻塞状态,并释放obj锁(谁调用的就释放哪个锁,如果是线程本身,那么锁就是线程this)。
2、notify()
唤醒一个需要该锁(调用者就是锁,如果是线程本身,那么锁就是线程this)且在锁池的线程,不能指定,优先唤醒优先级高的那个。
3、notifyAll()
唤醒所有需要该锁(调用者就是锁,如果是线程本身,那么锁就是线程this)且在锁池的线程。

? 注意:(9-1、线程 并发(重点关注JUC:java.util.concurrent,并发包),\color{red}{重要},第1张)
(1) 以上三个方法必须使用在同步代码块,或者同步方法中!
(2) 以上三个方法的调用者必须为同步代码块,或者同步方法中的同步监视器,即锁本身!

(3) 以上三个方法是定义在java.lang.Object类中的,即锁可以是任何一个对象来充当。

9-1、线程 并发(重点关注JUC:java.util.concurrent,并发包),第2张
经典面试题

2、应用-生产者/消费者问题

public class Clerk{
  public int productNum = 0;
  public sychronized void produce(){
    if(productNum <= 0){
      productNum++;
      System.out.println("当前生产者正在生产第" + productNum + "个产品。");
      // 唤醒消费者
      notify();
    }else{
      wait();
    }
  }
  public sychronized void consume(){
    if(productNum >= 20){
      System.out.println("当前消费者正在消费第" + productNum + "个产品。");
      productNum--;
      // 唤醒生产者
      notify();
    }else{
      wait();
    }
  }
}
class Consumer implements Runnable{
  private Clerk clerk;
  @Override
  public void run(){
    System.out.println("消费者" + this.getName() + "正在消费");
    while(true){
      try{
        Thread.sleep(100);
      }catch(Exception e){
        e.printStackTrace();
      }
      clerk.consume();
    }
  }
}
class Producer implements Runnable{
  private Clerk clerk;
  @Override
  public void run(){
    System.out.println("生产者" + this.getName() + "正在生产");
    while(true){
      try{
        Thread.sleep(100);
      }catch(Exception e){
        e.printStackTrace();
      }
      cleark.produce();
    }
  }
}

三、JDK5.0新增线程创建方式(JUC下的类之一)

3.1 实现Callable接口

类似于Runnable接口,但是提供比它更为强大的功能。
实现该接口需要实现它的【call方法】

与Runnable相比,Callable的不同:
(1) call可以有返回值,而run没有。
(2) call可以抛出异常,而run只能在内部捕获处理。
(3) call支持泛型的返回值
(4) 需要借助FutureTask类,比如启动线程、获取返回结果等

? FutureTask类

开启实现了Callable接口的线程,可以用该类,启动线程并获得call返回结果。
FutureTask实现了Future接口和Runnable接口,是Future接口下的唯一实现类。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

9-1、线程 并发(重点关注JUC:java.util.concurrent,并发包),第3张
FutureTask构造器
class NumThread implements Callable<Integer>{
 public Integer call throws Exception(){
   int sum = 1;
   return sum;
 }
}

try{
  1、通过传入Callable对象来构造FutureTask对象
  FutureTask<Integer> futureTask = new FutureTask<>(new NumThread());
  2、由于实现了Runnable接口,所以可以通过new Thread().start();开启
  new Thread(futureTask).start();
  3、通过get方法获得结果
  Integer sum = futureTask.get();
}catch(Exception e){
  e.rpintStackTrace();
}
3.2 JDK5.0使用线程池【实际开发常用】(JUC下的类之一)

【JDK5.0】起提供了线程池相关的API:ExecutorService和Executors
主要思想:提前创建好多个线程,放入线程池中。使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

(1) ExecutorService接口

常用子类ThreadPoolExecutor

● 常用方法

1、void execute(Runnable); 启动线程并执行run方法
2、Future<T> submit(Callable<T>);
   Future<?> submit(Runnable);
   Future对象可以用于检查任务是否执行完成、获取任务执行结果或取消任务。该方法返回的Future对象的get()方法将会返回null
   Future<T> submit(Runnable, T result);
   Future对象可以用于检查任务是否执行完成、获取任务执行结果或取消任务。该方法返回的Future对象的get()方法将会返回传递的result参数
3、void shutdown(); 关闭【线程池】

(2) Executors工具类

是用于创建并返回不同类型线程池的一个工厂类

Executors.createCachedThreadPool:创建一个可根据需要创建新线程的线程池
Executors.createFixedThreadPool:创建一个可重用固定线程数的线程池
Executors.createSingleThreadExecutor:创建一个只有一个线程的线程池
Executors.createScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行或定期执行

注意:上面的线程池运行类型都是ThreadPoolExecutor.

四、ConcurrentHashMap(JUC下的类之一)

是线程安全版本的HashMap基本思想:同一时间只有一个线程可以访问加锁的资源。

● 在JDK1.7中,主要是通过ReentrantLock + 分段锁实现的。
基于HashMap1.7的数组+链表的结构下,它将数组分成若干个段,每一段形成一个Segment,若干个Segment形成大数组;而每一个Segment中又有若干的小数组,结构式HashEntry。加锁的时候在Segment大数组中加,锁的粒度很大。

● 在JDK1.8中,主要是通过 volatile + CAS + Synchronized 来保证线程安全。
基于HashMap1.8的数组+链表+红黑树的结构下,对table数组用volatile关键字修饰,并且只在数组的头节点加锁(沿用了大数组小数组模式,但这只是为了保证兼容性,实际上对操作没有任何优化功能),添加元素时,当容器为空,会使用volatile + CAS来初始化容器;当容器不为空,并且当前位置没有任何元素时,使用CAS来添加元素;当容器不为空,但当前位置有元素时,使用Synchronied来添加元素。锁的粒度更小。

4.1 CAS

Compare And Swap(比较与交换)。它用来实现多线程同步的原子指令,允许线程执行读-修改-写操作,而无需担心其他线程同时修改变量。
● 原子操作:不会被线程调度机制打断的操作指令,这种操作要么全部完成,要么全部中断。
它的基本思想:通过比较内存中的值是否与预期的相等,如果相等则更新,否则不进行更新。可以看作是一种乐观锁的实现方式,有时候,它可以用来取代Synchronized的强制同步,用来提升性能。另外,CAS需要底层硬件的支持。(在应用层面上,CAS是无锁的,但是在底层硬件上往往用到了锁的思想)

? CAC三大问题
(1) ABA问题
并发环境下,假设初始值为A,线程1准备修改数据时,此时线程2将数据改成了B,随后又改为A,那么线程1回来查看发现还是A,此时修改成功。
但是此"A"已经非彼"A"了,会导致线程安全问题。这种A→B→A的问题被称作ABA。
(特别是当操作引用对象时,线程2修改了对象的其中一个属性,但是线程1回来查看发现对象本身没变,因为地址没变,所以又做了修改)

● 解决:加上版本号。既判断预期值是否相等,也要判断版本号是否相等。(JUC中的AtomicStampReference就是参考了这种思想)

(2) 循环性能开销
CAS一般是乐观自旋锁,即它会一直循环执行,如果一直不成功,就会白白浪费大量的CPU资源
● 如果是短时间内就可以成功的,那么这种自旋的方式,相比来说还是有很高的性能的,因为它避免了切换的开销
● 但如果是长时间的话,不能让CPU持续处于空转状态,一般会限制一个自旋的次数,如果超过这个次数,就会停止自旋。

(3) 只能保证一个变量的原子操作
CAS只能保证一个变量的原子操作,无法保证多个变量的原子操作。

● 解决:① 考虑用锁来保证操作的原子性;② 考虑将多个变量合并成一个变量,用AtomicReference来保证原子性。


https://www.xamrdz.com/backend/37s1922573.html

相关文章: