Java并发编程(二):具体使用

前言:本文为原创 若有错误欢迎评论!

一.线程间的通信

1.wait、notify、notifyAll

  1. 何时使用
    在多线程环境下,有时候一个线程的执行,依赖于另外一个线程的某种状态的改变,这个时候,我们就可以使用wait与notify或者notifyAll

  2. wait跟sleep的区别
    wait会释放持有的锁,而sleep不会,sleep只是让线程在指定的时间内,不去抢占cpu的资源

  3. notify跟notifyAll的区别
    nofity随机唤醒一个等待的线程
    notifyAll唤醒所有在该对象上等待的线程

注意点:

  1. wait notify必须放在同步代码块中, 且必须拥有当前对象的锁,即不能取得A对象的锁,而调用B对象的wait
  2. 哪个对象wait,就得调哪个对象的notify)

2.join

  1. 使用场景:
    线程A执行到一半,需要一个数据,这个数据需要线程B去执行修改,只有B修改完成之后,A才能继续操作

  2. 使用方法:
    线程A的run方法里面,调用线程B的“threadB.join()”方法,这个时候,线程A会等待线程B运行完成之后(join的前提时线程已经启动),再接着运行

3.ThreadLocal

  • 线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。为每个线程单独存放一份变量副本(也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值)
ThreadLocal stringThreadLocal=ThreadLocal.withInitial(()->"test");

public void test(){
    stringThreadLocal.set(stringThreadLocal.get()+"ttt");
    System.out.println(stringThreadLocal.get());
}
  • 常用方法:

ThreadLocal.initialValue(()->[要初始化的值 必须和泛型一致])
ThreadLocal没有被当前线程赋值时 或当前线程刚调用remove()后再调用get(),返回此初始的值

ThreadLocal.get()
获取ThreadLocal中当前线程共享变量的值。

ThreadLocal.set(Object)
设置ThreadLocal中当前线程共享变量的值(和泛型的类型相同)

ThreadLocal.remove
移除ThreadLocal中当前线程共享变量的值

6.Condition

  • 可以在一个锁里面,存在多种等待条件
public class Medium{  //线程通信的生产者消费模型

    private ReentrantLock lock=new ReentrantLock(false);
    private Condition condition1=lock.newCondition();//生产者的
    private Condition condition2=lock.newCondition();//消费者的
    private int productNum=10;

    public void producer(){
        lock.lock();
            try {
                if(productNum<20){
                    productNum++;
                    condition1.signalAll();
                    condition2.signalAll();
                    Thread.sleep(100);
                    System.out.println("生产,商品总数"+productNum);
                }else {
                    condition1.await();
                    condition2.signalAll();
                    System.out.println("生产满,商品总数"+productNum);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
    }

    private void consumer(){
        lock.lock();
        try {
            if(productNum>0){
                productNum--;
                condition1.signalAll();
                condition2.signalAll();
                Thread.sleep(100);
                System.out.println("消费,商品总数"+productNum);
            }else {
                condition1.signalAll();
                condition2.await();
                System.out.println("消费完,商品总数"+productNum);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Medium test = newMedium();

        for(int i=0;i<5;i++){
            new Thread(()->{
                for(;;){
                    test.consumer();
                }
            }).start();
        }

        for(int i=0;i<3;i++){
            new Thread(()->{
                for(;;){
                    test.producer();
                }
            }).start();
        }
    }
}
  • 主要的方法

await:让线程等待

signal:唤醒其中一个同一condition对应的wait的线程

signalAll:唤醒其中全部同一condition对应的wait的线程

  • 即:
    • 和synchronized(obj){ obj.wait() } 、synchronized(obj){ obj.notify() } 使用不同的obj差不多
    • 使用LocknewCondition可以获得多个不同Condition对象
    • Lock.lock()加锁 定义不同的Condition来区分要唤醒的对象

二.原子类

1.基本类型

AtomicBoolean、AtomicInteger、AtomicLong
元老级的原子更新,方法几乎一模一样

DoubleAdder、LongAdder
对Double、Long的原子更新性能进行优化提升

DoubleAccumulator、LongAccumulator
支持自定义运算

LongAccumulator longAccumulator=new LongAccumulator(((left, right) -> left>right?left:right,99);
  • 注意:
    [Type]BinaryOperator accumulatorFunction是一个运算函数作为参数 用lamdam表达式“(left,right)->{ }”)

2.数组类型

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

int [] arr={1,2,3};
AtomicIntegerArray atomicIntegerArray=new AtomicIntegerArray(arr);

3.更新对象属性

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference、AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater atomicReferenceFieldUpdater=AtomicReferenceFieldUpdater.newUpdater(User.class,String.class,"name");
  • 注意:
    • 要修改的字段必须可见 即字段类型要不是public 要不就完全在一个包底下 用默认的default修饰(default是完全一个包下可访问)
    • 字段必须是volatile类型的,在线程之间共享变量时保证立即可见
    • 对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
    • 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)如果要修改包装类型就需要使用AtomicReferenceFieldUpdater

4.更新对象引用

AtomicReference 、AtomicMarkableReference(返回boolean类型版本戳)、AtomicStampedReference(返回int类型版本戳)

AtomicReference atomicReference=new AtomicReference<>();

User user1=new User(99,"test1");
User user2=new User(99,"test2");
atomicReference.set(user1);
atomicReference.compareAndSet(user1,user2);//设置的和比较的(第一个)是一个对象才可以实现set
atomicReference.get().getName();
  • 用法:
    • 先给atomicReference通过.set()设置一个对象
    • 然后就可以让别的线程一起修改这个对象
    • 最后再调用get()获得这个对象

三.容器

  • 容器的实现就是集合 数组

1.同步容器

  • 单独使用里面的方法的时候,可以保证线程安全,但是,复合操作需要额外加锁来保证线程安全

Vector stringVector = new Vector<>();//本质也是继承了List接口

正确得在迭代中删除(易错)

// 错误删除(在遍历时删除)
// stringVector.forEach(e->{
//      if (e.equals("demo3")) {
//          stringVector.remove(e);
//      }
// });

//正确迭代(用迭代器)
Iterator stringIterator = stringVector.iterator();
while (stringIterator.hasNext()) {
    String next = stringIterator.next();
    if (next.equals("demo")) {
        stringIterator.remove();
    }
}

Collections.synchronizedXXX (本质是对相应的容器进行包装 实现线程安全)

List synchronizedList = Collections.synchronizedList(stringVector);

2.并发容器

  • 根据具体场景进行设计,使用时尽量避免有锁,提高容器的并发访问性
  1. CopyOnWriteXXX 系列(用法和选定的Collection相似)
CopyOnWriteArrayList strings = new CopyOnWriteArrayList<>();

迭代中删除

//错误 并发容器不支持迭代删除
// Iterator iterator = strings.iterator();
// while (iterator.hasNext()) {
//     String next = iterator.next();
//     if (next.equals("demo2")) {
//         iterator.remove();
//     }
// }

//正确 用自带得foreach
strings.forEach(e->{
    if (e.equals("demo2")) {
        strings.remove(e);
    }
});
  1. ConcurrentXXX 系列

ConcurrentBlockingQueue:基于queue实现的FIFO的队列。队列为空,取操作会被阻塞
ConcurrentLinkedQueue,队列为空,取得时候就直接返回空

  1. BlockingQueue 系列
    • 阻塞队列(即队列的任务没有完成就一直等待 如果满了就无法再放入 不会舍弃队列最早的任务)
    • 只有LinkedBlockingQueue一个实现 但是作为并发得队列最常用)

LinkedBlockingQueue
在并发编程中LinkedBlockingQueue使用的非常频繁 其可以作为生产者消费者的中间商

LinkedBlockingQueue stringLinkedBlockingQueue = new LinkedBlockingQueue<>();
//往队列里存元素
strings.add("111");
strings.offer("111");
strings.put("111");

//从队列中取元素
String remove = strings.remove();
strings.poll();
strings.take();

并发容器中Quene用法

  • 添加:

add:实际上调用的是offer,区别是在队列满的时候,add会报异常
offer:对列如果满了,直接入队失败
put(“111”):在队列满的时候,会进入阻塞的状态

  • 删除(也是取出并获得):

remove():直接调用poll,唯一的区别即使remove会抛出异常,而poll在队列为空的时候直接返回null
poll():队列为空的时候直接返回null
take():在队列为空的时候,会进入等待的状态

四.并发工具类

1.CountDownLatch

countDownLatch.await(),进入等待的状态
countDownLatch.countDown(),计数器减一

注意:构造方法设置需要完成得个数 然后用await()阻塞住线程 等其他线程就绪用countDown() 使个数减为零时 等待的线程继续执行

CountDownLatch countDownLatch = new CountDownLatch(8);

new Thread(()->{
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("800米比赛结束,准备清空跑道并继续跨栏比赛");
}).start();

for (int i = 0; i < 8; i++) {
    int finalI = i;
    new Thread(()->{
        try {
            Thread.sleep(finalI * 1000L);
            System.out.println(Thread.currentThread().getName()+"到达终点");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
    }).start();
}

2.CyclicBarrier–栅栏

cyclicBarrier.await() 进入等待的状态
多个线程一起等待的个数达到构造方法的个数 就都可以往下执行

CyclicBarrier cyclicBarrier = new CyclicBarrier(8);

for (int i = 0; i < 8; i++) {
    int finalI = i;
    new Thread(() -> {
        try {
            Thread.sleep(finalI * 1000L);
            System.out.println(Thread.currentThread().getName() + "准备就绪");
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("开始比赛");
    }).start();
}
  • 跟countDownLatch的区别
    • CountDownLatch一般用于某个线程等待若干个其他线程执行完任务之后,它才执行;不可重复使用
    • CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;可重用的

3.Semaphore–信号量

semaphore.acquire() 相当于获取锁
semaphore.release() 相当于释放锁
相当于在构造函数设置可以获取到锁的线程的数量 然后用acquire获取锁 realse释放锁

Semaphore semaphore = new Semaphore(2);

for (int i = 0; i < 10; i++) {
    new Thread(()->{
        try {
            semaphore.acquire();//获取锁
            System.out.println(Thread.currentThread().getName() + "开始执行");
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();//释放锁
        }
    }).start();
}
  • 用于:
    • 控制并发数量
    • 使用场景:接口限流)

4.Exchanger

exchanger.exchange(obj) //交换数据 会把交换的到的值返回
是泛型 用于交换数据

Exchanger stringExchanger = new Exchanger<>();

String str1 = "xdclass";
String str2 = "wiggin";

new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + "初始值==========>" + str1);
    try {
        String exchange = stringExchanger.exchange(str1);//交换时先到的线程会等待同一个Exchanger对象的exchange方法调用
        System.out.println(Thread.currentThread().getName() + "交換后的数据==========>" + exchange);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, "线程1").start();

new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + "初始值==========>" + str2);
    try {
        String exchange = stringExchanger.exchange(str2);
        System.out.println(Thread.currentThread().getName() + "交換后的数据==========>" + exchange);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, "线程2").start();
  • Exchanger提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当同一Exchanger对象两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方,因此该工具类的线程对象是【成对】的。

  • 注意:
    并发编程一定要很多时候要注意操作同一: 如同一个锁(不能new了多个锁 然后用不同的锁去lock)、同一个工具类(不同对象的工具类无法达到效果)、同一操作对象(在run方法里调用一个类的方法时 如果不是一个对象的的同一方法 数据不仅不一致 而且各个线程操作的不是同一目标)

五.线程池

1.ThreadPoolExecutor

 public ThreadPoolExecutor(int corePoolSize,
                int maximumPoolSize, 
                long keepAliveTime,
                TimeUnit unit,
                BlockingQueue workQueue,
                ThreadFactory threadFactory,
                RejectedExecutionHandler handler)

corePoolSize:核心线程池大小
maximumPoolSize:线程池最大容量
keepAliveTime:当线程数量大于核心时,多余的空闲线程在终止之前等待新任务的最大时间。
unit:时间单位
workQueue:工作队列 nWorks
ThreadFactory:线程工厂
handler:拒绝策略

LinkedBlockingQueue blockingQueue=new LinkedBlockingQueue<>(10); //不设置参数就没有限制任务队列大小 又可能堆积任务(注意 泛型必须时Runnable才是线程的任务)
blockingQueue.put(()->{  //只给任务队列放入一条任务
    System.out.println(Thread.activeCount());
});
ThreadPoolExecutor poolExecutor=new ThreadPoolExecutor(10,20,60,TimeUnit.SECONDS,blockingQueue);
poolExecutor.prestartAllCoreThreads();
  • 注意:
    通过new创建线程池时,除非调用prestartAllCoreThreads方法初始化核心线程,否则此时线程池中有0个线程,即使工作队列中存在多个任务,同样不会执行

2.运行机制

按照:核心线程->核心线程+任务队列->核心线程+全部线程->拒绝策略

任务数X
      x <= corePoolSize 
            只启动x个线程

      x >= corePoolSize && x < workQueueNums + corePoolSize 
            会启动 <= corePoolSize 个线程 其他的任务就放到工作队列里

      x > corePoolSize + workQueueNums 
            x <= maximumPoolSize + workQueueNums   
               会启动x-(workQueueNums )个线程
            x > maximumPoolSize + workQueueNums
               会启动mSize个线程来执行任务,其余的执行相应的拒绝策略    

3.Future与Callable、FutureTask

  • Callable与Runable功能相似,Callable的call有返回值,可以返回给客户端,而Runable没有返回值一般情况下,
  1. Callable与FutureTask一起使用
    • FutureTask则是一个RunnableFuture,而RunnableFuture实现了Runnbale又实现了Futrue这两个接口
    • 因为Thread的构造参数是Runnable所以Callable接口必须使用FutureTask使其有Runnable接口
public class CallableDemo implements java.util.concurrent.Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("callable");
        return "success";
    }
}

FutureTask stringFutureTask=new FutureTask<>(new CallableDemo());
new Thread(stringFutureTask).start();
System.out.println(stringFutureTask.get());

返回的结果会放在FutureTask中 用get取出返回的结果

  1. 通过线程池的submit方法返回相应的Future
  • Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。get方法会阻塞,直到任务返回结果
ThreadPoolExecutor poolExecutor=new ThreadPoolExecutor(10,20,60,TimeUnit.SECONDS,blockingQueue);
Future future = poolExecutor.submit(new Callable());

System.out.println(future.get());
  • 注意:
    • 用threadPoolExecutor的submit 不管runable还是callable都会返回future
    • future不一定是存返回结果 如果用submit提交线程池 就算有异常也不会在运行时抛出 用get从Future取出后才抛出)
  1. submit和execute

submit 如果发生异常,不会立即抛出,而是在get的时候,再抛出异常
execute 直接抛出异常

3.Executor框架

  • 通过不同的构造参数 最终都创建了6种ThreadPoolExecutor
		ExecutorService executorService = Executors.newCachedThreadPool();
        ExecutorService executorService1 = Executors.newFixedThreadPool(2);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ExecutorService executorService2 = Executors.newWorkStealingPool();
        ExecutorService executorService3 = Executors.newSingleThreadExecutor();
        ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();

newCachedThreadPool:创建一个可以根据需要创建新线程的线程池,如果有空闲线程,优先使用空闲的线程

newFixedThreadPool:创建一个固定大小的线程池,在任何时候,最多只有N个线程在处理任务

newScheduledThreadPool:能延迟执行、定时执行的线程池

newWorkStealingPool:工作窃取,使用多个队列来减少竞争

newSingleThreadExecutor:单一线程的线程次,只会使用唯一一个线程来执行任务,即使提交再多的任务,也都是会放到等待队列里进行等待

newSingleThreadScheduledExecutor:单线程能延迟执行、定时执行的线程池

4.线程池拒绝策略

AbortPolicy:该策略直接抛出异常,阻止系统正常工作

CallerRunsPolicy:只要线程池没有关闭,该策略直接在调用者线程中,执行当前被丢弃的任务(叫老板帮你干活)

DiscardPolicy:直接啥事都不干,直接把任务丢弃

DiscardOldestPolicy:丢弃最老的一个请求(任务队列里面的第一个),再尝试提交任务

5.线程池的使用建议

  • 尽量避免使用Executor框架创建线程池

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

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

  • 创建线程池时,核心线程数不要过大

  • 相应的逻辑,发生异常时要处理
    即:使用excute不用submit 然后捕获异常处理 不要等反馈了再处理

你可能感兴趣的:(JAVA,JVM,并发,设计模式)