并发编程知识

1、线程基础、线程之间的共享和协作

1.1、基础概念

1.1.1、cpu核心数、线程数

​ cpu的核心数与线程数是1:1的关系,例如一个8核的cpu支持8个线程同时运行。但在intel引入超线程技术以后,关系就是1:2。在开发过程中并没有感觉到线程的限制,那是因为cpu时间片轮转机制(RR调度)的算法的作用。

1.1.2、cpu时间片轮转机制

​ cpu给每个进程分配一个“时间段”,这个时间段就叫做这个进程的“时间片”,这个时间片就是这个进程允许运行的时间,如果当这个进程的时间片段结束,操作系统就会把分配给这个进程的cpu剥夺,分配给另外一个进程。如果进程在时间片还没结束的情况下阻塞了,或者说进程跑完了,cpu就会进行切换。cpu在两个进程之间的切换称为“上下文切换”,上下文切换是需要时间的,大约需要花费5000~20000(5毫秒到20毫秒,这个花费的时间是由操作系统决定)个时钟周期,尽管我们平时感觉不到。所以在开发过程中要注意上下文切换(两个进程之间的切换)对我们程序性能的影响。

1.1.3、什么是进行和线程

​ 进程:它是属于程序调度/运行的资源分配的最小单位,一个进程的内部可能有多个线程,多个线程之间会共享这个进程的资源。进程与进程之间是相互独立的 线程:它是cpu调度的最小单位,线程本身是不能独立进行的,它必须依附某个进程,线程本身是不拥有系统资源的。

1.1.4、什么是并行和并发

​ 并行是同一时刻可以处理多少件事,并发是在单位时间内可以处理多少件事情。

1.1.5、高并发编程的意义、好处和注意事项

​ 通过以上1.1~1.4的了解,我们可以知道高并发编程可以充分利用cpu的资源,例如一个8核的cpu跑一个单线的程序,那么意味着在任意时刻,我有7个CPU核心是浪费掉的。另外可以充分地加快用户的响应时间。同时使用并发编程可以使我们的代码模块化、异步化。

​ 注意事项/难点: 线程之间会共享进程的资源,既然说是共享资源,就有可能存在冲突。在高并发编程中如果控制不好,还有可能会造成线程的死锁。每启动一个线程,操作系统就需要为这个线程分配一定的资源,线程数太多还可能会把内存消耗完毕,会导致系统死机。

1.2、启动和终止线程

1.2.1、启动的方法

​ a、类Thread

public class ThreadStartDemo extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread方式启动线程");
    }
    public static void main(String[] args) {
        new ThreadStartDemo().start();
    }
}

 

​ b、接口Runnable(没有返回值)

public class ThreadStartDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable接口方式启动线程");
    }
    public static void main(String[] args) {
        //要启动实现Runnablede的线程的话还需要把runnable的实例传到Thread里
        new Thread(new  ThreadStartDemo()).start();
    }
}

 

​ c、接口Callable(允许有返回值)

public class ThreadStartDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "实现Callable接口方式启动线程";
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadStartDemo callableThread = new ThreadStartDemo();
        // 由于new Thread只接受Runnable类型的构造参数,所以要先把Callable包装一下
        FutureTask<String> futureTask = new FutureTask<>(callableThread);
        new Thread(futureTask).start();
        // 获取返回值,get方法是阻塞的
        System.out.println(futureTask.get());
    }
}

1.2.2、结束的方法

​ a、 方法执行完自动终止

​ b、抛出异常,又没有捕获异常

​ c、早期还提出过三个方法终止线程stop(), resume(), suspend() 。这三个方法都不建议用于终止线程

​ 原因: 一旦调用stop会强行终止线程,无法保证线程的资源正常释放。

​ suspend()调用后线程是不会释放资源的,很容易引起死锁。 不推荐使用 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行 resume() 方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。但是,如果 resume() 操作出现在 suspend() 之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。而且,对于被挂起的线程,它的线程状态居然还是 Runnable。

​ 正确的终止线程的方法:interrupt(),isinterrupted()以及静态方法interrupted().

三者的区别:

​ d、 interrupt():是属于Thread类的方法,作用终止一个线程,但并不是强行关闭一个线程(java的线程是协作式的,不是强迫式的,调用一个线程的interrupt()方法并不会强制关闭一个线程,它就好比其他线程对要关闭的线程打了一声招呼,告诉被关闭线程它要中断了,但被关闭线程什么时候关闭完全由它自身做主),线程调用该方法并不会立刻终止。既然调用interrupt()方法起不到终止线程的目的,那么它为什么要这样设计?这样设计时为了让每个线程有充分的时间去做线程的清理工作。进行开发的时候要对线程的中断请求进行处理,否则就不是一个设计良好的并发程序。总的来说,它的目的只是把线程中的“中断标志位”置为true

​ e、isInterrupted(),判定当前线程是否处于中断状态。通过这个方法判断中断标志位是否为true。

​ f、static方法isInterrupted(), 也是判断当前线程是否处于中断状态。当调用此方法时,它会把中断标志位改为false。

1.3、线程再认识

​ 1、当线程调用了wait(),join(),sleep()方法时,方法会抛出InterruptedException,这个时候线程的中断标志会被复位成为false,所以这个时候我们应该在catch里面再调用一次interrupt(),再次中断一次。

public class ThreadStartDemo extends Thread {
    public ThreadStartDemo(String name) {
        super(name);
    }
    @Override
    public void run() {
         //如果现在这个while的条件不是“!isInterrupted()”而是“true”,
        //那么即使main方法里调用了test.interrupt()还是无法终止线程的,这就是java协作式。
        //通过实现Runnable接口创建的线程 -> !Thread.currentThread().isInterrupted()
        while(!isInterrupted()) {
            System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
            try {
                Thread.sleep(1000);
            } catch (Throwable e) {
                e.printStackTrace();
                //当线程调用了wait(),join(),sleep()方法时,方法会抛出InterruptedException,
                //这个时候线程的中断标志会被复位成为false,所以这个时候我们应该在catch里面再调用一次interrupt(),再次中断一次。
                interrupt();
            }
        }
    }
    public static void main(String[] args) throws Throwable{
        ThreadStartDemo test = new ThreadStartDemo("MyThread");
        test.start();
        //让main线程等待
        Thread.sleep(5000);
        test.interrupt();
    }
}

​ 2、开发过程中也可为线程设置优先级,线程的优先级的范围为1~10,缺省值为5,优先级较高的线程获得分配的时间片就较高。调用Thread.setPriority()方法进行设置。这个优先级的设置在不同的操作系统中会有不一样的结果,有些系统设置会忽略这个优先级的设定,有的操作系统可能全部给你设置为5,所以在程序开发过程中, 不能指望这个操作。

​ 3、守护线程:守护线程的线程和主线程是同时销毁的,主线程退出了,守护进程就一定会结束。设置守护进程是通过Thread.setDaemon(true)进行设置的,而且需要在调用start()方法之前设置。使用守护线程需要注意:守护进程里非try..finally是不能保证一定执行finally的。

​ 4、 volatile是最轻量级的保证同步的方法,但它一般只使用于一个线程写,多个线程读这种场景。

​ 5、run()和start()的区别:调用run方法其实就像普通的类调用类中的方法一样,run()方法由谁调用就归宿与哪个线程。只有调用start()方法才是真正的启动线程。

​ 6、就绪状态也成为可运行状态,调用了一个线程的start()方法,形成就处于可运行状态,但这个时候并不是运行状态,只有当cpu调度到该线程才是运行状态。

​ 7、yield()方法的作用是,当一个线程运行完了,就把cpu的时间让出来。那么它与sleep()方法的区别呢?调用sleep()方法,在sleep()的时间段内,cpu是不会再调度该线程,但是调用了yield()方法的下一时刻,cpu还是有可能会调度到该线程的 。

1.4、线程间的共享

1.4.1、synchronized

​ synchronized(内置锁),要么加载方法上面,要么是用作同步块的形式来使用,最大的作用是确保在多个线程在同一时刻只能有一个线程处于方法或同步块之中,这样它就保证了线程对变量访问的可见性与排差性。 锁的是对象,不是代码块,每个对象在内存的对象头上有一个标志位,标志位上有1~2个字节标志它为一个锁,synchronized的作用就是当所有的线程去抢这个对象的标志位,谁把这个标志位指向了自己,那就认为这个线程抢到了这个锁。

​ 对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

1.4.2、volatile

​ volatile关键字是与Java的内存模型有关的,因此在理解volatile关键词之前,我们需要先了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。

​ 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,会强制将修改的值立即写入主存。(当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。 )每次要用该变量时,总是要在主内存中读取(非工作内存)。volatile并不是线程安全的,只能保证变量的可见性,不能保证原子性。那么就具备了两层语义:

​ 1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

​ 2、禁止进行指令重排序。 所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可见性

处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
​
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
​
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

有序性

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

 

volatile的原理和实现机制

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
​
  lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:
​
  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
​
  2)它会强制将对缓存的修改操作立即写入主存;
​
  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

应用场景:

1)对变量的写操作不依赖于当前值
​
2)该变量没有包含在具有其他变量的不变式中
​
下面列举几个Java中使用volatile的几个场景。
​
①.状态标记量
    
volatile boolean flag = false;
 //线程1
while(!flag){
    doSomething();
}
  //线程2
public void setFlag() {
    flag = true;
}
​
根据状态标记,终止线程。
​
②.单例模式中的double check
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
为什么要使用volatile 修饰instance?
​
主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
​
1.给 instance 分配内存
​
2.调用 Singleton 的构造函数来初始化成员变量
​
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。
​
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

 

备注:读取变量的原始值、进行操作、写入工作内存 。

1.4.3、ThreadLocal

​ ThreadLoacl(线程变量):可以确保每个线程只使用自己那一部分的东西。例如一个变量使用ThreadLocal包装的话,那么每个线程都是使用自己的那一份变量的拷贝。可以理解为Map

​ ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

1.5、线程间协作

1.5.1、等待和通知的标准格式

​ 轮询无法保证及时性,资源的开销也比较大,大部分时间都在做无用功。为了解决这种情况,java里提供了一种等待和通知机制,当线程调用wait方法会进入等待状态,当调用notify或notifyAll(首选notifyAll,因为notify通知的是等待队列中的一个线程,有可能发生信号丢失的情况。)方法就会唤醒线程。wait、notify、notifyAll这三个方法是对象本身的方法,并不是线程的方法。

​ 在线程之间进行通信,往往有一个”等待和通知的标准范式”,如下: 调用wait的线程(等待方): ① 获取对象的锁 ② 在一个循环里判定条件是否满足,不满足就调用wait方法 ③ 条件满足就执行业务逻辑 通知方: ① 获取对象的锁 ② 改变条件

​ ③ 通知所有等待在对象的线

1.5.2、join方法

​ thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

t.join();      //调用join方法,等待线程t执行完毕

t.join(1000);  //等待 t 线程,等待时间是1000毫秒。

public class ThreadStartDemo{
​
    public static void main(String[] args) throws Throwable {
         System.out.println("MainThread run start.");
        //启动一个子线程
         Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("threadA run start.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("threadA run finished.");
            }
        });
         threadA.start();
         System.out.println("MainThread join before");
         try {
             threadA.join();    //调用join()
        } catch (Exception e) {
             e.printStackTrace();
        }
         System.out.println("MainThread run finished.");
    }
​
}

运行结果如下:

MainThread run start.
MainThread join before
threadA run start.
threadA run finished.
MainThread run finished.
  
对子线程threadA使用了join()方法之后,我们发现主线程会等待子线程执行完成之后才往后执行。
当子线程threadA执行完毕的时候,jvm会自动唤醒阻塞在threadA对象上的线程,在我们的例子中也就是主线程。至此,threadA线程对象被notifyall了,那么主线程也就能继续跑下去了。
可以看出,join()方法实现是通过wait()。当main线程调用threadA.join()时候,main线程会获得线程对象threadA的锁(wait)意味着拿到该对象的锁,调用该对象的wait(等待时间),直到该对象唤醒main线程(也就是子线程threadA执行完毕退出的时候)
总结
首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了threadA.join()方法,相当于在threadA.join()代码这块写了一个同步代码块,谁去执行了这段代码呢,是主线程,所以主线程被wait()了。然后在子线程threadA执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,会继续执行。

1.5.3、调用yield、sleep、wait、notify等方法对锁有何影响?

​ 1、调用yield()方法和sleep()方法以后,持有的锁是不释放的,所以一般调用这两个方法的良好写法是不要写在synchronized代码块外面。

​ 2、调用wait()方法和notify()方法是会释放锁的,调用这两个方法的前提是必须持有锁,而且调用这两个方法之后,还是会把这两个方法所在的synchronized代码块中的代码执行完成才释放锁(调用这两个方法是不会释放锁的),所以良好的写法是写在synchronized代码块中的最后一行。

​ 3、wait()、notify()、notifyAll()方法是和对象绑定一起的,话句话来说,就是你在object1上调用了notifyAll方法,那么通知的就是在object1上等待的线程,并不能通知到object2对象上的线程。

2、线程的并发工具类

2.1、Fork/Join

什么是分而治之?
规模为N的问题,N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解
动态规范
工作密取:workStealing
Fork/Join使用的标准范式
RecursiveTask有返回值RecursiveAction没有返回值   

2.2、CountDownLatch作用、应用场景和实战

作用:是一组线程等待其他的线程完成工作以后在执行,加强版join

await用来等待,countDown负责计数器的减一

Latch 为门闩的意思。如果翻译成倒计数门闩,表示:把门锁起来,不让里面的线程跑出来。因此这个类用来控制线程等待,可以让某个线程等待直到倒计时结束,再开始执行。

发令枪

例子1:

public class CountDownLatchDemo {
​
    private int count  = 0;
    private final static CountDownLatch countDownLatch = new CountDownLatch(5);
    
    private void add() {
        System.out.println("线程:" + Thread.currentThread().getName());
        for(int i = 0; i < 10000;i++) {
            count ++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        CountDownLatchDemo test = new CountDownLatchDemo();
        for(int i  = 0 ; i < 5; i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                        countDownLatch.countDown();
                        test.add();
                }
            }).start();
        }
        countDownLatch.await();
        Thread.sleep(1000);
        System.out.println("----------" + test.count);
    }
}
​
运行结果:
线程:Thread-0
线程:Thread-3
线程:Thread-4
线程:Thread-1
线程:Thread-2
----------41041

 

2.3、CyclicBarrier作用、应用场景和实战

​ 字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。我们暂且把这个状态就叫做 barrier,当调用 await() 方法之后,线程就处于 barrier 了。

​ 让一组线程达到某个屏障,被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行CyclicBarrier(int parties)。

CyclicBarrier(int parties, Runnable barrierAction),屏障开放,barrierAction定义的任务会执行

CountDownLatch和CyclicBarrier辨析

1、countdownlatch放行由第三者控制,CyclicBarrier放行由一组线程本身控制 2、countdownlatch放行条件》=线程数,CyclicBarrier放行条件=线程数

例子1: 用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任务;

public class CyclicBarrierDemo {
​
    public static void main(String[] args) {
        int num = 4;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
        for (int i = 0; i < num; i++) {
            new Writer(cyclicBarrier).start();
        }
    }
}
​
class Writer extends Thread {
​
    private CyclicBarrier cyclicBarrier;
​
    public Writer(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }
​
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + "正在写入数据...");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 写入数据完毕,等待其他线程写入...");
            cyclicBarrier.await();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 线程写入完毕,继续执行其他任务");
    }
}
运行结果:
Thread-0正在写入数据...
Thread-3正在写入数据...
Thread-2正在写入数据...
Thread-1正在写入数据...
Thread-1 写入数据完毕,等待其他线程写入...
Thread-0 写入数据完毕,等待其他线程写入...
Thread-3 写入数据完毕,等待其他线程写入...
Thread-2 写入数据完毕,等待其他线程写入...
Thread-3 线程写入完毕,继续执行其他任务
Thread-1 线程写入完毕,继续执行其他任务
Thread-0 线程写入完毕,继续执行其他任务
Thread-2 线程写入完毕,继续执行其他任务

例子2:让这些线程等待至一定的时间,如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务。

public class CyclicBarrierDemoWithAwaitTime {
​
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
 
        for(int i=0;i<N;i++) {
            if(i<N-1)
                new Writer(barrier).start();
            else {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                new Writer(barrier).start();
            }
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(3000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                try {
                    cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务...");
        }
    }
}
​
运行结果:
线程Thread-0正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-3正在写入数据...
java.util.concurrent.TimeoutException
Thread-1所有线程写入完毕,继续处理其他任务...
Thread-0所有线程写入完毕,继续处理其他任务...
Thread-2所有线程写入完毕,继续处理其他任务...
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
线程Thread-3写入数据完毕,等待其他线程写入完毕
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)Thread-3所有线程写入完毕,继续处理其他任务...
​
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
​
上面的代码在main方法的for循环中,故意让最后一个线程启动延迟,因为在前面三个线程都达到barrier之后,等待了指定的时间发现第四个线程还没有达到barrier,就抛出异常并继续执行后面的任务。

例子3:想在所有线程写入操作完之后,进行额外的其他操作 .

public class CyclicBarrierDemoWithExtraThing {
​
    public static void main(String[] args) {
        int num = 4;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num, new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程:" + Thread.currentThread().getName() + ",所有线程执行完成,进行额外的操作");
            }
        });
        for (int i = 0; i < num; i++) {
            new Writer2(cyclicBarrier).start();
        }
    }
​
}
​
class Writer2 extends Thread {
    private CyclicBarrier cyclicBarrier;
​
    public Writer2(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在写入数据...");
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入...");
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "线程写入完毕,继续执行其他任务");
    }
}
​
运行结果:
Thread-0正在写入数据...
Thread-2正在写入数据...
Thread-3正在写入数据...
Thread-1正在写入数据...
Thread-0写入数据完毕,等待其他线程写入...
Thread-2写入数据完毕,等待其他线程写入...
Thread-1写入数据完毕,等待其他线程写入...
Thread-3写入数据完毕,等待其他线程写入...
当前线程:Thread-3,所有线程执行完成,进行额外的操作
Thread-3线程写入完毕,继续执行其他任务
Thread-0线程写入完毕,继续执行其他任务
Thread-1线程写入完毕,继续执行其他任务
Thread-2线程写入完毕,继续执行其他任务

例子4:检测 CyclicBarrier 是否重用

public class CyclicBarrierDemoReusing {
​
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier = new CyclicBarrier(N);
​
        for (int i = 0; i < N; i++) {
            new Writer4(barrier).start();
        }
​
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
        System.out.println("CyclicBarrier重用");
        for (int i = 0; i < N; i++) {
            new Writer4(barrier).start();
        }
    }
​
}
​
class Writer4 extends Thread {
    private CyclicBarrier cyclicBarrier;
​
    public Writer4(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入...");
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "  线程写入完毕,继续执行其他任务");
    }
}
​
运行结果:
Thread-0 正在写入数据...
Thread-1 正在写入数据...
Thread-3 正在写入数据...
Thread-2 正在写入数据...
Thread-0写入数据完毕,等待其他线程写入...
Thread-3写入数据完毕,等待其他线程写入...
Thread-2写入数据完毕,等待其他线程写入...
Thread-1写入数据完毕,等待其他线程写入...
Thread-1  线程写入完毕,继续执行其他任务
Thread-0  线程写入完毕,继续执行其他任务
Thread-3  线程写入完毕,继续执行其他任务
Thread-2  线程写入完毕,继续执行其他任务
CyclicBarrier重用
Thread-4 正在写入数据...
Thread-6 正在写入数据...
Thread-5 正在写入数据...
Thread-7 正在写入数据...
Thread-7写入数据完毕,等待其他线程写入...
Thread-5写入数据完毕,等待其他线程写入...
Thread-4写入数据完毕,等待其他线程写入...
Thread-6写入数据完毕,等待其他线程写入...
Thread-6  线程写入完毕,继续执行其他任务
Thread-5  线程写入完毕,继续执行其他任务
Thread-7  线程写入完毕,继续执行其他任务
Thread-4  线程写入完毕,继续执行其他任务

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行; 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行; 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

 

2.4、Semaphore作用、应用场景和实战

控制同时访问某个特定资源的线程数量,用在流量控制 .

Semaphore 翻译成字面意思为 信号量,广义上说,信号量是对锁的扩展,无论是内部锁 synchronized 还是重入锁 ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问一个资源,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

/*
*假若一个工厂有 5 台机器,但是有 8 个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过 Semaphore 来实现:
*/
public class SemaphoreDemo {
    public static void main(String[] args) {
        int N = 8; //8 个工人
        Semaphore semaphore = new Semaphore(5);
        for (int i = 0; i < N; i++)
            new Worker(i, semaphore).start();
        ;
    }
​
    static class Worker extends Thread {
        private int num;
        private Semaphore semaphore;
​
        public Worker(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }
        @Override
        public void run() {
            try {
                semaphore.acquire();// 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
                 System.out.println("工人 "+this.num+" 占用一个机器在生产...");
                Thread.sleep(2000);
                 System.out.println("工人 "+this.num+" 释放出机器");
                semaphore.release();//用来释放许可。注意,在释放许可之前,必须先获获得许可。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​
运行结果:
工人 0 占用一个机器在生产...
工人 1 占用一个机器在生产...
工人 3 占用一个机器在生产...
工人 2 占用一个机器在生产...
工人 4 占用一个机器在生产...
工人 0 释放出机器
工人 4 释放出机器
工人 3 释放出机器
工人 1 释放出机器
工人 2 释放出机器
工人 5 占用一个机器在生产...
工人 6 占用一个机器在生产...
工人 7 占用一个机器在生产...
工人 7 释放出机器
工人 5 释放出机器
工人 6 释放出机器

 

2.5、Exchange作用、应用场景和实战

两个线程间的数据交换

2.6、Callable、Future和FutureTask

​ 2.6.1、Callable与Runnable

先说一下java.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();//由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。
}

Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():

@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;
}
可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。
那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
​
第一个submit方法里面的参数类型就是Callable。
​
暂时只需要知道Callable一般是和ExecutorService配合来使用的,具体的使用方法讲在后面讲述。
​
一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。

​ 2.6.2、Future

 Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

  Future类位于java.util.concurrent包下,它是一个接口:

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;
}
  在Future接口中声明了5个方法,下面依次解释每个方法的作用:
​
    cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
    isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
    isDone方法表示任务是否已经完成,若任务完成,则返回true;
    get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
    get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
​
  也就是说Future提供了三种功能:
​
  1)判断任务是否完成;
​
  2)能够中断任务;
​
  3)能够获取任务执行结果。
​
  因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

​ 2.6.3、FutureTask

我们先来看一下FutureTask的实现:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
 可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

 事实上,FutureTask是Future接口的一个唯一实现类。

​ 2.6.4、实例:

​ 例子1:使用Callable+Future获取执行结果

public class CallableFutureDemo {
​
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<Integer> result = executor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("子线程在进行计算");
                Thread.sleep(3000);
                int sum = 0;
                for (int i = 0; i < 100; i++)
                    sum += i;
                return sum;
            }
​
        });
        executor.shutdown();
​
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
​
        System.out.println("\"主线程在执行任务");
​
        try {
            System.out.println("task运行结果" + result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
​
        System.out.println("所有任务执行完毕");
    }
}
​

​ 例子2: Callable和FutureTask

public class CallableFutureTaskDemo {
​
    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
​
            @Override
            public Integer call() throws Exception {
                System.out.println("子线程在进行计算");
                Thread.sleep(3000);
                int sum = 0;
                for (int i = 0; i < 100; i++)
                    sum += i;
                return sum;
            }
        });
        //单线程
//      new Thread(futureTask).start();
        //线程池
        executor.submit(futureTask);
        executor.shutdown();
​
         //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/
​
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
​
        System.out.println("主线程在执行任务");
​
        try {
            System.out.println("task运行结果" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
​
        System.out.println("所有任务执行完毕");
    }
}

​ 例子3:实现自己的FutureTask

public class WlFutureTask<V> implements Runnable, Future<V> {
​
    private Callable<V> callable;// 封装业务逻辑
​
    V result;
    
    public WlFutureTask(Callable<V> callable) {
        this.callable = callable;
    }
​
    @Override
    public void run() {
        //执行业务逻辑
        try {
            result = callable.call();// http接口,返回值
            System.out.println("----------");
            synchronized (this) {
                this.notifyAll();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
​
    @Override
    public V get() throws InterruptedException, ExecutionException {
        if(result != null) {
            return result;
        }
        synchronized (this) {
            this.wait();//阻塞
        }
        return result;
    }
​
    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        return false;
    }
​
    @Override
    public boolean isCancelled() {
        return false;
    }
​
    @Override
    public boolean isDone() {
        return false;
    }
​
    @Override
    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return null;
    }
​
}
​
测试:
​
public class WlFutureTaskTest {
​
    
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Thread.sleep(3000);
                return 1;
            }
        };
        
        WlFutureTask<Integer> futureTask = new WlFutureTask<Integer>(callable);
        
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
​

 

3、原子操作CAS

3.1、什么是原子操作

​ 不可被中断的一个或者一系列操作

3.2、实现原子操作的方式

​ Java可以通过锁和循环CAS的方式实现原子操作

3.3、CAS( Compare And Swap ) 为什么要有CAS?

​ Compare And Swap就是比较并且交换的一个原子操作,由Cpu在指令级别上进行保证。

为什么要有CAS:因为通过锁实现原子操作时,其他线程必须等待已经获得锁的线程运行完以后才能获得资源,这样就会占用系统的大量资源

3.4、CAS包含哪些参数?

​ CAS包含三个参数:1、变量所在内存地址V;2、变量对应的值A;3、我们将要修改的值B。如果说V上的变量的值是A的话,就用B重新赋值,如果不是A,那就什么事也不做,操作的返回结果原值是多少。

循环CAS:在一个(死)循环【for(;;)】里不断进行CAS操作,直到成功为止(自旋操作即死循环)。

3.5、CAS实现原子操作的三大问题

​ 1、 ABA问题:其他的线程把值改成了B,很快改成了A,原子操作的线程发现值是A就修改,这样会有问题。解决ABA,引入版本号:1A-》2C-》3A

​ 2、 循环时间很长的话,cpu的负荷比较大

​ 3、 对一个变量进行操作可以,同时操作多个共享变量有点麻烦

3.6、CAS线程安全(面试点)

​ 通过硬件层面的阻塞实现原子操作的安全

3.7、原子更新基本类型类

​ AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference。

3.8、原子更新数组类

​ AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

3.9、原子更新引用类型提供的类

​ ·AtomicReference: 可以解决更新多个变量的问题

​ ·AtomicStampedReference:解决ABA问题 使用数字作为版本 关心得是有几个人改过

​ ·AtomicMarkableReference:解决ABA问题 使用Boolean作为版本,关心的是有没有修改过

3.10、原子更新字段类

​ Atomic包提供了以下3个类进行原子字段更新。

​ ·AtomicReferenceFieldUpdater:

​ ·AtomicIntegerFieldUpdater:

​ ·AtomicLongFieldUpdater:

​ 违反了面向对象的原则,一般不使用

4、显示锁和AQS

4.1、显示锁

4.1.1、Lock接口、核心方法和使用以及和synchronized的比较

lock显示锁是基于jdk层面的实现是接口,通过这个接口可以实现同步访问。

不同于synchronized关键字是java内置特性,基于jvm实现的,阻塞式获取锁。

Lock接口核心方法:

public interface Lock {
    void lock(); //逻辑划分-获取锁(如果获取不到锁,那么将会进入阻塞状态,与synchronized关键字一样)
    void lockInterruptibly() throws InterruptedException;//逻辑划分-获取锁(可中断的锁获取操作在尝试获取锁的过程中,如果不能够获取到,如果被中断,那么它将能够感知到这个中断,而不是一直阻塞下去.)
    boolean tryLock();//逻辑划分-尝试获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//逻辑划分-尝试获取锁
    void unlock();//逻辑划分-释放锁
    Condition newCondition();//逻辑划分-条件变量
}

小结lockInterruptibly:

如果锁不可用(被其他线程持有),除非发生以下事件,否则将会等待
    该线程成功获得锁
    发生中断
如果当前线程遇到下面的事件,则将抛出 InterruptedException,并清除当前线程的已中断状态。
    在进入此方法时已经设置了该线程的中断状态
    在获取锁时被中断

unlock方法介绍:

unlock并没有什么特殊的,他替代了synchronized关键字隐式的解锁操作
通常需要在finally中确保unlock操作会被执行,之前提到过,对于synchronized关键字解锁是隐式的,也是必然的,即使出现错误,JVM也会保障能够正确的解锁
但是对于Lock接口提供的unlock操作,则必须自己确保能够正确的解锁  

tryLock方法介绍:

相对于synchronized,Lock接口另一大改进就是try lock
顾名思义,尝试获取锁,既然是尝试,那显然并不会势在必得
tryLock方法就是一次尝试,如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false
也就是说方法会立即返回,如果获取到锁返回true,否则返回false,不管如何都是立马返回.
tryLock只是一次尝试,如果你需要不断地进行尝试,那么可以使用while替代if的条件判断
尽管tryLock只是一次的测试,但是可以借助于循环(有限或者无限)进行多次测试  

tryLock(long time, TimeUnit unit) 方法介绍:

在指定的超时时间内,如果能够获取到锁,那么将会返回true;
如果超过了指定的时间,但是却不能获取到锁,那么将会返回false;
另外很显然,这个方法是可中断的,也就是说如果尝试过程中,出现了中断,那么他将会抛出InterruptedException
所以,对于这个方法,他会一直尝试获取锁(也可以认为是一定时长内的“阻塞”,当然可以被中断),除非:
​
    该线程成功获得锁
    超过了超时时长
    该线程被中断
​
可以认为是lockInterruptibly的限时版本
如果没有发生中断,也认为他就是“定时版本的lock()”
不管怎么理解,只需要记住:他会在一定时长内尝试进行锁的获取,也支持中断

Condition介绍:

在隐式锁的逻辑中,借助于Java底层机制,每个对象都有一个相关联的锁与监视器
对于synchronized的隐式锁逻辑就是借助于锁与监视器,从而进行线程的同步与通信协作
在显式锁中,Lock接口提供了synchronized的语意,对于监视器的概念,则借助于Condition,但是很显然,Condition也是与锁关联的
Lock接口提供了方法Condition newCondition();
Condition也是一个接口,他定义了相关的监视器方法
在显式锁中,可以定义多个Condition,也就是一个锁,可以对应多个监视器,可以更加细粒度的进行同步协作的处理

锁小结:

对于lock方法和unlock方法,就是类似于synchronized关键字的加锁和解锁,并没有什么特别的
其他几个方法是Lock接口针对于锁获取的阻塞以及可中断两个方面进行了拓展
隐式锁的阻塞以及不可中断,导致一旦开始尝试获取,那么则没办法唤醒,将会一直等待,除非获得
​
    lockInterruptibly()是阻塞式的,如果获取不到会一直等待,但是他是可中断的,能够通过阻塞打破这种等待
    tryLock()不会进行任何阻塞,只是尝试获取一下,能获取到就获取,获取不到就false,拉倒
    tryLock(long time, TimeUnit unit),即是可中断的,又是限时阻塞的,即使不中断,也不会一直阻塞,即使处于阻塞中(超时时长还没到),也可以随时中断
​
对于lockInterruptibly()方法以及tryLock(long time, TimeUnit unit),都支持中断,但是需要注意:
在某些实现中可能无法中断锁获取,即使可能,该操作的开销也很大  
​

总结:

Lock接口提供了相对于synchronized关键字,而更为灵活的一种同步手段
它的核心与本质仍旧是为了线程的同步与协作通信
所以它的核心仍旧是锁与监视器,也就是Lock接口与Condition接口
但是灵活是有代价的,所以并不需要在所有的地方都尝试使用显式锁,如果场景满足需要,synchronized仍旧是一种很好的解决方案(也是应该被优先考虑的一种方式)
与synchronized再次对比下
    synchronized是JVM底层实现的,Lock是JDK接口层面的
    synchronized是隐式的,Lock是显式的,需要手动加锁与解锁
    synchronized乌无论如何都会释放,即使出现错误,Lock需要自己保障正确释放
    synchronized是阻塞式的获取锁,Lock可以阻塞获取,可中断,还可以尝试获取,还可以设置超时等待获取
    synchronized无法判断锁的状态,Lock可以进行判断
    synchronized可重入,不可中断,非公平,Lock可重入,可中断、可配置公平性(公平和非公平都可以)
    如果竞争不激烈,两者的性能是差不多的,可是synchronized的性能还在不断的优化,当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized   
    等   

4.1.2、ReentrantLock可重入锁与锁的公平和非公平

重入锁:

重入锁指的是当前线成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中对ReentrantLock和synchronized都是可重入锁,synchronized由jvm实现可重入即使,ReentrantLock都可重入性基于AQS实现。同时,ReentrantLock还提供公平锁和非公平锁两种模式。

ReentrantLock重入锁:

重入锁的基本原理是判断上次获取锁的线程是否为当前线程,如果是则可再次进入临界区,如果不是,则阻塞。
由于ReentrantLock是基于AQS实现的,底层通过操作同步状态来获取锁,下面看一下非公平锁的实现逻辑:
        final boolean nonfairTryAcquire(int acquires) {
             //获取当前线程
            final Thread current = Thread.currentThread();
             //通过AQS获取同步状态
            int c = getState();
            //同步状态为0,说明临界区处于无锁状态,
            if (c == 0) {
             //修改同步状态,即加锁
                if (compareAndSetState(0, acquires)) {
                    //将当前线程设置为锁的owner
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果临界区处于锁定状态,且上次获取锁的线程为当前线程
            else if (current == getExclusiveOwnerThread()) {
             //则递增同步状态
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
重入锁的最主要逻辑就锁判断上次获取锁的线程是否为当前线程。

非公平锁:

非公平锁是指当锁状态为可用时,不管在当前锁上是否有其他线程在等待,新近线程都有机会抢占锁。
上述代码即为非公平锁和核心实现,可以看到只要同步状态为0,任何调用lock的线程都有可能获取到锁,而不是按照锁请求的FIFO原则来进行的。

公平锁:

公平锁是指当多个线程尝试获取锁时,成功获取锁的顺序与请求获取锁的顺序相同。
  protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                此处为公平锁的核心,即判断同步队列中当前节点是否有前驱节点
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
从上面的代码中可以看出,公平锁与非公平锁的区别仅在于是否判断当前节点是否存在前驱节点!hasQueuedPredecessors() &&,由AQS可知,如果当前线程获取锁失败就会被加入到AQS同步队列中,那么,如果同步队列中的节点存在前驱节点,也就表明存在线程比当前节点线程更早的获取锁,故只有等待前面的线程释放锁后才能获取锁。

 

4.1.3、ReentrantReadWriteLock使用场景

一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。

 

   protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
上述代码的处理流程已经非常清晰:
​
    1、获取同步状态,并从中分离出低16为的写锁状态
    2、如果同步状态不为0,说明存在读锁或写锁
    3、如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
    4、如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
    5、如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入)
    6、将当前线程设置为写锁的获取线程
​
写锁的释放过程与独占锁基本相同:
        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
       在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。
读锁的获取与释放
​
读锁是一个共享锁,获取读锁的步骤如下:
    1、获取当前同步状态
    2、计算高16为读锁状态+1后的值
    3、如果大于能够获取到的读锁的最大值,则抛出异常
    4、如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
    5、如果上述判断都通过,则利用CAS重新设置读锁的同步状态
​
读锁的获取步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。

 

4.1.4、Condition用法

https://blog.csdn.net/a_runner/article/details/80640675

此前,我们知道用synchronized与wait()和notify()/notifyAll()方法结合可以实现等待/通知模式。但是,在使用notify()/notifyAll()方法进行通知时,被通知的线程却是由JVM随机选择的。为了摆脱这种窘境,Java在1.5引入了ReentrantLock和Condition类结合使用来达到有选择性的进行线程通知,在调度线程上更加灵活。下面是个测试方法:

4.2、LockSupport工具进阶

ockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。
​
     Java锁和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。LockSupport很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。
​
     LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。
​
LockSupport函数列表
​
// 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。
static Object getBlocker(Thread t)
// 为了线程调度,禁用当前线程,除非许可可用。
static void park()
// 为了线程调度,在许可可用之前禁用当前线程。
static void park(Object blocker)
// 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。
static void parkNanos(long nanos)
// 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。
static void parkNanos(Object blocker, long nanos)
// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(long deadline)
// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(Object blocker, long deadline)
// 如果给定线程的许可尚不可用,则使其可用。
static void unpark(Thread thread)
​
说明:LockSupport是通过调用Unsafe函数中的接口实现阻塞和解除阻塞的。
​
park和wait的区别
​
在调用对象的Wait之前当前线程必须先获得该对象的监视器(Synchronized),被唤醒之后需要重新获取到监视器才能继续执行。
​
而LockSupport并不需要获取对象的监视器。LockSupport机制是每次unpark给线程1个"许可"——最多只能是1,而park则相反,如果当前线程有许可,那么park方法会消耗1个并返回,否则会阻塞线程直到线程重新获得许可,在线程启动之前调用 park/unpark方法没有任何效果。
​
因为它们本身的实现机制不一样,所以它们之间没有交集,也就是说LockSupport阻塞的线程,notify/notifyAll没法唤醒.
​
总结下 LockSupport的park/unpark和Object的wait/notify:
​
    面向的对象不同;
    跟Object的wait/notify不同LockSupport的park/unpark不需要获取对象的监视器;
    实现的机制不同,因此两者没有交集。
​
虽然两者用法不同,但是有一点, LockSupport 的park和Object的wait一样也能响应中断.
        
public class MyLock implements Lock{
​
    AtomicReference<Thread>  owner = new AtomicReference<Thread>();
    public LinkedBlockingDeque<Thread> waiter = new LinkedBlockingDeque<>();
    
    
    @Override
    public void lock() {
        while(!owner.compareAndSet(null, Thread.currentThread())) {
            waiter.add(Thread.currentThread());
            LockSupport.park();
            waiter.remove(Thread.currentThread());
        }
    }
​
    @Override
    public void unlock() {
        if(owner.compareAndSet(Thread.currentThread(),null)){
            Object[] objects = waiter.toArray();
            for(Object object :objects){
                Thread next = (Thread)object;
                LockSupport.unpark(next);
            }
        }
    }
    
    
    
    @Override
    public void lockInterruptibly() throws InterruptedException {
        
    }
​
    @Override
    public boolean tryLock() {
        return false;
    }
​
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
​
    
​
    @Override
    public Condition newCondition() {
        return null;
    }
​
}
测试:
public class MyLockTest {
​
    MyLock myLock = new MyLock();
    private int i = 0;
​
    public void incr() {
        // �ȽϺ��滻
        myLock.lock();
        i++;
        myLock.unlock();
​
    }
​
    public static void main(String[] args) {
        MyLockTest demo = new MyLockTest();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    demo.incr();
                }
            }).start();
        }
​
        try {
            Thread.sleep(2000);
            System.out.println("i=" + demo.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

4.3、AbstractQueuedSynchronizer实现及源码分析

AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。

5、并发容器

5.1、ConcurrentHashMap

5.2、其他并发容器

5.3、阻塞队列

6、线程池

7、并发安全

--------------------------------------------------------------------------------------

1、线程基础、线程之间的共享与协作
https://blog.csdn.net/aimashi620/article/details/82017700
2、LockSupport工具进阶
https://www.cnblogs.com/moonandstar08/p/5132012.html
3、原子操作CAS
https://www.cnblogs.com/wangzhuxing/p/5207019.html
4、显示锁和AQS
https://www.cnblogs.com/waterystone/p/4920797.html
https://blog.csdn.net/zhangdong2012/article/details/79983404
5、AbstractQueuedSynchronizer实现及源码分析
http://www.cnblogs.com/micrari/p/6937995.html
6、并发容器和并发工具类
https://www.cnblogs.com/love-yang/p/9798271.html
http://www.cnblogs.com/leeSmall/p/8439263.html
http://www.importnew.com/21889.html
7、线程池
https://www.cnblogs.com/dolphin0520/p/3932921.html
队列介绍https://www.cnblogs.com/coprince/p/6349401.html
创建方式介绍https://blog.csdn.net/HepBen/article/details/80088719
8、并发安全
https://blog.csdn.net/weixin_42447959/article/details/83758933
https://www.cnblogs.com/timlearn/p/4012501.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值