菜只因复习】Java复习二

51 阅读17分钟

Java复习

多线程

进程和线程

进程 具有一定独立功能的程序对某些数据的运行活动,是操作系统的分配资源的基本单元;例如我们可以一边听音乐,一边使用word,还时不时的用微信和他人聊天,每个程序都代表一个进程。 线程 一个进程拥有多个线程,是进程中独立运行的基本单位;线程不拥有系统资源,但是可以利用进程所拥有的资源,所以线程间的上下文切换比进程间的上下文切换开销要小得多;例如我们使用word的时候,可以看到word在实时检查我们的语法,又或者在我们在使用音乐软件时,可以边听音乐边搜索音乐信息等操作,都是多线程的表现

并发和并行

并行 两个或多个事件在同一时刻发生 并发 两个或多个事件在同一时间段内发生 用一个之前知乎看到的例子 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

线程的状态

  • NEW 新建
  • RUNNABLE 运行
  • BLOCKED 阻塞
  • WAITING 等待
  • TIMED_WAITING 计时等待
  • TERMINATED 终止 在这里插入图片描述

线程的创建

线程创建的方法有以下几种

  • 继承Thread类
  • 实现Runnable
  • 实现Callable
  • 使用线程池创建线程

继承Thread类

public class ThreadTest {
    public static void main(String[] args) {
        MyThread th = new MyThread("子线程");
        th.start();
        System.out.println("主线程在运行");
    }
}

class MyThread extends Thread{

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始运行...");
        System.out.println("运行中...");
        System.out.println(Thread.currentThread().getName() + "运行结束...");
    }
}

实现Runnable接口

Runnable实现类不能独自运行,需要借助Thread类

public class RunnableTest {
    public static void main(String[] args) {
        Thread th = new Thread(new MyRunnabled(),"线程2");
        th.start();
        System.out.println("主线程在运行");
    }
}
class MyRunnabled implements  Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在运行");
    }
}

// 得到的结果是
主线程在运行
线程2正在运行

我们也可通过lambda表达式去实现Runnable接口

public class RunnableTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "正在运行");
        },"线程B");
        thread.start();
        System.out.println("主线程在运行");
    }
}

实现Callable接口

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        // 使用FutureTask 执行实现Callable接口的线程
        FutureTask futureTask = new FutureTask(myCallable);
        Thread th = new Thread(futureTask);
        th.start();
        System.out.println("得到结果:" + futureTask.get());
    }
}

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int result = 0;
        System.out.println(Thread.currentThread().getName()+"开始执行1-10000的计算结果");
        for (int i = 1; i <= 10000; i++) {
            result+=i;
        }
        System.out.println(Thread.currentThread().getName()+"计算完成");
        return result;
    }
}

Future和FutureTask

public interface Future<V> {

    /**
     * 尝试取消开始执行的任务
     * 若该任务已经完成或者已经取消,或某些原因导致无法取消时返回false
     * 否则返回true
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
     * 在任务完成前取消成功则返回true
     */
    boolean isCancelled();

    /**
     * 返回true表示任务完成
     */
    boolean isDone();

    /**
     * 获取计算结果
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * 在指定时间去获取返回结果
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

RunnableFuture

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

FutureTask;这里只记录部分源码


/**
 * 因为FutureTask接口实现了Runnable接口,所以可以将FutureTask交给Thread去执行
 */
public class FutureTask<V> implements RunnableFuture<V> {
	...

	public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // 线程状态
    }
    
    ...
}

我们看一下它们之间的类图 在这里插入图片描述 当然不一定非得交给Thread类才可以启动线程,还有其他方法,后面会提到。现在说一下最后一种创建线程的方式

Executors工具类创建线程

Executors中的方法 在这里插入图片描述 这些newXXX的方法都是用于创建线程池,返回类型要么是ExecutorService,要么是ScheduledExecutorService;从类图可以看出顶层接口是Executor 在这里插入图片描述

public interface Executor {
    void execute(Runnable command);
}

在Executor接口基础上,多了停止任务的方法;支持提交Callable方法

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

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

    /**
     * 完成子线程任务后,返回result
     */
    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    /**
     * 返回一组子任务的Future对象
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

而ExecutorService的实现类我们最熟悉的就是ThreadPoolExecutor 接下来我们就通过Executors来创建线程

public class ExecutorServiceDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        pool.submit(() -> {
            System.out.println("开始执行");
            try {
                System.out.println("执行中...");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束执行");
        });
        pool.shutdown();
    }
}

线程池

上面描述了3种创建线程的方法。然而我们在创建线程的时候,建议使用线程池。

为什么要使用线程池

  1. 降低资源消耗;通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度;当任务到达时,任务不需要等待线程创建,就可以立即执行
  3. 提高线程的可管理性;线程是稀缺资源,如果无限制创建线程,会消耗系统资源,降低系统稳定性,使用线程池可进行统一的分配调优和监控

线程是计算机系统的重要资源,JVM中每创建一个线程就需要调用操作系统提供的API创建线程,销毁线程也需要通过调用系统来完成;系统调用就需要上下文切换,就意味销毁系统资源。所以我们需要避免线程频繁地创建与销毁,因此我们需要缓存一批线程,让它们时刻准备着执行任务。

ThreadPoolExecutor

我们前面分析了ExecutorService中实现类有ThreadPoolExecutor;Executors中有很多工具方法就是使用ThreadPoolExecutor创建线程。下面我们就分析一下

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

ThreadPoolExecutor有7个参数

  • corePoolSize;核心线程数,当任务数量小于核心线程数时,会直接创建线程去执行任务
  • maximumPoolSize;最大线程数,表示线程池最多有maximumPoolSize的线程执行
  • keepAliveTime ;非核心线程的空闲时间,当前正在执行的线程数大于核心线程数后,且这些线程都是空闲状态的时候,经过keepAliveTime 的时间后都会被回收
  • unit;空闲时间单位
  • workQueue;阻塞队列,当待执行任务数量大于核心线程数后,优先会保存到阻塞队列中
  • threadFactory;线程工厂
  • handler;拒绝策略,当执行线程数达到 maximumPoolSize,且阻塞队列也是满的状态时,就会根据拒绝策略做出响应。有4种拒绝策略: AbortPolicy:直接抛出RejectedExecutionException异常 CallerRunsPolicy:将任务退回给调用者调用,从而降低流量 DiscardOldestPolicy:抛弃队列中等待最久的任务,然后添加到队列中 DiscardPolicy:丢弃任务

下面就说一下ThreadPoolExecutor的执行流程

  1. 当前执行数小于核心线程数时,对于新到来的任务则会创建线程并执行
  2. 若此时核心线程数已满的话,则将新任务添加到阻塞队列
  3. 若队列已经满的话,那么就会开启新的线程,去执行新到来的任务,直到线程数达到最大线程数
  4. 此时线程池达到最大线程数,队列也是满的状态,若现在又有新任务到来,就会根据拒绝策略去执行对应的操作。
  5. 当有任务执行完成后,也没有新的任务,就会从阻塞队列中获取任务去执行
  6. 经过一段时间,任务都执行完了;经过keepAliveTime这段时间后,线程池就会将多出的空闲线程回收
// 核心线程数3
// 最大线程数5
// 空闲等待时间5s
// 阻塞队列最大长度为5
// 拒绝策略使用DiscardOldestPolicy
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 5,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());

Executors

我们也可以直接通过Executors工具方法来创建,这里举例几个

public class Executors {

	/**
     * 创建固定线程数的线程池
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
	
	/**
     * 创建一个线程数的线程池
     */
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

	/**
     * 创建一个可缓存的线程池
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

	/**
     * 创建一个可以定时执行线程池
     */
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
}

以上这些方法在阿里巴巴规范中禁止使用;原因是

  1. newFixedThreadPool 和 newSingleThreadExecutor中允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
  2. newCachedThreadPool 和 newScheduledThreadPool中允许创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM

假如遇到的并发量不大情况,我觉得也是可以用的

synchronized

synchronized是Java关键字,是一种JVM层面的锁。它可以保证某段代码某个时刻只有一个线程执行,从而保证线程安全。我们先写个例子,看看如何使用

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Ticket ticket = new Ticket();
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sell();
            }
        }, "A销售窗口");

        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sell();
            }
        }, "B销售窗口");

        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();
        System.out.println("当前剩余票数:" + ticket.getTicketNum());
    }
}

class Ticket {
    private int ticketNum = 100;

    public void sell() {
        if (ticketNum <= 0) {
            return;
        }
        try {
        	// 模拟其他操作
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
    }

    public int getTicketNum() {
        return ticketNum;
    }
}

上面是一段简单的卖票的代码;有100张票,两个线程,各买50张票,最后结果应该是0。但是结果却出现票数有剩余,还有买到相同票的情况,这就是线程安全的问题。原因就在于A线程在执行卖票的方法的同时,B线程也来执行卖票方法。解决的办法就是确保只有一个线程在执行sell方法,这里就可以用到synchronized。 synchronized有两种写法

  1. 同步方法
// 写在方法上
public synchronized void sell() {
    if (ticketNum <= 0) {
        return;
    }
    try {
    	// 模拟其他操作
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
}
  1. 同步代码块
public void sell() {
    if (ticketNum <= 0) {
        return;
    }
    synchronized (this) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
    }
}

那么这两种方法有什么不同呢?

  • 同步方法,锁的粒度是整个方法,线程在调用方法的时候就需要获取锁; 同步代码块,锁的粒度更细,锁的是括号内部的代码。例如上面的代码,多个线程都可以进入到sell方法,但是只有获得锁的线程才可以进入到synchronized代码块内部。
  • 同步方法的锁对象 -- 若方法是实例方法,那么锁对象是当前对象实例(this),所以当线程执行的方法是属于不同对象的话,那么它们使用的锁由于不是同一把,就会导致线程不安 -- 若是静态方法,那么锁的对象是当前class类,即使是有多个对象,由于static成员在整个类只存在一份,所以它们的锁是相同的

注意:假如A线程调用静态synchronized方法,B线程调用非静态synchronized方法,它们是互不影响的,因为使用的不是同一把锁;同理,调用同步方法和非同步方法也是互不影响的

线程通信

经典的例子就是生产者消费者;一个线程在生产产品,而一个或多个线程消费这个产品。直接上代码

public class ProducerConsumerTest {
    public static void main(String[] args) throws InterruptedException {
        List<String> baskets = new ArrayList<>(5);
        Producer p = new Producer(baskets);
        Consumer c1 = new Consumer(baskets);
        Consumer c2 = new Consumer(baskets);
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    p.produce();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer1 = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    c1.consume();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A");

        Thread consumer2 = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    c2.consume();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B");

        producer.start();
        consumer1.start();
        consumer2.start();
        producer.join();
        consumer1.join();
        consumer2.join();
    }
}

class Producer {
    private int count = 5;
    private List<String> baskets;

    public Producer(List<String> baskets) {
        this.baskets = baskets;
    }

    public void produce() throws InterruptedException {
        synchronized (baskets) {
            while (baskets.size() == count) {
                System.out.println("容器已满,等待消费");
                baskets.wait();
            }
            Thread.sleep(100);
            String product = "产品" + new Random().nextInt(10);
            baskets.add(product);
            System.out.println("生产一个产品 => " + product);
            baskets.notifyAll();
        }
    }
}

class Consumer {
    private List<String> baskets;

    public Consumer(List<String> baskets) {
        this.baskets = baskets;
    }

    public void consume() throws InterruptedException {
        synchronized (baskets) {
            while (baskets.size() == 0) {
                System.out.println("等待生产");
                baskets.wait();
            }
            Thread.sleep(100);
            String product = baskets.remove(0);
            System.out.println("消费者" + Thread.currentThread().getName() + "消费一个产品 => " + product);
            baskets.notifyAll();
        }
    }
}

线程通信这里用到了Object类中的wait和notifyAll wait:使当前线程释放占有的对象锁 notify:唤醒一个正在wait当前对象锁的线程,并使其获得锁; notifyAll:唤醒所有正在wait当前对象锁的线程

注意:wait需要写在while中,而不是if,否则可能会发生虚假唤醒。假如只有一个消费者那么不会有问题;但是假如有多个消费者,那么消费者A线程被唤醒后时条件可能已经不满足,产品已经被其他线程消费了,而A线程由于用的是if,被唤醒后就会直接执行后续代码,这样就可能出现只有一个产品,但是消费了两次的问题。

这里看几道面试题

为什么这些方法定义在Object类中,而不是在Thread类中

线程通信是发生在不同线程之间的,假如定义在Thread类中,那么线程之间通信的时候还得需要知道哪个线程拥有着锁,这样反而增加开发难度;而定义在Object的好处,就是可以使任何对象都可以成为锁。这样线程之间通信只需要知道锁是否可用,而不是去关心锁在哪个线程中。

为什么这些通信方法需要在Synchronized中调用

  • 调用wait,也就是当前线程释放锁,而线程进入到synchronized代码块中就是获取到锁了,假如连锁都没有,谈何释放锁,所以要想释放锁,首先就是获得到锁,也就是必须在synchronize的代码块中,而释放锁的线程将会被放入到WaitSet(等待队列),这个后续会详细讲到。
  • 调用notify或者notifyAll就是唤醒WaitSet中的等待线程,然后将锁交给被唤醒的线程,假如连锁都没有,又怎么将锁交给被唤醒的线程呢。 所以wait,notify,notifyAll必须在synchronized中调用,否则会报IllegalMonitorStateException异常

wait和sleep的区别

  • wait定义在Object类中,sleep定义在Thread类中
  • 调用wait会释放锁,而调用sleep不会释放锁
  • wait需要notify或notifyAll从而被唤醒,而sleep可用指定休眠时间,时间到了后就会自动唤醒
  • wait需要在同步方法或同步代码块中调用;而sleep可用任意地方调用

Lock

讲完Synchronized,那么接下来就是Lock;最常用的就是实现类就是ReentrantLock。ReentrantLock和Synchronized用法比较类似,不同的地方就是synchronized是自动加锁,解锁;而ReentrantLock则是需要自己手动加锁,解锁。 我们将synchronized的同步代码块部分用ReentrantLock替换,这样就可以达到同步的效果。下面就是将卖票程序稍微修改一下。

class Ticket {
    private int ticketNum = 100;
    private Lock lock = new ReentrantLock();

    public void sell() {
        if (ticketNum <= 0) {
            return;
        }
        lock.lock();
        try{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
        } finally {
        	// 需要手动释放锁	
            lock.unlock();
        }
    }

    public int getTicketNum() {
        return ticketNum;
    }
}

相比synchronized自动加锁解锁,ReentrantLock的加锁解锁操作更加灵活,除了有lock方法外,还有其他的加锁方法:

  • lockInterruptibly;简单讲就是lockInterruptibly可以被Thread.interrupt中断然后直接抛出异常,而lock无法被Thread.interrup方法中断,直到获取锁后才响应中断
  • tryLock;会尝试获取锁,不管是否获取到都会立刻返回结果
  • tryLock(long timeout, TimeUnit unit);效果和上面的tryLock类似,会在指定时间内去获取锁

ReentrantLock的通信方法也是和wait,notify类似;ReentrantLock通过newCondition方法获取Condition对象来实现线程间通信。我们还是通过一个消费者,生产者的代码作为例子

public class LockProducerConsumerTest {
    public static void main(String[] args) throws InterruptedException {
        VendingMachine vm = new VendingMachine();
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    vm.supplement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer1 = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    vm.sell();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A");

        Thread consumer2 = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    vm.sell();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B");

        producer.start();
        consumer1.start();
        consumer2.start();
        producer.join();
        consumer1.join();
        consumer2.join();
    }
}

class VendingMachine {
    private int count = 5;
    private List<String> volume = new ArrayList<>(5);
    private Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

	// 补货
    public void supplement() throws InterruptedException {
        lock.lock();
        try {
            while (volume.size() == count) {
                System.out.println("售货机商品已满,等待销售");
                condition.await();
            }
            Thread.sleep(100);
            String product = "商品" + new Random().nextInt(10);
            volume.add(product);
            System.out.println("补充一个商品 => " + product);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

	// 销售
    public void sell() throws InterruptedException {
        lock.lock();
        try {
            while (volume.size() == 0) {
                System.out.println("等待补货");
                condition.await();
            }
            Thread.sleep(100);
            String product = volume.remove(0);
            System.out.println("消费者" + Thread.currentThread().getName() + "消费一个产品 => " + product);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

synchronized和Lock的区别

  • synchronized是Java关键字;而Lock是Java类
  • synchronized不能判断锁的状态;而Lock则可以通过多个方法获得锁的状态,如isFair,isLocked,getHoldCount等等
  • synchronized自动释放锁;而Lock需要手动释放锁
  • synchronize的会一直等待直到获取到锁;Lock可以通过tryLock方法在指定时间内获取锁,超出时间范围则放弃获取锁
  • synchronized是可重入锁,不可中断,非公平;Lock是可重入锁,可中断,公平,非公平锁

线程辅助类

在java.util.concurrent包下有三个线程辅助类,可以帮助我们实现一些特定需求的多线程场景。下面我们分别了解一下

CountdownLatch

CountdownLatch的作用就是可以让线程1等待多个线程都执行完后,线程1再开始执行;就例如跑步比赛,需要等各个运动员都准备好,才可以开始比赛。下面就用代码模拟跑步比赛的场景

public class CountdownLatchTest {
    public static void main(String[] args) throws InterruptedException {
    	// 传入10,代表要调用10次countDown方法
    	// 在这里就代表需要等待10个运动员都准备好后,才可以开始比赛
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
        	// 一个线程代表一个运动员
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"做好准备");
                // 运动员准备好后,调用countDown方法
                latch.countDown();
            },(i+1) + "号选手").start();
        }
		// 10个运动员准备好后(调用了10次countDown方法),主线程才会继续执行
        latch.await();
        System.out.println("比赛开始");
    }
}

CyclicBarrier

没什么好例子,所以还是用上面的跑步比赛作为例子。这次我希望打印比赛开始后,再打印每个运动员起跑。CountdownLatch中调用countDown方法后依旧会继续执行,这就会导致在比赛开始的时候,就打印运动员起跑。这里就可以使用CyclicBarrier。

public class CyclicBarrierTest {
    public static void main(String[] args) {
    	// 
        CyclicBarrier barrier = new CyclicBarrier(10,()->{
            System.out.println("比赛开始");
        });
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"做好准备");
                try {
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"起跑");
            },"运动员"+i).start();
        }
    }
}

可以看到这两个工具类的用法很相似,我们做个总结

CountdownLatch和CyclicBarrier的区别

  • CountdownLatch,我们首先对CountdownLatch对象指定了一个数值count,通过调用await方法阻塞主线程,等待所有子线程结束并调用countDown方法,即对count进行减1的操作;当count等于0的时候,主线程才会继续执行await后续的代码
  • 而CyclicBarrier是子线程到达某个点的时候调用await方法,使其暂停执行,然后等待所有的子线程都到达某个点(调用await的地方),再一起执行后续代码。CyclicBarrier内部其实也是对变量count进行减1的操作,直到等于0的时候,就会执行我们指定的任务,然后放行所有子线程。后续我讲AQS后再剖析一下源码。
  • CountdownLatch不可用复用,CyclicBarrier可以重复使用

Semaphore

假如某个场景只能限定几个线程来执行,这个时候就可以使用Semaphore。 例如停车场的例子,停车场的车位就是这个场景的限定数,汽车相当于线程;当汽车要停车的时候,需要判断停车场的车位是否还有剩余的停车位,如果有的话则汽车可以进入停车,否则就需要等待。

public class SemaphoreTest {
    public static void main(String[] args) {
        // 停车场,有20个停车位
        Semaphore parkinglot = new Semaphore(20);
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    parkinglot.acquire();
                    System.out.println(Thread.currentThread().getName() + "进入停车场");
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                parkinglot.release();
                System.out.println(Thread.currentThread().getName() + "离开停车场");
            }, "汽车" + i).start();
        }
    }
}

volatile

最后讲一下volatile,但是在这之前需要0了解JMM

JMM

JMM,(Java Memory Model),即Java内存模型。因为Java是跨平台的语言。我们经常听到一次编译,到处运行;一次编译指的就是java文件通过JVM进行编译之后得到的class文件,可以在任意安装了JDK的系统运行。 在这里插入图片描述 这种跨平台也导致了一种问题:在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以为了统一这种内存读写的差异也就有了JMM。 JMM通过规范Java程序在读写内存时约定:

  • 每一个线程存在自己的工作内存,线程的工作内存保留了被线程使用的共享变量的拷贝副本。
  • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存来完成。 在这里插入图片描述

JMM三特性

  • 原子性; 指一个操作是不可分割、不可中断的,要么全部执行成功要么全部执行失败。常见例子就是数据库的事务,当开启事务后,后续的操作若出现异常,则之前的操作的数据都会回滚。 再比如Java中的i++,虽然看起只有一行语句,但实际不是原子操作,因为它操作的过程是:从内存中读取i,自增,再将新的值重新赋值给i
  • 可见性;线程操作共享变量时,其他线程可以感知到。
  • 不可重排序性;有序性是指程序执行的顺序按照代码的先后顺序执行。但是由于CPU会对代码进行优化,将代码进行重新排序,这就导致一段程序在多线程下执行可能出现不同的结果。

volatile

了解了JMM之后我们回到volatile。volatile可以保证变量的可见性和不可重排序性,但是无法保证原子性。所以,volatile也可以理解为 ‘ 简易版synchronized ’ 我们可以通过简单的例子

public class VolatileTest {

    public static void main(String[] args) {
        MyFlag myFlag = new MyFlag();
        new Thread(() -> {
            System.out.println("准备修改flag");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myFlag.setFlag(false);
            System.out.println("修改成功");
        }).start();

        while (myFlag.isFlag()) {
        }
        System.out.println("退出循环");
    }
}

class MyFlag {
    private boolean flag = true;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

我们在主线程中循环查看flag的变量,启动一个子线程,在3s后修改flag变量。执行的效果就是主线程一直在死循环中,没有因为子线程的修改而退出循环 volatile还有一个用法就是应用在单例模式的双重检测锁,就是为了避免多线程中出现的重排序情况。

public class Singleton{
    private volatile Singleton instance;
    private Singleton(){}
    public Singleton getInstance(){
        if(instance == null){
            synchronized(Singelton.class){
                if(instance == null){
                	instance = new Singleton();    
                }
            }
        }
        return instance;
    }
}

在多线程的情况下,会发生线程1为instance初始化,但是未将instance分配空间,而线程2刚好来到第一个if判断,接着线程2发现instance已经初始化,就会直接返回instance,最后导致出现Null的情况

线程的一些基本知识就先说到这里,还有其他的线程知识后续再细讲(我也得先研究研究) 今天是除夕,祝各位大佬除夕快乐