JUC基础笔记

juc,即java.util.concurrent包的缩写,掌握了juc,就是拿到了Java并发编程的钥匙。

在《Java并发编程实战》等书中,已经详细介绍juc用法,如果你懒得看书,或者是忘了juc的用法,想快速回忆一下,可以看我这篇教程。

本教程很长,有很多的代码示例供食用~

基础

volatile关键字

volatile关键字不属于juc的内容,但是为了铺垫后面的内容,这里先介绍下。

当多个线程之间共享一个数据时,该数据对彼此之间是不可见的。即使是同一个数据,每个线程还是会将其保存在自己独立的内存下面。

下面的代码显示了这一特性:

class Worker implements Runnable {
   

    public boolean flag = false;

    @Override
    public void run() {
   

        try {
   
            Thread.sleep(200);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }

        this.flag = true;
    }
}

public class VolatileDemo {
   

    public static void main(String[] args) {
   
        Worker worker = new Worker();

        new Thread(worker).start();

        while (!worker.flag) ;

        System.out.println("程序结束");
    }
}

按理说,worker线程将flag改为true,主线程在flag变为true之后会及时跳出循环,程序退出。

但是,实际运行下来程序并没有退出,这是因为worker线程的flag和主线程的flag是不共享的,workerflag的修改并不会影响到主线程。

要想改变这一点,需要将flag声明为volatile的。这个关键词的作用是,当变量被某个线程改变时,会及时刷新到主存中,读取时也会从主存中读取。可以保证变量是线程之间可见的。

要想让上面的程序及时退出,将上面的flag声明改为:

public volatile boolean flag = false;

这样worker对flag的改变对于主线程就是可见的了,程序可以及时退出了。

原子性

如果一个变量需要被多个线程同时访问,对其进行操作就要格外当心。除了可见性问题,可以使用volatile修正,还有原子性问题。

如果一个变量的操作需要多步完成,操作可以细分,则该操作就不具备原子性,例如i++操作就不具备原子性。在并发操作时,就可能因为线程执行非原子操作导致数据读写不一致的情况。

我们可以通过给操作加上synchronized关键字,让操作只能允许一个线程进行,来实现操作的原子性。

另一种实现原子性的方法是使用CAS操作(Compare And Swap)。CAS操作由CPU直接提供,CAS需要下面三个操作数:

  • valueOffset:变量在内存中的位置
  • expect:变量的预估值
  • update:变量的更新值

CAS的操作过程:

  • 从valueOffset取出value,若等于expect,则将valueOffset的值设为update
  • 否则不进行任何操作

那么想要将i++变为原子的,只需要将valueOffset设为iexpect设为读取到的i的值,update设为i+1。这样,只有当数据一致时,才会执行i+1操作。

java.util.concurrent.atomi下,提供了很多原子变量,这些变量都具备:

  • 使用volatile确保变量可见性
  • 使用CAS操作确保操作是原子的

例如,下面的代码:

package cn.offer.juc;

import java.util.concurrent.atomic.AtomicInteger;

class AtomWorker implements Runnable {
   

    private AtomicInteger i = new AtomicInteger();

    @Override
    public void run() {
   
        while (true) {
   
            System.out.println(i.addAndGet(1));
            try {
   
                Thread.sleep(100);
            } catch (InterruptedException e) {
   
                e.printStackTrace();
            }
        }
    }
}

public class Atomicity {
   

    public static void main(String[] args) {
   
        AtomWorker worker = new AtomWorker();
        for (int i = 0; i < 10; ++i) {
   
            new Thread(worker).start();
        }
    }
}

就可以保证各个线程不会读到重复的i

ThreadPool

线程池的概念不再介绍,这里只介绍juc提供的线程池操作。

要想说线程池,就不得不说一下juc的Executor执行框架,在这个框架下,所有的并发执行单位都以“任务”的形式存在,将任务提交给ExecutorService,即可实现任务的并发调度执行。

ExecutorService可以有很多种,它负责接收任务,执行任务,使用Executors可以创建各种ExecutorService,有下面几种常用的:

  • newSingleThreadExecutor:单一线程,任务会顺序执行
  • newCachedThreadPool:大小不受限制的线程池
  • newFixedThreadPool:大小固定的线程池,当线程不够时,任务需要等待
  • newScheduledThreadPool:大小固定线程池,支持定时及周期性任务执行

ExecutorService提供了下面的将Runnable任务提交执行的方法:

<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

关于Future的使用,见Callable和Future,这里不关心。

下面的代码演示了将线程提交给线程池执行:

package cn.offer.juc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Task implements Runnable {
   

    @Override
    public void run() {
   
        System.out.println(Thread.currentThread().getId() + "执行");
        try {
   
            Thread.sleep(1000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getId() + "执行完毕");
    }
}

public class ThreadPoolDemo {
   
    public static void main(String[] args) {
   
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; ++i) {
   
            service.submit(new Task());
        }
    }
}

Callable和Future

使用传统的Runnable,可以启动一个线程并发执行,但是run方法是没有返回值的,如果我们想要线程能够返回一个值,就可以使用Callable+Future

我们想要一个线程能返回值,这时候让其实现java.util.concurrent.Callable接口,在泛型中指定返回类型,例如,我们让一个worker返回字符串:

class CallableWorker implements Callable<String> {
   
    @Override
    public String call() throws Exception {
   
        Thread.sleep(2000);
        return "运行完毕";
    }
}

这个call方法和传统的run方法相比,有两个不同:

  • 方法有返回值
  • 方法可以抛出异常

那么,如何执行呢?一般使用ExecutorService来执行,该接口中有如下这个方法:

<T> Future<T> submit(Callable<T> task);

Future用于查询执行的Callable(或Runnable)的执行结果、是否完成等信息。该接口的定义如下:

public interface Future<V> {
   

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

这些函数的解释如下:

  • cancel:尝试取消任务的执行。
    • mayInterruptIfRunning:是否允许取消已经启动但是没有执行的任务。
    • 返回:有如下几种情况:
      • 任务已经完成,返回false
      • 任务还没有启动,取消任务,返回取消结果。
      • 任务已经启动,但是mayInterruptIfRunningfalse,返回false
      • 任务已经启动,且mayInterruptIfRunningtrue,取消任务,返回取消结果。
  • isCancelled:返回任务是否在其正常结束之前被取消。
  • isDone:任务是否结束。不论是任务正常结束、抛出异常、被cancel,该函数都会返回true。
  • get():阻塞直到任务结束,随后获取其返回值。
  • get(timeout, unit):在指定的timeout时间内等待任务结束并获取结果,如果超过这个时间没有结束,抛出TimeoutException异常。

另外说明一下get方法可能抛出的其它异常:

  • CancellationException:在等待途中任务被cancel
  • ExecutionException:任务抛出了异常
  • InterruptedException:阻塞过程中被打断

通过Future,我们就可以获取任务的返回值了:

public class CallableDemo {
   

    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        CallableWorker worker = new CallableWorker();
        // 单一线程执行器
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(worker);

        // 获取线程的执行结果
        System.out.println("执行结果:" + future.get());
    }

}

你也可以用Future做很多其它事情,就看你自己发挥了。

Lock

Lock接口的定义如下:

public interface Lock {
   

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

下面简单介绍一下这些方法:

  • lock:获取锁。如果锁已经被占用,会阻塞。
  • lockInterruptibly:可中断地等待锁,如果等待被中断,抛出InterruptedException
  • tryLock:尝试获取锁,如果获取失败,返回false。
  • tryLock(time, unit):在timeout时间内尝试获取锁,如果在这段时间内获取到锁,返回true,如果没有,返回false。
  • unlock:释放锁
  • newCondition:返回一个绑定到该锁的Condition示例,关于Condition,见Condition一节

我们一般会使用到下面这个Lock的实现类:

  • ReentrantLock:可重入锁,也叫递归锁。指的是,当一个线程获取锁之后,再次获取时,不需要重复等待,可以直接获取锁。
    • 构造时将fair设为true,表示公平锁,公平锁指的是严格按照先来先得的顺序排队等待去获取锁。
    • 构造时将fair设为false,表示非公平锁,非公平锁每次获取锁时,是先直接尝试获取锁,获取不到,再按照先来先得的顺序排队等待。
    • 默认是非公平锁。

锁的操作不难,下面我们重点介绍下读写锁。

ReadWriteLock

读写锁指的是没有线程进行写操作时,多个线程可同时进行读操作,当有线程进行写操作时,其它读写操作只能等待。

即,对于读写锁来说,“读-读能共存,读-写不能共存,写-写不能共存”。

ReadWriteLock接口定义如下:

public interface ReadWriteLock {
   

    Lock readLock();

    Lock writeLock();
}

其中,readLock用于获取读锁,writeLock用于获取写锁。

我们一般使用实现类ReentrantReadWriteLock,即可重入的读写锁。

下面我们来看一个具体的例子:

package cn.offer.juc
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值