JUC回顾

时光怂恿深爱的人放手 提交于 2020-03-17 20:16:15

某厂面试归来,发现自己落伍了!>>>

一.JUC是什么?

java.util.concurrent在并发编程中使用的工具类。

前景回顾

1.进程\线程是什么?

进程\线程都是操作系统而不是JVM的。

进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。进程是一个独立运行的程序。
 
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

电脑上运行的QQ就是一个进程,而使用QQ能支持视频、聊天、发文件。每一个功能都是一个独立的线程。
 

2.线程的状态?

NEW(新建)

RUNNABLE(准备就绪)

BLOCKED(阻塞)

WAITING(不见不散)

TIMED_WAITING(过时不候)

TERMINATED(终结)

3.wait/sleep的区别?

功能都是暂停当前线程。区别是 wait是睡了放开手里的锁,sleep是带着锁睡。

4.什么是并发?什么是并行?

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
        例子:春运抢票、电商秒杀...
           
并行:多项工作一起执行,之后再汇总
       例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

二.LOCK

复习:synchronized

作用:给对象加锁

实现步骤:

1.创建资源类

2.资源类里创建同步方法、同步代码块

class Ticket {
    private int number = 30;
   

    public synchronized void sale(){

         if(number>0){
             System.out.println(Thread.currentThread().getName()
             +"\t 卖出"+number--+"号票\t还剩"+number
             );
         }
     }

}

复习:创建线程的方式

1.继承Thread

public class SaleTicket extends Thread

java是单继承,资源宝贵,要用接口方式

2.实现Runnable接口

//方式一:匿名内部类
new Thread(new Runnable() {
    @Override
    public void run() {
 
    }
   }, "your thread name").start();

//方式二:lombda表达式
new Thread(() -> {
 
 }, "your thread name").start();

LOCK(JDK 1.5)

LOCK是什么?

Lock implementations provide more extensive locking 
operations than can be obtained using synchronized
 methods and statements. They allow more flexible 
structuring, may have quite different properties, 
and may support multiple associated Condition objects. 
 
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。
它们允许更灵活的结构,可能具有非常不同的属性,
并且可能支持多个关联的条件对象。

2.Lock怎么用

实现ReentrantLock可重入锁

class Demo {
   private final ReentrantLock lock = new ReentrantLock();
  
 
   public void m() {
     lock.lock();  
     try {
       
     } finally {
       lock.unlock()
     }
   }
 }

LOCK与Synchronized的区别?

  1. Synchronized能实现的功能LOCK都能实现
  2. LOCK比Synchronized更灵活
  3. Synchronized是自动上锁解锁,LOCK可以手动上锁解锁

三.线程间通信

线程间通信:1.生产者+消费者 2、通知等待唤醒机制

模板:1.判断 2.干活 3.通知

(1)使用Synchronized实现

//资源类
class ShareDataOne
{
  private int number = 0;//初始值为零的一个变量
 
  public synchronized void increment() throws InterruptedException 
  {
     //1判断
     while(number !=0 ) {
       this.wait();
     }
     //2干活
     ++number;
     System.out.println(Thread.currentThread().getName()+"\t"+number);
     //3通知
     this.notifyAll();
  }
  
  public synchronized void decrement() throws InterruptedException 
  {
     // 1判断
     while(number == 0) {
       this.wait();
     }
     // 2干活
     --number;
     System.out.println(Thread.currentThread().getName() + "\t" + number);
     // 3通知
     this.notifyAll();
  }
}
 
/**
 * 
 * 
 *现在两个线程,
 * 可以操作初始值为零的一个变量,
 * 实现一个线程对该变量加1,一个线程对该变量减1,
 * 交替,来10轮。 
 */
public class NotifyWaitDemoOne
{
  public static void main(String[] args)
  {
     ShareDataOne sd = new ShareDataOne();
     new Thread(() -> {
       for (int i = 1; i < 10; i++) {
          try {
            sd.increment();
          } catch (InterruptedException e) {
           
            e.printStackTrace();
          }
       }
     }, "A").start();
     new Thread(() -> {
       for (int i = 1; i < 10; i++) {
          try {
            sd.decrement();
          } catch (InterruptedException e) {
            
            e.printStackTrace();
          }
       }
     }, "B").start();
  }
}

在“判断”这一步中,使用if会造成虚假唤醒,必须使用while

(2)使用LOCK实现

//资源类
class ShareData{
  //初始值为零的一个变量
  private int number = 0;
 
  private Lock lock = new ReentrantLock();//锁
  private Condition condition  = lock.newCondition(); //钥匙
   
  public  void increment() throws InterruptedException {
     
      lock.lock();
         try {

          //判断
          while(number!=0) {
            condition.await();
          }
          //干活
          ++number;
          System.out.println(Thread.currentThread().getName()+" \t "+number);

          //通知
          condition.signalAll();

     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       lock.unlock();
     }

面试题:两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信

package cn.hbuas.springcloud;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
 *@auther 阿伟正是在下
 *@create 2020/3/15 22:08
 */
class ShareData {
    private int tag = 1; //1:12  2:A
    private int num = 1;
    private char str = 'A';

    private Lock lock = new ReentrantLock();//锁
    Condition cd1 = lock.newCondition();//线程1的钥匙
    Condition cd2 = lock.newCondition();//线程2的钥匙

    public void incrNum() {
        lock.lock();
        try {
            //判断
            while (tag != 1) {
                cd1.await();
            }

            //干活
            System.out.print(num++);
            System.out.print(num++);
            //通知
            tag = 2;
            cd2.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void incrStr() {
        lock.lock();
        try {
            //判断
            while (tag != 2) {
                cd2.await();
            }

            //干活
            System.out.print(str++);

            //通知
            tag = 1;
            cd1.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }



}

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

        ShareData shareData = new ShareData();

        new Thread(() -> {
            for (int i = 0; i < 52 / 2; i++) {
                shareData.incrNum();
            }
        }, "111").start();
        new Thread(() -> {
            for (int i = 0; i < 52 / 2; i++) {
                shareData.incrStr();
            }
        }, "222").start();
    }

}

四.NotSafe

为什么集合类是不安全的

ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常,并发修改异常

List<String> list = new ArrayList<>();
for (int i = 0; i <30 ; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
 
//看ArrayList的源码
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
//没有synchronized线程不安全

解决方案:

1.Vector:vector是JDK 1.0产物,效率比较低,不建议使用

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
//有synchronized线程安全

2.Collections:Collections提供了方法synchronizedList保证list是同步线程安全的

如图:HashMap,HashSet也不是线程安全的

3.写时复制:CopyOnwritArrayList

不加锁性能提升出错误,加锁数据一致性能下降。

CopyOnWriteArrayList是arraylist的一种线程安全变体,
其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

举例:签到名单

扩展类比

HashSet

  • Set<String> set = new HashSet<>();//线程不安全
  • Set<String> set = new CopyOnWriteArraySet<>();//线程安全

HashSet底层数据结构是HashMap  ,HashSet的add是放一个值,放在<K,V>中的Key中,而V放的是一个Object常量。

HashMap

  • Map<String,String> map = new HashMap<>();//线程不安全
  • Map<String,String> map = new ConcurrentHashMap<>();//线程安全

五. 获得多线程的方式

面试题:获得多线程的方式哪几种?

传统的是继承thread类和实现runnable接口,
java5以后又有实现callable接口和java的线程池获得

1.Callable接口

是一个函数式接口,因此可以用作lambda表达式或方法引用的赋值对象。

与runnable对比

 //实现runnable接口
class MyThread implements Runnable{
 @Override
 public void run() {
 
 }


}
//实现callable接口
class MyThread2 implements Callable<Integer>{
 @Override
 public Integer call() throws Exception {
  return 200;
 } 
}

面试题:callable接口与runnable接口的区别?

答:(1)是否有返回值
       (2)是否抛异常
       (3)重写方法不一样,一个是run,一个是call

实现方法:FutureTask

FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread());
new Thread(ft, "AA").start();

//运行成功后获得返回值
ft.get();

在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
 
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
 
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,
然后会返回结果或者抛出异常。
只计算一次
get方法放到最后

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;


public class CallableDemo {


    public static void main(String[] args) throws Exception {
      
        FutureTask<Integer> futureTask = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+"  come in callable");
            TimeUnit.SECONDS.sleep(4);
            return 1024;
        });

        FutureTask<Integer> futureTask2 = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+"  come in callable");
            TimeUnit.SECONDS.sleep(4);
            return 2048;
        });

        new Thread(futureTask,"zhang3").start();
        new Thread(futureTask2,"li4").start();


        //1、一般放在程序后面,直接获取结果
        //2、只会计算结果一次

        while(!futureTask.isDone()){
            System.out.println("***wait");
        }
        System.out.println(futureTask.get());
        System.out.println(Thread.currentThread().getName()+" come over");
    }
}

2.线程池

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程。

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类

编码实现

  • Executors.newFixedThreadPool(int); 一池N线程
  • Executors.newSingleThreadExecutor();一池一线程
  • Executors.newCachedThreadPool();可扩容
public static void main(String[] args) {
        ExecutorService threadpool1 = Executors.newFixedThreadPool(3);//一池多线程
        //ExecutorService threadpool2= Executors.newSingleThreadExecutor();//一池1线程
        //ExecutorService threadpool3 = Executors.newCachedThreadPool();//一池可扩展


        try {
            for (int i = 0; i <10 ; i++) {
                threadpool1.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"正在办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadpool1.shutdown();
        }
    }

线程池底层原理

ThreadPoolExecutor的七大参数

1、corePoolSize:线程池中的常驻核心线程数(惰性加载,执行execure方法才会加载线程)

2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1

3、keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止。

4、unit:keepAliveTime的单位

5、workQueue:任务队列,被提交但尚未被执行的任务

6、threadFactory:表示生成线程池中工作线程的线程工厂,
用于创建线程,一般默认的即可

7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝请求执行的runnable的策略。

工作原理

  1. 在创建了线程池后,线程池中的线程数为零。
  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    • 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?

答:根据业务逻辑自己手写线程池

ExecutorService threadPool = new ThreadPoolExecutor(
      //线程池中的常驻核心线程数
      2, 
      //最大线程数                                 
      5, 
      //多余的空闲线程的存活时间                                       
      2L,
      //时间单位                                       
      TimeUnit.SECONDS,                
      //任务队列   
      new ArrayBlockingQueue<Runnable>(3),
       //生成线程池中工作线程的线程工厂
      Executors.defaultThreadFactory(),        
      //拒绝策略      
      new ThreadPoolExecutor.DiscardOldestPolicy() 
 
    //new ThreadPoolExecutor.AbortPolicy()    //直接抛出RejectedExecutionException异常阻止系统正常运行
    //new ThreadPoolExecutor.CallerRunsPolicy() //将某些任务回退到调用者
    //new ThreadPoolExecutor.DiscardOldestPolicy()//抛弃队列中等待最久的任务,然后把当前任务加人队列中
尝试再次提交当前任务。
        );

六.JUC常用的辅助工具类

CountDownLatch

 * CountDownLatch主要有两个方法

  • 当一个或多个线程调用await方法时,这些线程会阻塞。
  •  其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)

 * 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。

package cn.hbuas.springcloud;

import java.util.concurrent.CountDownLatch;

/*
 *@auther 阿伟正是在下
 *@create 2020/3/16 22:37
 *
 * 6个同学陆续离开教室后值班同学才可以关门。
 * main主线程必须要等前面6个线程完成全部工作后,自己才能开干
 */
public class CountDownLathDemo {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(6);

            for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致
            {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
                    countDownLatch.countDown();
                }, String.valueOf(i)).start();
            }
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长");
        }
    }

CyclicBarrier

* CyclicBarrier可循环(Cyclic)使用的屏障(Barrier)。

它要做的事情是,让一组线程到达一个同步点时被阻塞,直到最后一个线程到达同步点 时, 同步点才会开门,所有被 同步点 拦截的线程才会继续干活。
 线程进入 同步点 通过CyclicBarrier的await()方法。

package cn.hbuas.springcloud;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/*
 *@Description: TODO(CyclicBarrierDemo)
 *@auther 阿伟正是在下
 *@create 2020/3/16 22:51
 */
public class CyclicBarrierDemo {
    private static final int NUMBER = 7;

    public static void main(String[] args) {
        //CyclicBarrier(int parties, Runnable barrierAction)

        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("*****集齐7颗龙珠就可以召唤神龙");
        });

        for (int i = 1; i <= 7; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "\t 星龙珠被收集 ");
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            }, String.valueOf(i)).start();
        }


    }
}

Semaphore

 在信号量上我们定义两种操作:
 * acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
 * release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
 
 * 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

package cn.hbuas.springcloud;

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/*
 *@Description: TODO(SemaphoreDemo )
 *@auther 阿伟正是在下
 *@create 2020/3/16 22:57
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);//模拟3个停车位

        for (int i = 1; i <= 6; i++) //模拟6部汽车
        {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t 抢到了车位");
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName() + "\t------- 离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }

    }

}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!