多线程基础概念总结

单线程下的程序,是一段代码一段代码按顺序执行,而一些代码在执行时往往用不到 CPU,这样CPU就得不到充分的利用,效率非常低下。所以 "并发编程"  这一概念就产生了,我们可以在没有用到 CPU 的时候去执行其他代码来提高总体代码的执行效率。比如在修改文件时,CPU 需要先等待读取文件,然后才能进行修改。在读取过程中 CPU 就处于空闲状态,这时我们可以让他去执行其他代码来减少总代码的执行时间。

基本概念

并发多个任务在同一个 CPU 核上,按细分的时间块交替执行

并行多个处理器或多核处理器同时处理多个任务,是真正意义上的同时执行。

串行多个任务按顺序执行,一个任务执行完后再开始执行下一个任务。

同步指的是能按预期的方式去进行,也就是能 "控制" 的执行。

并发编程多个线程以并发的方式执行

 

进程一段程序的执行过程。是操作系统进行资源分配的最小单位。

线程进程执行的单位,是处理器任务调度和执行的最小单位。

进程与线程的区别

1、每个进程都有独立的代码的数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和线程计数器,线程之间切换的开销小。

2、进程是包含线程的,一个进程包含若干个线程,一个进程内的线程共享这个进程的空间和资源。而进程之间的地址空间和资源是互相独立。

3、一个进程崩溃后,不会对其他进程造成影响;一个线程崩溃后,包含这个线程的进程都会收到影响。

4、每个独立的进程包含程序运行的入口、顺序执行序列和程序出口;线程不能独立执行,必须依附于进程才能执行。

 

并发编程的三要素原子性(不会在执行过程中受到其他线程的影响)、可见性(每次获取的都是主内存中最新的值)、有序性(禁止指令重排)。

指令重排:是单线程下为了提高代码执行效率而做出的优化,比如x=1; y=1; x++; y++; y=x+y; 这里JVM在加载时可能先加载到 y,那么它不会再去等待x加载,直接去执行 y++  ,这样就提高运算效率。但同时指令重排也遵守数据依赖性,比如虽然先加载了y,执行了 y++ ,也加载了 x,但是并不会接着去执行 y=x+y;因为右边操作的y 在前面的 y++修改了值,所以产生了对y++的数据依赖,JVM并不会允许这样的指令重排(其实这个例子里的x++,y++指令会划分为三步,这里只需要知道表达的意思就可以了)。但是在多线程下数据依赖性只能保证各自线程内的指令的数据依赖,不能保证多线程之间的数据依赖,所以多线程下应该禁止指令重排。

 

饥饿一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

活锁任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

上下文切换在悲观锁中,一段被加锁的代码同一时间只能被一个线程锁获取,其他线程在等待时会先保存当前线程的执行状态,然后进入阻塞等待,等到分配到锁资源时再去加载读取之前保存的执行状态,这种在运行时切换到等待时再切回运行时状态的动作就是一次上下文切换。

JMM

  在正式了解并发编程之前,要先理解 java 内存模型( java Memory Model),主要规定了在并发编程中变量存储的规则。主变量是存储在主内存中的,而每个线程在进行变量的计算时是先将变量拷贝一个副本到当前线程的本地内存中去的,然后在本地内存进行修改,然后再更新到主内存中去。这就是线程修改数据的过程。而读取操作也是从各个线程的工作内存上读取的。

 带来的问题是:1、线程读取的数据可能不是最新数据。

2、线程修改更新到主内存可能会覆盖掉其他线程的修改。比如:主内存是1,线程1读取进工作内存,然后线程2也读取进工作内存,线程1执行+1操作,线程2也执行+1操作,线程2更新到主内存,线程1也更新到主内存,此时主内存的值本应该是3,但是线程2的修改却被覆盖了。

 

多线程基础

线程状态

  线程主要有五种状态,新建、就绪、运行、阻塞、死亡。

当一个线程对象创建完成后就进入 "新建" 状态,随后调用 start() 方法后就会等待 CPU 调度分配,此时就是 "就绪" 状态,等到获得了 CPU 调度后,就开始执行线程代码,此时就是 "运行" 状态。如果在代码中调用了 join()、sleep()、wait()方法,当前线程就会阻塞,进入 "阻塞" 状态,join() 是让调用此方法的线程先执行完再继续执行当前线程,所以在调用此方法后当前线程就会进入 "阻塞" 状态,直到调用 join() 方法的线程执行完毕才会先进入 "就绪" 状态,然后才恢复到 "运行" 状态;sleep() 方法则是一个静态方法 ,同时也是一个本地方法,无论哪个线程对象调用 sleep() 方法,都会使当前线程进入 "阻塞" 状态,直到规定的时间到达后,才会先进入 "就绪" 状态,然后才会再进入 "运行" 状态;wait()方法同理,只不过它是直到调用 notify() 才会进入 "就绪" 状态,然后去等待 CPU 调度。当线程代码执行完毕,就会中断当前线程,释放 CPU 资源,然后死亡。

 

sleep(0)的作用:重置CPU的调度。 

 

线程创建

  创建线程主要有三种方式:1、继承 Thread  2、实现 Runnable 接口  3、实现 Callable 接口

1、继承 Thread

 1 public class StartThread extends Thread{
 2     @Override
 3     public void run() {
 4         for(int i=0;i<20;i++) {
 5             System.out.println("进程"+currentThread().getName());
 6         }
 7     }
 8 
 9     public static void main(String[] args) {
10         StartThread st=new StartThread();
11         st.start();                               
12         for(int i=0;i<20;i++) {
13             System.out.println("1111111");
14         }
15 
16     }
17 
18 }

  thread类是线程类,它的 run方法就对应着线程执行的内容,我们在创建继承 Thread类的类的对象时,只需要调用 start() 方法就可以唤醒这个线程,让他启动起来,需要注意的是,如果直接调用 run() 方法的话,是不能达成多线程的执行的,它相当于只是调用这个方法 "阻塞式" 的执行。这种方式也是比较常见的创建多线程的方式,但是因为 Java 中的类是单继承的,所以通过这种方式来创建线程就不能再去继承其他类了,所以这种方式并不是最佳方案。

多线程下不能同步:值得注意的是,主线程 main 和新建的线程 st 并不是按顺序执行的,多执行几次就会产生类似下面这种结果:

 

不同线程之间是交替执行的,这就是多线程 "并发"  的特点,这个交替的顺序是不能人为控制的。这就是线程的不能 "同步" 性。

 

2、实现 Runnable 接口

 1 public class StartRun implements Runnable{
 2     public void run() {
 3         for(int i=0;i<10;i++) {
 4             System.out.println(i + "_________________");
 5         }
 6     }
 7 
 8     public static void main(String[] args) {
 9         new Thread(new StartRun()).start();;
10         for(int i=0;i<10;i++) {
11             System.out.println(i);
12         }
13 
14     }
15 
16 }

   相比于继承 Thread 类,实现 Runnable 类更加灵活,因为一个类是可以实现多个接口的,所以我们在实现 Runnable 并不影响我们的继承和实现。这也是最常用的创建线程的方式。同时 Thread 类也是 Runnable 接口的实现类,实现了 Runnable 接口的 run 方法,这也能说明为什么这两种方式都需要去重写和实现 run 方法。

 

3、实现 Callable 接口

 1 public class FutureTaskTest implements Callable<String>{
 2     @Override
 3     public String call() throws Exception {
 4         Thread.sleep(2000);
 5         System.out.println("123");
 6         return 12+"";
 7     }
 8     public static void main(String[] args) throws InterruptedException, ExecutionException {
 9         FutureTask<String> future=new FutureTask<String>(new FutureTaskTest());
10         new Thread(future).start();
11          System.out.println("获取值");
12          System.out.println(future.isCancelled());
13          System.out.println(future.isDone());
14          System.out.println(future.get());        
15     }
16 }

 

 这种方式是比较特殊的一种,因为上面两种线程在执行时执行的是 run 方法,而 run 方法是没有返回值的,而使用这种方式来创建线程实现的是 call() 方法,并且 call() 是有返回值的,返回值类型是和实现接口指定的泛型一致。这就意味着我们可以调用某个方法去获取这个返回值,下面就详细说一下 Callable 接口相关的方法。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

 

上面是 Callable 接口的源码,可以看到里面只有一个 call() 方法,和 Runnable 接口如出一辙,我们知道创建一个线程对象就是创建一个 Thread 对象,但是在查看 Thread 类的源码后,会发现没有 Callable 类型的构造函数

 那么我们该如何创建 Callable 类型的线程呢?这就要说到 FutureTask 类来间接创建 Thread 对象了,还是先看一下 FutureTask 的源码 

public class FutureTask<V> implements RunnableFuture<V> {

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
 public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

}

 

 由于篇幅只截了构造器和类部分。首先可以看到,FutureTask 内部有 Callable 类型参数的构造器,所以可以通过它来创建 Callable 类型对象的对象,再看它的结构,FutureTask 实现了 RunnableFuture 接口,那么这个接口的结构是什么呢?

1 public interface RunnableFuture<V> extends Runnable, Future<V> {
2     /**
3      * Sets this Future to the result of its computation
4      * unless it has been cancelled.
5      */
6     void run();
7 }

 

这个接口继承了 Runnable 接口,所以我们可以将 FutureTask 对象作为参数来创建 Thread 对象。同时从开始的例子可以看到这个类也有一些特殊的方法。下面将一一讲解。

1、get()

这个方法是用来获取线程执行方法 call() 方法的返回值的,如果该线程代码还没有执行完,会一直等待,直到执行完得到返回值。

2、cancel(boolean   mayInterruptIfRunning)

取消 Callable 线程 call() 方法的执行,如果 call() 方法已经执行结束,或者已经被取消,或者不能被取消,这个方法就会执行失败并返回false;如果 call() 方法还没有开始执行,那么 call() 方法会被取消,不会再被执行;如果 call() 方法已经开始执行了,但是还没有执行结束,这时如果调用 get() 方法会抛出异常,至于过程会不会执行会根据 mayInterruptIfRunning 的值,如果 mayInterruptIfRunning = true,那么会中断 call() 方法的线程,然后返回true,如果参数为false,会返回true,不会中断 call() 方法的线程。需要注意,这个方法执行结束,返回结果之后,再调用isDone()会返回true

3、isCancelled()

返回 cancel 方法的结果,只要没有成功中断 call() 方法都是 true。

4、isDone()

是否执行结束,如果执行完毕返回 true,负责返回 false。

 

现在再来看上面的例子,首先会创建FutureTask 对象,再创建对应的线程对象并调用 start() 方法,线程开始进入 "就绪" 状态,随后开始与主线程 "并发" 执行,而 future 线程调用 Sleep() 方法,所以输出台还是先执行主线程的输出语句,等执行到 get() 方法就会阻塞当前线程,并且主线程前面的输出代码执行消耗时间比较短,所以约2秒后,future  sleep() 方法结束,继续执行,等到 future 线程执行完,那么 get() 方法得到返回值,输出返回值 "12" 。

 而如果在main函数中调用完 future 线程的 start() 方法后立刻调用 future.cancel(true),那么 future 线程的执行就会取消,同时执行到 get() 方法时会抛出异常。

 

 需要注意的是因为FutureTask调用 Callable 接口的线程会有返回值,所以在执行完一次这个线程后就会将这个过程的返回值缓存起来,下次执行直接返回这个返回值而不会去重复执行Callable接口线程的方法。

public static void main(String[] args) throws InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            System.out.println("执行了一次 call 方法");
            return 1;
        });
        new Thread(futureTask).start();
        new Thread(futureTask).start();
    }

执行结果:

 

 

跳出阻塞

  一般情况下当一个线程进入 "阻塞" 状态后就不能继续执行代码,必须达到必要的条件后才能继续执行代码,比如调用 Sleep() 方法后就需要等到规定的时间结束后才能退出 "阻塞" 状态,那么有没有别的方法提前退出 "阻塞" 状态呢?答案是有的。事实上,每个线程都有一个 "中断状态",当我们在调用 sleep() ,join(),wait() 方法时都需要处理 InterruptedException 异常,这个异常就是中断异常,当该线程中断状态为 true后进入 "阻塞" 状态后就会抛出 InterruptedException 异常,然后中断 "阻塞" 状态,跳出try catch 包围的代码继续执行,如果是方法上的抛出异常那么就跳到上一级方法。下面先介绍中断的几个方法。

1、interrupt()

调用此方法的线程会将中断状态设为 true,也就是打开中断状态(线程默认中断状态是 false)。

2、interrupted()

是一个静态方法,无论哪个线程对象调用此方法,它的实现都是返回当前线程的中断状态,然后清空中断状态。也就是如果当前线程中断状态是 true,再设为 false。

3、isInterrupted()

返回调用次方法的线程的中断状态。

 

下面来看一个例子。

 1 public class InterruptionInJava implements Runnable{
 2 
 3     public static void main(String[] args) throws InterruptedException {
 4 
 5         Thread testThread = new Thread(new InterruptionInJava(),"InterruptionInJava");
 6 
 7         testThread.start();
 8         Thread.sleep(3000);
 9 
10         System.out.println("中断前中断状态:" + testThread.isInterrupted());
11         testThread.interrupt();
12 
13         System.out.println("中断后中断状态:" + testThread.isInterrupted());
14 
15     }
16 
17     @Override
18     public void run() {
19         try {
20             System.out.println("testThread 开始执行");
21             Thread.sleep(10000000);
22         } catch (InterruptedException e) {
23             // TODO Auto-generated catch block
24             e.printStackTrace();
25         }
26         System.out.println("testThread 执行结束");
27     }
28 }

执行结果:

在 testThread 线程启动后,主线程调用 sleep() 进入短暂的 "阻塞" 状态,而 testThread 在输出通知语句后进入长时间的 "阻塞",等到主线程的 sleep() 时间到时,调用 testThread 的 interrupt() 方法,让 testThread 的中断状态打开,这时 testThread 就会跳出 "阻塞" 状态,然后继续执行 try catch 外面的代码,而主线程在调用 testThread 的 interrupt() 方法前后输出的中断状态不同。  

 

 

线程同步

  上面已经说过了,多线程的执行是 "并发" 的,所以当多个线程需要对同一个数据进行不同的操作时,往往会因为执行顺序的不同而造成不同的结果。这就是线程没有同步造成了数据的不安全性。而保证线程同步就成为了多线程编程最重要的一个环节。保证线程同步的方式可以是直接使用 synchronized、lock 锁锁住特定的对象,让同一时间内只有一个线程去执行;也可以使用一些同步容器,譬如 ConcurrentHashMap、Vector、ThreadLocal等来存储数据,保证数据线程安全;在计算时可以使用原子系列类来保证数据安全;如果不涉及数据的修改,只需要实时读取最新的数据,那么还可以使用 volatile 来修饰;还有一些是特殊的封装类,用于特定的场景,比如 Semaphore、CycliBarriar、CountdownLatch等。其中 synchronized 和 lock 已经另开文章说明了Lock、Synchronized锁解析 ,集合容器会另开一篇讲解。这里就说一下剩下的内容吧。

AQS

   AQS可以说是JUC的基石,许多同步器都是通过AQS实现的,它规定了多线程情况下各个线程对资源的争夺规则。

  AQS内部维护了一个int类型的属性State,这个属性用来表示当前资源的占用情况,如果是0表示资源是空闲的,如果>0表示资源被占用,当资源被一个线程获取并加锁后,state就会+1,当其他线程尝试获取资源时就会来查看这个state,如果>0,就放弃争夺,同时如果一个线程在已拥有锁的情况下再次对这个对象进行加锁操作(前提是可重入锁),那么state就会再次+1,后面每释放一次state会-1,直到变成0。

  除此之外,AQS内部还维护了一个FIFO队列,它是由一个双向链表组成的,当线程获取资源失败时,就会进入这个队列阻塞并等待CPU调度分配资源。

  AQS正是通过这个这两个机制才能保证资源的有序分配,保证同步。

   关于AQS更详细的介绍可以移步 AQS解析 。

  注意AQS的队列是线程的等待队列,当调用wait、await方法进入阻塞状态会进入另一个 "阻塞队列" ,也就是说调用wait方法并不能使阻塞队列中的线程重新获取资源,而是在FIFO队列中选择最前面(准确的来说是头结点后面一个有效线程,因为头结点线程是一个哨兵线程)的线程分配资源。

 

 

Volatile

  volatile 是一个保证线程安全的轻量级方式,因为直接加锁,比如使用 synchronized、lock 会进行加锁、解锁,线程上下文的切换,这样是比较耗时的,在一些对属性的简单操作上,加锁会显得 "大材小用",反而不利于程序的执行效率。volatile 则可以在避免加锁、解锁的操作同时,保证 "可见性"、"有序性",但不能保证"原子性"。

  可见性:在开篇的 JMM 说过,JMM这样的结构会导致两个问题,读问题和写问题。volatile 的作用就是屏蔽了JMM这一结构,让线程每次读取都是直接从主内存读取最新的值,保证数据的 "可见性" ,同时更新也是会直接更新主内存上的值,保证这个操作是实时更新到主内存。所以volatile 保证了并发编程的 "可见性" 。

  有序性由于单线程下的指令重排在多线程下会有线程安全问题,所以在多线程下应该禁止指令重排,而 volatile 可以禁止指令重排。保证指令执行的有序性。

  原子性volatile并不能保证修饰属性的原子性。比如下面这个例子。

public volatile static int a = 0;
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ;i < 100 ; i++){
            new Thread(()->{
                for (int m = 0;m < 10000; m++)
                    a++;
            }).start();
        }
        TimeUnit.SECONDS.sleep(8);  // 保证代码完成执行
        System.out.println(a);
    }

预期的打印结果是1000000,执行结果却会发现怎么也不会达到这个数值,这是因为 a++ 底层是分为了三步,使用idea 插件 jclasslib对 class查看a++这个代码的过程

 先是获取属性,再压栈常量1,再相加,最后再更新到属性,这个过程是分为四步,并不能保证原子性,所以在中间一步时其他线程可能就进行读取指令,这样就会导致更新被覆盖,从而使最终结构小于预期值。

综上所述,volatile适用于一些场景简单,特别修饰的变量不涉及修改,只涉及读取的场景。并且在多线程下使用的属性都应该使用 volatile 修饰来保证其值的可见性。

  

 

Atomic系列

  从上面的 volatile 关键字的解析可以知道类似于 i++ ,i-- , i = i + 9 这些算术指令在多线程下都是线程不安全的,那么如何保证这些计算的安全性呢?最简单的方式就是使用锁,直接将这个代码锁住就可以了,但是锁一般是在一段业务逻辑的代码必须保证原子性才使用的。因为其涉及到上下文切换,所以盲目的加锁会降低程序整体的性能。所以针对与这些简单的算术指令,JDK内部维护了 Atomic 系列的类来处理。

打开JDK 源码压缩包可以看到,在 java.util.concurrent.atomic包下,包含一系列Atomic开头的类,下面就以 AtomicInteger 为例,来看一下其中的一些常用方法。

1、addAndGet(int delta) : 先添加后获取,就是将存储的值与参数相加再返回添加后的数。

2、compareAndSet(int expect, int update):比较并替换,一个CAS方法,参数分别是期望值,更新值。

3、decrementAndGet():先自减再返回自减后的值,相当于线程安全的 --i。

4、get():获取存储的值。

5、getAndAdd(int delta):先获取再增加,返回的是增加前的值。

6、getAndIncrement():先获取再自增,相当于线程安全版的 i++。

其他方法类似,都是通过方法名就可以知道作用。接下来以上面 volatile 的例子为例,使用 AtomicInteger 改成线程安全。

 public static AtomicInteger atomicInteger = new AtomicInteger(0);   // 设置初始值

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ;i < 100 ; i++){
            new Thread(()->{
                for (int m = 0;m < 10000; m++)
                    atomicInteger.getAndIncrement();    // 线程安全的 i++
            }).start();
        }
        TimeUnit.SECONDS.sleep(8);  // 保证代码完成执行
        System.out.println(atomicInteger.get());
    }

结果:

 

AtomicReference

   所以我们在多线程下对于要修改的数据类型直接使用对应的数据类型的Atomic类来处理就可以了,那么如果是自定义类的对象存储呢?通过上面的包截图可以看到有一个类是 AtomicReference ,这个就是一个通用的 Atomic类,我们可以通过它对我们自定义的类对象进行操作。

public class AtomicTest {
    public static AtomicReference<User> atomicReference = new AtomicReference<>();

    public static void main(String[] args) {
        User user = new User("张三", 400);
        User user1 = new User("李四", 200);
        System.out.println(atomicReference.compareAndSet(null, user));  // CAS方式进行修改
        System.out.println(atomicReference.compareAndSet(null, user1));
        System.out.println(atomicReference.get());
    }
}
class User{
    private String name;
    private Integer money;

    public User(String name, Integer money) {
        this.name = name;
        this.money = money;
    }

    public User() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

  结果:

CAS修改的特点是直接在工作内存中进行修改,当要更新到主内存时判断期望值与实际值是否相等,如果相等就将该值改成要修改的值。但是这种方式有一个缺陷,那就它只会比较最终值与预期值是否相等,如果中间产生其他改变,那么就不能判断出来,这就是ABA问题。意思就是在开始时使用CAS修改A,那么期望值是A,但是中间主内存A变成了B,并且在更新到主内存之前又从B改回了A,此时CAS修改发现A并没有修改过,以为A还是那个值,但其实A已经是变动过了。所以 AtomicReference 适用于不注重过程变化的对象。而那么比较注重 ABA 问题的对象就使用 AtomicStampedReference。

 

 

AtomicStampedReference

   相比于 AtomicReference,它增加了版本号,在CAS更新时会增加版本号的比较。

public class AtomicTest {
    public static AtomicStampedReference<User> atomicReference = new AtomicStampedReference<>(null, 0);

    public static void main(String[] args) {
        User user = new User("张三", 400);
        User user1 = new User("李四", 200);
        System.out.println(atomicReference.compareAndSet(null, user, 1 ,1));  // CAS方式进行修改
        System.out.println(atomicReference.compareAndSet(null, user1, 0 ,1));
        System.out.println(atomicReference.getReference());
        System.out.println(atomicReference.getStamp());
    }
}
class User{
    private String name;
    private Integer money;

    public User(String name, Integer money) {
        this.name = name;
        this.money = money;
    }

    public User() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

 结果

 

关于Atomic包中的方法,大部分都调用了unsafe类中的 native 本地乐观锁方法。unsafe类是乐观锁实现的一个核心类,内部包含大量的乐观锁方法,这些乐观锁方法都是native类型的,因为乐观锁实现要用到操作系统层面的东西,所以使用了C、C++语言来实现效率更高,同时unsafe内部还有一些自旋锁配合乐观锁的方法,这些方法用于一些要求数据必须修改成功而不是只是尝试一下的场景。注意 compareAndSet 也就是CAS 方法只是调用底层的乐观锁方法,并没有使用到自旋锁。

 

 

 

 

其他类

1、CountDownLatch

  countDownLatch 可以看作是 "阻塞-通知" 模型的一种封装,执行 await 方法后会进入阻塞,在创建 countDownLatch 对象时指定释放方法的个数,当调用该个数的 countDown 方法后,调用 await 方法的线程才会被唤醒,继续执行。

public class CountDownLatchTest {
    public static void main(String[] args){
        CountDownLatch countDownLatch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                try { TimeUnit.SECONDS.sleep(2); }catch(Exception e) {e.printStackTrace();}
                System.out.println(Thread.currentThread().getName() + "调用countDown");
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + "结束");
            }).start();
        }
        System.out.println(Thread.currentThread().getName() + "开始等待");
        try { countDownLatch.await(); }catch(Exception e) {e.printStackTrace();}
        System.out.println(Thread.currentThread().getName() + "结束");
    }
}

结果:

 可以看到main线程在调用 await 方法后就进入阻塞,得到其他线程调用 countDown 方法次数达到规定次数后才恢复执行,并且其他线程并不会因为 countDown 方法而阻塞。值得注意的是这里先执行 countDown 也是计入次数的,比如说将上面的代码改成

public static void main(String[] args){
        CountDownLatch countDownLatch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
//                try { TimeUnit.SECONDS.sleep(2); }catch(Exception e) {e.printStackTrace();}
                System.out.println(Thread.currentThread().getName() + "调用countDown");
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + "结束");
            }).start();
        }
        try { TimeUnit.SECONDS.sleep(2); }catch(Exception e) {e.printStackTrace();}
        System.out.println(Thread.currentThread().getName() + "开始等待");
        try { countDownLatch.await(); }catch(Exception e) {e.printStackTrace();}
        System.out.println(Thread.currentThread().getName() + "结束");
    }

main线程也是会正常执行结束的,这是因为 countDownLatch 底层使用也是 AQS,在初始化 CountDownLatch 时 state被设置成3,后面每次的 countDown 方法会去减1,等到执行三次后就变成0了,而此时调用 await 方法因为 state 已经变成了0所以直接换取到锁资源执行。

 

2、CyclicBarrier

  cyclicBarrier 意为 "屏障",它的使用与 CountDownLatch 正好相反,它是调用 await 方法达到一定次数才会执行内部设置的方法,而线程在执行完 await 方法后线程也会进入阻塞状态。

public static void main(String[] args){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
            System.out.println("条件达成,总方法开始执行");
            try { TimeUnit.SECONDS.sleep(5); }catch(Exception e) {e.printStackTrace();}
            System.out.println("总方法执行完成");
        });
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "执行");
                try { cyclicBarrier.await(); }catch(Exception e) {e.printStackTrace();}
                System.out.println(Thread.currentThread().getName() + "执行完毕");
            }).start();
        }
    }

结果:可以看到其他线程在调用 await 方法后就会进入阻塞状态,随时直到 CyclicBarrier 执行完以后才会继续执行。

cyclicBarrier 底层并没有使用 AQS ,而是直接使用 lock 锁,也是间接使用了 AQS。

 

3、Semaphore

  Semaphore 意为 "信号" ,是控制并发量的一个类。

public static void main(String[] args){
        Semaphore semaphore = new Semaphore(2);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try { semaphore.acquire(); }catch(Exception e) {e.printStackTrace();}
                System.out.println(Thread.currentThread().getName() + "获取到资源------" + (System.currentTimeMillis()-startTime)/1000);
                try { TimeUnit.SECONDS.sleep(2); }catch(Exception e) {e.printStackTrace();}
                semaphore.release();
                System.out.println(Thread.currentThread().getName() + "释放资源" + (System.currentTimeMillis()-startTime)/1000);
            }).start();
        }
    }

结果: 可以看到五个线程同时启动,但是并发量却控制在规定的两个以内。

 

posted on 2020-11-05 20:17  萌新J  阅读(240)  评论(0编辑  收藏  举报