JUC相关知识点

生产者和消费者问题(线程之间的通信问题)

线程交替执行 操作同一个变量

判断等待,业务,通知

synchronized版

​ 存在问题:如果有四个线程,两个加两个减,就会出现问题,怎么办?

​ 出现了虚假唤醒:线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒。

​ 当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其 它的唤醒都是无用功 1.比如说买货,如果商品本来没有货物,突然进了一件商 品,这是所有的线程都被唤醒了 ,但是只能一个人买,所以其他人都是假唤醒, 获取不到对象的锁

​ 怎么解决虚假唤醒问题:把if改成while,因为if是一次性判断。为了避免这种情况我们应 该让wait()在while()循环中多次判断。

大神说:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

JUC版

lock替换synchronized方法和语句的使用,condition取代了对象监视器方法的使用

具体语句:

  • lock.newCondition();
  • .await();
  • .signal();
public class B{
  public static void main(String[] args){
    Data2 data = new Data2();
    
    new Thread(()->{
      for(int i=0; i<10; i++){
        data.increment();
      }
    },"A").start();
    new Thread(()->{
      for(int i=0; i<10; i++){
        data.decrement();
      }
    },"B").start();
    new Thread(()->{
      for(int i=0; i<10; i++){
        data.increment();
      }
    },"C").start();
    new Thread(()->{
      for(int i=0; i<10; i++){
        data.decrement();
      }
    },"D").start();
  }
}

class Data2{
  private int number = 0;
  
  Lock lock = new ReentrantLock();
  Condition condition = lock.newCondition();
  
  //+1
  public void increment(){
    lock.lock();
    try{
      //业务代码
      while(number != 0){
      //等待
        condition.await();
      }
      number++;
      System.out.println(Thread.currentThread().getName()+"-->"+number);
      //通知其他线程
      condition.signalAll();
    }catch(Exception e){
      e.printStackTrace();
    }finally{
      lock.unlock();
    }
  }
  
  public void decrement(){
    lock.lock();
    try{
       while(number == 0){
        //等待
         condition.await();
      }
      number--;
      System.out.println(Thread.currentThread().getName()+"-->"+number);
      //通知其他线程
      condition.signalAll();
    }catch(Exception e){
      e.printStackTrace();
    }finally{
      lock.unlock();
    }  
  }
}
//现在的结果是个随机的状态
//不是按照ABCD的顺序

Condition的优势:

精准的通知和唤醒线程

//顺序执行 A->B->C->A
public class C{
  public static void main(String[] args){
    Data3 data = new Data3();
    new Thread(()->{
      for(int i=0; i<10; i++){
        data.printA();
      }
    }, "A").start();
    new Thread(()->{}, "B").start();
    new Thread(()->{}, "C").start();
  }
}

class Data3{//资源类lock
  private Lock lock = new ReentrantLock();
  //三个监视器
  private Condition condition1 = new Condition();
  private Condition condition2 = new Condition();
  private Condition condition3 = new Condition();
  
  private int number =1;//1A 2B 3C
  
  public void printA(){
    lock.lock();
    //try catch finally省略了
    while(number != 1){
      //wait
      condition1.await();
    }
    System.out.println("AAAA");
    //唤醒B,唤醒指定的监视器
    number = 2;
    condition2.signal();
    lock.unlock();
  }
  public void printB(){
    lock.lock();
    //try catch finally省略了
    
    lock.unlock();
  }
  public void printC(){
    lock.lock();
    //try catch finally省略了
    
    lock.unlock();
  }
}

8锁现象

关于锁的八个问题:

  1. 标准情况下,两个线程先打印哪一个?

    SEND MESSAGE

  2. 标准情况下,先打印的那一个线程在方法中延迟了几秒,谁先打印?

    SEND MESSAGE

    • 锁的对象是调用方法的phone,只有一个锁,SEND方法先拿到,先执行
public class Test1{
  public static void main(String[] args){
    Phone phone = new Phone();
    new Thread(()->{
      phone.sendSms();
    },"A").start();
    
    TimeUnit.SECONDS.sleep(1);
    
    new Thread(()->{
      phone.call();
    },"B").start();
  }
}

class Phone{
  //synchronized 锁的对象是方法的调用者
  //两个方法用的是一个锁,谁先拿到谁执行
  public synchronized void sendSms(){
    //第一个问题里面,没有sleep
    TimeUnit.SECONDS.sleep(4);
    System.out.println("SEND MESSAGE");
  }
  public synchronized void call(){
    System.out.println("CALL");
  }
}
  1. 增加了一个普通方法,先执行哪个? 先执行普通方法

    一秒后执行HELLO,四秒后执行SEND

  2. 两个对象,两个同步方法,先执行哪个?

    先CALL,后SEND

public class Test2{
  public static void main(String[] args){
    //两个调用者,两把锁
    //所以是按时间来的,SEND有4秒的延迟,所以是CALL先执行
    Phone2 phone = new Phone2();
    Phone2 phone2 = new Phone2();
    
    new Thread(()->{
      phone.sendSms();
    },"A").start();
    
    TimeUnit.SECONDS.sleep(1);
    
    //new Thread(()->{
    //  phone.hello();
    //},"B").start();
    
    new Thread(()->{
      phone2.call();
    },"B").start();
  }
}

class Phone2{
  //synchronized 锁的对象是方法的调用者
  public synchronized void sendSms(){
    TimeUnit.SECONDS.sleep(4);
    System.out.println("SEND MESSAGE");
  }
  public synchronized void call(){
    System.out.println("CALL");
  }
  //这里没有锁,不存在抢的问题,不是同步方法,不受锁的影响
  public void hello(){
    System.out.println("HELLO");
  }
}
  1. 增加两个静态的同步方法,只有一个对象,先打印谁?

    先SEND,后CALL

  2. 两个静态方法,两个对象,先执行谁?

    先SEND,后CALL

    • 因为是static,锁到了class上,所以两个对象的class只有一个Phone3,锁还是只有一把
public class Test3{
  public static void main(String[] args){
    Phone3 phone = new Phone3();
    Phone3 phone3 = new. phone3();
    new Thread(()->{
      phone.sendSms();
    },"A").start();
    
    TimeUnit.SECONDS.sleep(1);
    
    //new Thread(()->{
    //  phone.call();
    //},"B").start();
    
    new Thread(()->{
      phone3.call();
    },"B").start();
  }
}

//Phone3是唯一的一个class对象
class Phone3{
  //synchronized 锁的对象是方法的调用者
  //static 静态方法,类一加载就有了
  //锁的是class,所以也只有一把锁
  public static synchronized void sendSms(){
    TimeUnit.SECONDS.sleep(4);
    System.out.println("SEND MESSAGE");
  }
  public static synchronized void call(){
    System.out.println("CALL");
  }
}
  1. 一个普通同步方法,一个静态同步方法,一个对象,先执行谁?

    先CALL,后SEND

    • 两把锁,先执行没有等待的那一个
  2. 一个普通同步方法,一个静态同步方法,两个对象,先执行谁?

    先CALL,后SEND

    • 两把锁
public class Test4{
  public static void main(String[] args){
    Phone4 phone = new Phone4();
    Phone4 phone4 = new Phone4();
    new Thread(()->{
      phone.sendSms();
    },"A").start();
    
    TimeUnit.SECONDS.sleep(1);
    
    //new Thread(()->{
    //  phone.call();
    //},"B").start();
    
    new Thread(()->{
      phone4.call();
    },"B").start();
  }
}

class Phone4{

  //锁的是class
  public static synchronized void sendSms(){
    TimeUnit.SECONDS.sleep(4);
    System.out.println("SEND MESSAGE");
  }
  //锁的是调用者
  public synchronized void call(){
    System.out.println("CALL");
  }
}

总结

锁只有两种,一个锁对象(方法的调用者),一个通过static锁class

集合类不安全

单线程的代码所有都安全

多线程:

List不安全

public class ListTest{
  public static void main(String[] args){
    //并发下ArrayList不安全
    
    List<String> list = 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();
    }
  }
}

//运行后会报错
//java.util.ConcurrentModificationException 并发修改异常

/*
解决方案:
1. 换成安全的Vector
List<String> list = new Vector<>();
2. 把ArrayList变得安全
List<String> list = Collections.synchronizedList(new ArrayList<>());
3. JUC的解决方案
List<String> list = new CopyOnWriteArrayList<>();
*/
CopyOnWrite 写入时复制 COW 读写分离

计算机程序设计领域的一种优化策略

多个线程调用的时候,list,读取的时候固定,写入的时候覆盖

写入的时候复制一个数组出来,写完再插入进去,避免覆盖造成的数据问题

  • CopyOnWriteArrayList 比 Vector好在哪里?

    vector是synchronized方法,效率相对较低

    CopyOnWriteArrayList是lock锁

Set不安全

Set、list、blockingQueue是同级的

public class SetTest{
  public static void main(String[] args){
    
    //java.util.ConcurrentModificationException
    Set<String> set = new HashSet<>();
    for(int i=1; i<=30; i++){
      new Thread(()->{
        set.add(UUID.randomUUID().toString().substring(0,5));
        System.out.println(set);
      },String.valueOf(i)).start();
    }
  }
}

/*
解决方案:
1. Set<String> set = Collections.synchronizedSet(new HashSet<>());
2. Set<String> set = new CopyOnWriteArraySet<>();
*/
HashSet的底层是什么?

HashMap

set中key是无法重复的

HashMap不安全

public class MapTest{
  public static void main(String[] args){
    Map<String, String> map = new HashMap<>();
    //工作中不这样使用map
    //HashMap有两个参数,加载因子和初始化容量
   	//默认为new HashMap<>(16, 0.75);
    for(int i=1; i<=30; i++){
      new Thread(()->{
        map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
        System.out.println();
      },String.valueOf(i)).start();
  }
}
  //java.util.ConcurrentModificationException
  /*解决方案:
  1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
  2. Map<String, String> map = new ConcurrentHashMap<>();
  */
HashMap加载因子和初始化容量

容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。

加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。

当哈希表中条目的数目超过 容量乘加载因子 的时候,则要对该哈希表进行rehash操作,从而哈希表将具有大约两倍的桶数。(以上摘自JDK6)

为什么选择0.75做加载因子?

提高空间利用率和减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,

加载因子是表示Hash表中元素的填满的程度。
加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。

Q:java hashmap,如果确定只装载100个元素,new HashMap(?)多少是最佳的,why?

A:100/0.75 = 133.33。为了防止rehash,向上取整,为134。

Q:但是还有另外一个问题,就是hash碰撞的问题。如果我们将HashMap的容量设置为134,那么如何保证其中的哈希碰撞会比较少呢?

A:除非重写hashcode()方法,否则,似乎没有办法保证。但只要将hash表的长度设为2的N次方,那么,所有的哈希桶均有被使用的可能。与134最靠近的2^n无疑是128。如果只修改HashMap的长度而不修改HashMap的加载因子的话,HashMap会进行rehash操作,这是一个代价很大的操作,所以不可取。那么应该选择的就应该是256。

还有ConcurrentLinkedDeque和ConcurrentLinkedQueue

ConcurrentHashMap原理

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

Callable(简单)

和Runnable的不同点:

  1. 可以有返回值
  2. 可以抛出异常
  3. 方法不同,run()/call()
public class CallableTest{
  public static void main(String[] args){
    MyThread thread = new MyThread();
    //适配类 FutureTask
    FutureTask futureTask = new FutureTask(thread);
    new Thread(futureTask, "A").start();
    new Thread(futureTask, "B").start();//结果会被缓存,效率高
    String s = (String) futureTask.get();//获取Callable的返回结果
    //get方法可能会产生阻塞,把它放到最后,或者使用异步通信来处理
    System.out.println(s);
  }
  //Callable不能直接调用Thread,要通过Runnable的FutureTask
  /*
  new Thread(new Runnable()).start();
  new Thread(new FutureTask<V>()).start();
  new Thread(new FutureTask<V>(Callable)).start();
  */
}

class MyThread implements Callable<String>{
  //Callable范型的参数就是call方法的返回值类型
  public String call(){
    return "12345";
  }
}

常用的辅助类三个(重要!!

CountDownLatch

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助

用来计数的

//减法计数器
public class CountDownLatchDemo{
  public static void main(String[] args){
    //总数是6
    COuntDownLatch cdl = new CountDownLatch(6);
    for(int i=1; i<=6; i++){
      new Thread(()-<{
        System.out.println(Thread.currentThread().getName()+"Go out!");
        cdl.countDown();//-1
      },String.valueOf(i)).start();
    }
    cdl.await();//等待计数器归零,再向下执行
    //如果不使用await()方法,可能上面线程还没执行完,就先关门了
    System.out.println("close door");
    
  }
}

原理:

countDown(); 每次有线程调用countDown()就会数量减1,不会阻塞

await(); 计数器变为0,await就会被唤醒,执行后面的操作

CyclicBarner

允许一组线程全部等待彼此达到共同屏障点的同步辅助

//加法计数器
public class CyclicBarrierDemo{
  public static void main(String[] args){
    CyclicBarrier cb = new CyclicBarrier(7,()-<{
      System.out,println("召唤神龙!")
    });
    //两个参数,一个是达到的数量,一个是执行的Runnable
    
    for(int i=1; i<=7; i++){
      //Lambda能操作到i吗? 不能 因为lambda里面是一个类,除非借助final变量
      final int temp = i;
      new Thread(()->{
        System.out.println(Thread.currentThread().getName()+"收集"+temp+"龙珠");
          
          //try-catch省略了
          cb.await();//等待7个线程,计数器变成7
      }).start();
    }
  }
}
Semaphore信号量

一个计数信号量,在概念上,信号量维持一组许可证

//demo:停车位
public class SemaphoreDemo{
  public static void main(String[] args){
    //线程数量-停车位数量,限流的时候用
    Semaphore semaphore = new Semaphore(3);
    
    for(int i=1; i<=6; i++){
      new Thread(()->{
        
        //try
        semaphore.acquire();//得到
        System.out.println(Thread.currentThread().getName()+"抢到停车位");
        TimeUnit.SECONDS.sleep(2);
        System.out.println(Thread.currentThread().getName()+"离开车位");
        
        //finally
        semaphore.release();//释放
      },String.valueOf(i)).start();
    }
  }
}

原理:

acquire(); 获得,如果已经满了,等待,等待被释放

release(); 释放,会将当前的信号量释放+1,然后唤醒等待的线程

作用:

多个共享资源互斥的使用

开发限流,控制最大的线程数

posted @ 2021-02-23 10:41  GladysChloe  阅读(44)  评论(0)    收藏  举报