JAVA并发

文章目录

一、线程安全性

1.什么是线程安全性?

一个对象是否需要是线程安全的,取决于它是否被多个线程访问。

线程安全性的定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

2.什么是竞态条件?

竞态条件的定义:由于不恰当的执行时序而出现不正确的结果。

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

public class LazyInitRace {
    
    private Object instance = null;

    public Object getInstance() {
        if(instance==null){
            instance = new Object();
        }
        return instance;
    }
}

上面的类包含一个竞态条件。假定线程A和线程B同时执行getInstance。A看到instance为空,于是执行new Object(),同时,B也同意需要判断instance是否为空,而这取决于B的执行时间。

3.如何避免竞态条件?

必须在某个线程修改某一变量时,通过某种方式防止其他线程使用这个变量。

4.内置锁 synchronized

利用 synchronized 实现的基础:Java中的每一个对象都可以作为锁。具体表现为以下3中形式:

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对应同步方法块,锁是 synchronized 括号里配置的对象

获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

5.锁升级

参考博客

6.重入

内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

重入的一种实现方法是,为每个锁关联一个计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。

当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值为1。

如果同一个线程再次请求获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

public class Widget {
    public synchronized void doSomething(){
        return;
    }
}

public class LoggingWidget extends Widget{
    @Override
    public synchronized void doSomething() {
        super.doSomething();
    }
}

如果内置锁是不能重入的,那么当子类 LoggingWidget 调用父类的doSomething()方法时将会被阻塞,因为Widget已经被子类持有了。

二、对象的共享

1.重排序

public class NoVisibility {
    private volatile static boolean ready;
    private static int number;

    public static class ReadThread extends Thread{
        @Override
        public void run() {
            while(!ready){
                Thread.yield();//yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReadThread().run();
        number = 42;
        ready = true;
    }
}

NoVisibility可能会持续循环下去,因为 ReadThread 可能永远都看不到ready的值,这种现象被称为 重排序

重排序:在没有同步的情况下,编译器,处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。

2.Volatile

当把变量声明为volatile之后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

volatile变量自身具有下列特性:

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

因此,当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖于变量的当前值,或者确保只有单个线程更新变量的值。
  • 在访问变量时不需要加锁。

2.1 volatile 写-读的内存语义

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1; // 1 
        flag = true; // 2 
    }
    public void reader() { 
        if (flag) { // 3
            int i = a; // 4 
        } 
    } 
}

volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
在这里插入图片描述
如图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。
此时,本地内存A和主内存中的共享变量的值是一致的。

volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
在这里插入图片描述
如图所示,在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一 致。

如果我们把volatile写和volatile读两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

下面对volatile写和volatile读的内存语义做个总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。 ·
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

3.ThreadLocal

参考博客
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建了ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题

public class ThreadLocalTest {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("thread1");
                System.out.println(threadLocal.get());
                threadLocal.remove();
                System.out.println(threadLocal.get());
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("thread2");
                System.out.println(threadLocal.get());
                threadLocal.remove();
                System.out.println(threadLocal.get());
            }
        });
        t1.start();
        t2.start();
    }
}

4.Final

final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的)

此外,final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享对象时无需同步。

对于final域,编译器和处理器要遵守两个重排序规则。

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用,而普通域不具有这个保障

5.双重检查锁定与延迟初始化

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销

双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法

5.1 双重检查锁定的由来

在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。此时,程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题。

比如,下面是非线程安全的延迟初始化对象的示例代码。

public class UnsafeLazyInitialization {

    private static Instance instance;

    public static Instance getInstance(){
        if(instance == null){           // 1:A线程执行
            instance = new Instance();  // 2:B线程执行
        }
        return instance;
    }
}

在UnsafeLazyInitialization类中,假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化.

可以使用 synchronized 来解决上述问题

public class SafeLazyInitialization {

    private static Instance instance;

    public static synchronized Instance getInstance(){
        if(instance == null){           
            instance = new Instance();  
        }
        return instance;
    }
}

由于对getInstance()方法做了同步处理,synchronized将导致性能开销

如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降。

反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

另一个解决方法是使用 **双重检查锁定(Double-Checked Locking)**来减少同步的开销。

public class DoubleCheckedLocking {

    private static Instance instance;

    public static synchronized Instance getInstance(){
        if(instance == null){
            synchronized (DoubleCheckedLocking.class){
                if(instance == null){
                    instance = new instance();   //问题出在这里
                }
            }
        }
        return instance;
    }
}

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美

但其实,由于初始化时可能会存在重排序,因此可能 instance != null 但是还未完成初始化,如下图所示的重排序:

原本顺序:
在这里插入图片描述
重排序后:
在这里插入图片描述

DoubleCheckedLocking示例代码的第7行(instance=new Instance();)如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!

在这里插入图片描述

5.2 基于 volatile 的解决方案

对于前面的基于双重检查锁定来实现延迟初始化的方案,只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。

public class DoubleCheckedLocking {

    private volatile static Instance instance;

    public static synchronized Instance getInstance(){
        if(instance == null){
            synchronized (DoubleCheckedLocking.class){
                if(instance == null){
                    instance = new instance();   //问题出在这里
                }
            }
        }
        return instance;
    }
}

当声明对象的引用为volatile后,2和3之间的重排序,在多线程环境中将会被禁止。

5.3 基于 类初始化 的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案

public class InstanceFactory {
    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }
    public static Integer getInstance(){
        return InstanceHolder.instance; //这里将导致InstanceHolder类被初始化
    }
}

三、对象的组合

1.实例封闭

封装简化了线程安全类的实现过程,它提供一种实例封闭机制,通常也简称为 封闭,被封闭对象一定不能超出它们既定的作用域。

public class StringSet {
    private final HashSet<String> set = new HashSet();
    
    public synchronized void addPerson(String p){
        set.add(p);
    }
    
    public synchronized boolean containsPerson(String p){
        return set.contains(p);
    }
}

在上面代码中,StringSet的状态是由HashSet来管理的,而HashSet并非线程安全的。但是由于set是私有的并且不会逸出,因此HashSet被封闭在StringSet中。

唯一能访问set的代码路径是addPerson和containsPerson,在执行它们都要获得PersonSet上的锁。

2.客户端加锁

public class ListHelper{
    public List<String> list = Collections.synchronizedList(new ArrayList<>());
    .......
    public synchronized boolean putIfAbsent(String x){
        boolean absent = !list.contains(x);
        if(absent){
            list.add(x);
        }
        return absent;
    }
}

以上代码是非线程安全的,虽然其中的 putIfAbsent 方法被声明为 synchronized,但却使用了不同的锁,这意味着 putIfAbsent 相对于List的其他操作并不是原子的,因此就无法确保当 putIfAbsent 执行时另一个线程不会修改链表。

修改代码后可以实现线程安全。

public class ListHelper{
    public List<String> list = Collections.synchronizedList(new ArrayList<>());

    public synchronized boolean putIfAbsent(String x){
        synchronized (list){
            boolean absent = !list.contains(x);
            if(absent){
                list.add(x);
            }
            return absent;   
        }
    }
}

3.组合

当为现有的类添加一个原子操作时,有一种更好的方法:组合

public class ImprovedList<T> implements List<T> {
    
    private final List<T> list;
    
    public ImprovedList(List<T> list){
        this.list = list;
    }
    
    public synchronized boolean putIfAbsent(T x){
        boolean contains = list.contains(x);
        if(!contains){
            list.add(x);
        }
        return !contains;
    }
    
    @Override
    public void clear() {
        list.clear();
    }
    //..按照类似的方式委托List的其他方法
}

ImprovedList 通过将List对象的操作委托给底层的List实例来实现List的操作,同时还添加了一个原子的putAbsent方法(与Collections.synchronizedList和其他容器封装器一样,ImprovedList 假设把某个链表对象传给构造函数以后,客户代码不会再直接使用这个对象,而只能通过 ImprovedList 来访问它

四、基础构建模块

1.迭代器与ConcurrentModificationException

对容器类进行迭代的标准方式是使用 Iterator,然而,如果有其他线程并发地修改容器,那么它们表现出的行为是 快速失败fail-fast)的,这意味着,当它们发现容器在迭代过程中被修改时,就会抛出 ConcurrentModificationException 异常。

1.1 快速失败(fail—fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount != expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。

场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

1.2 安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了 ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

2.隐藏迭代器

在某些情况下,迭代器会隐藏起来:

public class HiddenIterator {
    private final Set<Integer> set = new HashSet<>();
    
    public synchronized void add(Integer i){
        set.add(i);
    }
    public synchronized void remove(Integer i){
        set.remove(i);
    }
    public void addTenThings(){
        Random r = new Random();
        for (int i = 0; i < 10; i++) {
            add(r.nextInt());
        }
        System.out.println("DEBUG: added ten elements to "+set);
    }
}

在上面的代码中,HiddenIterator 没有显式的迭代操作,但在最后的 System.out.println 语句中将进行迭代操作。

编译器将字符串的连接操作转换为调用 StringBuilder.append(Object),而这个方法又会调用set容器的toString方法,标准容器的toString方法将迭代容器(容器的hashCode和equals等方法也会间接地执行迭代操作),并在每个元素上调用toString来生成容器内容的格式化表示。因此,addTenThings 方法可能会抛出 ConcurrentModificationException。如果在HiddenIterator 中用synchronziedSet来包装HashSet,那么就不会发生这种错误。

3.并发容器

3.1 ConcurrentHashMap

ConcurrentHashMap是线程安全且高效的HashMap。

JDK1.7的ConcurrentHashMap底层采用:Segments数组+HashEntry数组+链表

JDK1.8的ConcurrentHashMap底层采用:Node数据+链表+红黑树

(1)线程不安全的HashMap

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final HashMap<String,String> map  = new HashMap<>();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            map.put(UUID.randomUUID().toString(),"");
                        }
                    },"ftf"+i).start();
                }
            }
        });
        t.start();
        t.join();
    }
}

HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。(这是在JDK1.7(头插法)中,在JDK1.8(尾插法)中导致的线程不安全主要是put方法可能会导致值被覆盖)参考博客

(2)效率低下的HashTable

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方
法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低

(3)ConcurrentHashMap 锁分段(JDK 1.7 )

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

ConcurrentHashMap的结构

ConcurrentHashMap是由Segment数组结构HashEntry数组结构组成。

Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;

HashEntry则用于存储键值对数据

一个ConcurrentHashMap里包含一个Segment数组。

Segment的结构和HashMap类似,是一种数组和链表结构。

一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁.

(4) ConcurrentHashMap CAS + synchronized(JDK 1.8)
在JDK 1.8中,ConcurrentHashMap抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性,取消了 ReentrantLock 改为了 synchronized。

底层的数据结构如下:
在这里插入图片描述

(5)不会抛出ConcurrentModificationException

ConcurrentHashMap 与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出 ConcurrentModificationException,因此不需要再迭代过程中加锁。

3.2 CopyOnWriteArrayList

CopyOnWriteArrayList 用于替代同步List,并且在迭代期间不需要对容器进行加锁或者复制。在每次修改 CopyOnWriteArrayList 时,都会创建并重新发布一个新的容器副本,从而实现可变性。

写入时复制”容器返回的迭代器不会抛出 ConcurrentModificationException ,并且返回的元素与迭代器创建时的元素完全一致。

思考:会不会有两个线程同时对 CopyOnWriteArrayList 进行写操作?
:不会,CopyOnWriteArrayList 在执行写操作时会加锁,保证每次只会有一个线程修改List。(读操作时不会加锁

3.3 ConcurrentLinkedQueue

参考博客

4.阻塞队列

4.1 什么是阻塞队列?

阻塞队列是一个在队列基础上又支持了两个附加操作的队列。

这两个附加的操作支持阻塞的插入和移除方法。

  • 支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
  • 支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。
    在这里插入图片描述

参考博客

5.Fork/Join框架

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

我们再通过Fork和Join这两个单词来理解一下Fork/Join框架。Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。

比如计算1+2+…+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和, 最终汇总这10个子任务的结果。

在这里插入图片描述

5.1 工作窃取算法

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行

那么,为什么 需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如A线程负责处理A 队列里的任务。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有 任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行
在这里插入图片描述
工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争。

工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。

Fork and Join: Java也可以轻松地编写并发程序

4.同步工具类

1.闭锁 CountDownLatch

闭锁是一种同步工具类,可以延迟线程的进度直到其到终止状态。

闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过。
当到达结束状态时,这扇门会打开并允许所有的线程通过。
当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。
闭锁可以用来确保某些活动直到其他活动都完成再继续执行。

例如:

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动
  • 等待直到某个操作的所有参与着都就绪再继续执行

CountDownLatch 是闭锁的一种实现方式。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。

  • countDown方法会使计数器减一,表示有一个事件已经发生了。
  • await方法等待计数器达到 0 ,这表示所有需要等待的事件都已经发生。如果计数器的值非 0 ,那么await会一直阻塞直到计数器为 0 ,或者等待中的线程中断,或者等待超时。
  • CountDownLatch 不能重新初始化或者修改其内部计数器的值
public class CountDownLatchTest {
   static CountDownLatch countDownLatch = new CountDownLatch(2);

   public static void main(String[] args) throws InterruptedException {
       new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("1");
               countDownLatch.countDown();
               System.out.println("2");
               countDownLatch.countDown();
           }
       }).start();
       countDownLatch.await();
   }
}
public class CountDownLatchTest {
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("1");
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("2");
                countDownLatch.countDown();
            }
        }).start();
        countDownLatch.await();
    }
}

由于 countDown方法可以用在任何地方,所以在递减计数器的时候,可以在一个线程里面递减多次,也可以在多个线程递减多次。

2.信号量 Semaphore

计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。

Semaphore 中管理着一组虚拟的许可,许可的初始数量可以通过构造函数来指定。
在执行操作时可以首先获得许可(只要还要剩余的许可),并在使用以后释放许可。
如果没有许可,那么 acuqire方法阻塞直到有许可,release方法将返回一个许可给信号量。
初始值为1的Semaphore可以作为互斥锁(mutex)

应用场景

1.可以使用 Semaphore 将任何一种容器变成 有界阻塞容器

public class SemaphoreSet<T> {

    private final Semaphore semaphore;
    private final Set<T> set;

    public SemaphoreSet(Semaphore semaphore) {
        this.semaphore = semaphore;
        this.set = Collections.synchronizedSet(new HashSet<T>());
    }

    public boolean add(T x) throws InterruptedException {
        semaphore.acquire();
        boolean wasAdd = false;
        try {
            wasAdd = set.add(x);
            return wasAdd; 
        }finally {
            if(!wasAdd){
                semaphore.release();
            }
        }
    }
    
    public boolean remove(Object o){
        boolean wasRem = set.remove(o);
        if(wasRem){
            semaphore.release();
        }
        return wasRem;
    }
    
}

2.流量控制:假设要读取几万个文件的数据,但是只有10个数据库连接,因此,我们必须控制只有10个线程同时获取数据库来保存数据。

public class SemaphoreTest {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(10);
        ExecutorService executorService = Executors.newFixedThreadPool(30);
        for(int i = 0;i<30;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println("sava data");
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                }
            });
        }
        executorService.shutdown();
    }
}

3.栅栏 Barrier

栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。

所有线程必须同时到达栅栏位置,才能继续执行

闭锁用于等待事件,而栅栏用于等待其他进程

CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

public class CyclicBarrierTest {
    static CyclicBarrier c = new CyclicBarrier(2);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    c.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(1);
            }
        }).start();
        try {
            c.await();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(2);
    }
}

4.CyclicBarrier 和 CountDownLatch 的区别

CountDownLatch 的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。

闭锁用于所有线程等待一个外部事件的发生;栅栏则是所有线程相互等待,直到所有线程都到达某一点时才打开栅栏,然后线程可以继续执行。

五、任务执行

1.Executor框架

参考博客

1.1 饱和策略

当有界队列被填满后,饱和策略开始发挥作用。

ThreadPoolExecutor 的饱和策略可以通过调用 setRejectedExecutionHandler 来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略

JDK提供了几种不同的 RejectedExecutionHandler 实现:

  • AbortPolicy 默认的饱和策略,该策略将抛出未检查的 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
  • DiscardPolicy 当新提交的任务无法保存到队列中等待执行时,该策略会抛弃该任务。
  • DiscardOldestPolicy 该策略会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么DiscardOldestPolicy 将导致抛弃优先级最高的任务,因此最好不要将该策略与优先级队列放在一起使用)
  • CallerRunsPolicy 调用者回调策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

2.FutureTask

2.1 FutureTask简介

FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给 Executor 执行,也可以由调用线程直接执行(FutureTask.run())。

可以把FutureTask交给Executor执行;也可以通过 ExecutorService.submit(Runnable) 方法返回一个 FutureTask,然后执行 FutureTask.get() 方法或 FutureTask.cancel() 方法。

在这里插入图片描述
当多个线程试图同时执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行。下面是对应的示例代码

public class FutureTaskTest {

    private final ConcurrentMap<Object, Future<String>> taskCache = new ConcurrentHashMap();

    private String executionTask(final String taskName) throws ExecutionException, InterruptedException {
        while(true){
            Future<String> future = taskCache.get(taskName);
            if(future==null){
                Callable<String> task = new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        return taskName;
                    }
                };
                FutureTask<String> futureTask = new FutureTask<>(task);
                //putIfAbsent 如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null
                future = taskCache.putIfAbsent(taskName,futureTask);
                if(future==null){
                    future = futureTask;
                    futureTask.run();
                }
            }
            try {
                return future.get();
            }catch (CancellationException e){
                taskCache.remove(taskName,future);
            }
        }
    }
}

2.2 FutureTask的实现

FutureTask的实现基于AbstractQueuedSynchronizer(以下简称为AQS)。java.util.concurrent中的很多可阻塞类(比如ReentrantLock)都是基于AQS(AQS详解)来实现的。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。JDK 6中AQS 被广泛使用,基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、 CountDownLatch和FutureTask。

六、取消与关闭

1.中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。

Java的中断是一种协作机制,也就是说通过中断并不能直接STOP另外一个线程,而需要被中断的线程自己处理中断,即仅给了另一个线程一个中断标识,由线程自行处理。
参考博客

2.停止基于线程的服务

2.1 关闭 ExecutorService

ExecutorService 提供了两种关闭方法:

  • shutdown 正常关闭 正常关闭虽然速度慢,但却更安全,因为ExecutorService会一直等待队列中的所有任务都执行完成后才关闭
  • shutdownNow 强行关闭 强行关闭的速度更快,但风险也更大,因为任务很可能在执行到一半时被结束

在进行强行关闭时,shutdownNow 首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单。

然而我们无法通过常规方法来找出哪些任务已经开始但尚未执行,下面的代码给出了如何在关闭过程中判断正在执行的任务。

要使这项技术能发挥作用,任务在返回时必须维持线程的中断状态

public class TrackingExecutor extends AbstractExecutorService {
    
    private final ExecutorService exec;
    private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
    
    public List<Runnable> getCancelledTasks(){
        if(!exec.isTerminated()){
            throw new IllegalStateException();
        }
        return new ArrayList<Runnable>(tasksCancelledAtShutdown);
    }
    
    @Override
    public void execute(final Runnable runnable) {
        exec.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    runnable.run();
                }finally {
                    if(isShutdown()&&Thread.currentThread().isInterrupted()){
                        tasksCancelledAtShutdown.add(runnable);
                    }
                }
            }
        });
    }
    ....
}

2.JVM关闭

2.1守护线程

有时候,你希望创建一个线程来执行一些辅助任务,但又不希望这个线程阻碍JVM的关闭,在这种情况下就要使用守护线程(Daemon Thread)。

Daemon线程是一种支持性线程,因为它只要被用作程序后台调度以及支持性工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

这意味着,当一个Java虚拟机中不存在Daemon线程的时候,Java虚拟机将会退出。

可以通过调用 Thread.setDaemon(true) 将线程设置为Daemon线程。

Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块不一定会执行,代码如下:

public class Daemon {

    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner());
        thread.setDaemon(true);
        thread.start();
    }

    static class DaemonRunner implements Runnable{

        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                System.out.println("finally run");
            }
        }
    }
}

七、锁

1.Lock接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。
以下是Lock的使用方法:不要将获取锁的过程放在try块中,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故释放。

		Lock lock = new ReentrantLock();
        lock.lock();
        try {
            
        }finally {
            lock.unlock(); //在finally块中释放锁,目的是保证在获取到锁之后,最终能释放锁
        }

在这里插入图片描述

2.队列同步器 AbstractQueuedSynchronizer AQS

更多详细内容参考:参考博客

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。:

  • getState():获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性

同步器可重写的方法与描述如下:

在这里插入图片描述

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下:
在这里插入图片描述

自定义同步组件 TwinsLock

该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞。

public class TwinsLock implements Lock {

    private final Sync sync = new Sync(2);
    private static final class Sync extends AbstractQueuedSynchronizer{

        Sync(int count){
            if(count<=0){
                throw new IllegalArgumentException("count must large than zero");
            }
            setState(count);
        }

        @Override
        //负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取
        protected int tryAcquireShared(int reduceCount) {
            for (;;){
                int current = getState();
                int newCount = current - reduceCount;
                if(newCount<0 || compareAndSetState(current,newCount)){
                    return newCount;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int reduceCount) {
            for(;;){
                int current = getState();
                int newCount = current+reduceCount;
                if(compareAndSetState(current,newCount)){
                    return true;
                }
            }
        }
    }


    @Override
    public void lock() {
        sync.acquireShared(1);
    }

    @Override
    public void unlock() {
        sync.releaseShared(1);
    }
    //其他方法略
}

3.重入锁 ReentrantLock

参考博客

重入锁 ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

4.读写锁 ReadWriteLock

读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

Java并发包提供读写锁的实现是 ReentrantReadWriteLock,它提供的特性如表所示:
在这里插入图片描述

4.1 读写锁的接口与示例

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即**readLock()方法和writeLock()**方 法,而其实现——ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法以及描述如表所示。
在这里插入图片描述
接下来,通过一个缓存示例说明读写锁的使用方式:

public class Cache {
    static Map<String,String> map = new HashMap();
    static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    static Lock read = reentrantReadWriteLock.readLock();
    static Lock write = reentrantReadWriteLock.writeLock();
    
    public static final String get(String key){
        read.lock();
        try {
            return map.get(key);
        }finally {
            read.unlock();
        }
    }
    
    public static final String set(String key,String value){
        write.lock();
        try {
            return map.put(key,value);
        }finally {
            write.unlock();
        }
    }
    
    public static final void clear(){
        write.lock();
        try {
            map.clear();
        }finally {
            write.unlock();
        }
    }
}

上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,String value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式.

4.2 读写锁的实现分析

4.2.1 读写锁的实现与分析

读写锁同样依赖 自定义同步器(AQS) 来实现同步功能,而读写状态就是其同步器的同步状态。 回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图所示。

在这里插入图片描述
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算

4.2.2 写锁的获取与释放

写锁是一个支持重进入的排它锁。

如果当前线程已经获取了写锁,则增加写状态。

如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。

下面是 ReentrantReadWriteLock 的 tryAcquire 方法

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState(); // 获取同步状态
    int w = exclusiveCount(c); // 根据同步状态获取写锁状态
    // 已经有线程获取到了锁
    if (c != 0) { //c!=0说明存在读锁或者写锁
        // 如果写线程数(w)为0(换言之存在读锁) 或者写锁不为0,同时持有锁的线程不是当前线程就返回失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        
        // 写锁重入
        setState(c + acquires);
        return true;
    }
    
    // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    
    // 如果c=0,w=0(没有写锁也没有读锁)或者c>0,w>0(重入),则设置当前线程为锁的拥有者
    setExclusiveOwnerThread(current);
    return true;
}

如果存在读锁,则写锁不能被获取

原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

4.2.3 读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

下面是 ReentrantReadWriteLock 的 tryAcquireShared 方法

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    
     // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 获取读锁数量
    int r = sharedCount(c);
    
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

需要注意的是,读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护.

4.2.4 锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。

锁降级分为三个步骤:

1. 把持写锁
2. 获取读锁
3. 释放写锁

5.Condition 接口

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法;Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition();调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性

在这里插入图片描述

5.1 Condition接口与示例

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。

Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

Condition的使用方式比较简单,需要注意在调用方法前获取锁。

public class ConditionUseCase {

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        }finally {
            lock.unlock();
        }
    }

    public void conditionSignal(){
        lock.lock();
        try {
            condition.signal();
        }finally {
            lock.unlock();
        }
    }
}

如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

在这里插入图片描述
获取一个Condition必须通过Lock的newCondition()方法

下面通过一个有界队列的示例来深入了解Condition的使用方式

有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”.

public class BoundedQueue<T> {

    private Object[] items;
    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size){
        items = new Object[size];
    }
    // 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
    public void add(T t) throws InterruptedException {
        lock.lock();
        try{
            while (count==items.length){
                notFull.await();
            }
            items[addIndex] = t;
            if(++addIndex==items.length){
                addIndex = 0;
            }
            ++count;
            notEmpty.signalAll();
        }finally {
            lock.unlock();
        }
    }

    // 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while(count==0){
                notEmpty.await();
            }
            Object x = items[removeIndex];
            if(++removeIndex==items.length){
                removeIndex = 0;
            }
            --count;
            notFull.signal();
            return (T)x;
        }finally {
            lock.unlock();
        }
    }
}

使用Condition实现生产者-消费者模式

public class ConsumeAndProducer {

    PriorityQueue<Integer> queue;
    private int queueSize;
    Lock lock = new ReentrantLock();

    Condition notFull = lock.newCondition();
    Condition notEmpty = lock.newCondition();

    public ConsumeAndProducer(int size) {
        queueSize = size;
        queue = new PriorityQueue<>(size);
    }


    class Consume implements Runnable{

        @Override
        public void run() {
            consume();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        volatile boolean flag = true;

        private void consume(){
            while(flag){
                lock.lock();
                try {
                    while (queue.isEmpty()){
                        try {
                            System.out.println("队列空,等待队列");
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            flag = false;
                        }
                    }

                    queue.poll();
                    notFull.signal();
                    System.out.println("从队列取走一个元素,队列剩余" + queue.size()+"个元素");
                }finally {
                    lock.unlock();
                }
            }
        }

    }

    class Producer implements Runnable{

        @Override
        public void run() {
            producer();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        volatile boolean flag = true;

        public void producer(){
            while (flag){
                lock.lock();
                try {
                    while(queue.size()==queueSize){
                        try {
                            System.out.println("队列满,等待有空余空间");
                            notFull.await();
                        } catch (InterruptedException e) {
                            flag = false;
                        }
                    }

                    queue.offer(1);
                    notEmpty.signal();
                    System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
                }finally {
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConsumeAndProducer consumeAndProducer = new ConsumeAndProducer(10);
        Thread t1 = new Thread(consumeAndProducer.new Producer());
        Thread t2 = new Thread(consumeAndProducer.new Consume());
        t1.start();
        t2.start();
        Thread.sleep(10);
        t1.interrupt();
        t2.interrupt();
    }
}

5.2 Condition的实现分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所
以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

5.2.1 等待队列

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是 在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会

1.释放锁
2.构造成节点
3.加入等待队列
4.进入等待状态

事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)

当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列
在这里插入图片描述
如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter 指向它,并且更新尾节点即可

上述节点引用更新的过程并没有使用CAS保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

5.2.2 等待

调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁

如果从队列(同步队列等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中,如下图所示:
在这里插入图片描述

5.2.3 通知

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中

6.死锁

参考博客

public class LeftRightDeadlock {
    private final Object left = new Object();
    private final Object right = new Object();

    public void leftRight(){
        synchronized (left){
            synchronized (right){
                System.out.println("LR");
            }
        }
    }

    public void rightLeft(){
        synchronized (right){
            synchronized (left){
                System.out.println("RL");
            }
        }
    }
}

上面的代码存在死锁风险,原因是:两个线程试图以不同的顺序来获得相同的锁。如果按照相同的顺序来请求锁,那么就不会出现循环的加锁依赖性,因此也就不会产生死锁了。
在这里插入图片描述
如果所有线程以固定的顺序来获得锁,那么在查询中就不会出现锁顺序死锁问题

如何定义锁的顺序呢?可以使用System.identityHashCode方法,该方法将返回由Object.hashCode返回的值。

八、原始操作类

在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型原子更新数组原子更新引用原子更新属性(字段)

1. 原子更新基本类型类

使用原子的方式更新基本类型,Atomic包提供了以下3个类。

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

AtomicInteger的常用方法如下:

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的 value)相加,并返回结果。
  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方 式将该值设置为输入的值。
  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
public class AtomicIntegerTest {

    static AtomicInteger ai = new AtomicInteger(1);
    
    public static void main(String[] args) {
        System.out.println(ai.getAndIncrement());
        System.out.println(ai.get());
    }
}

2.原子更新引用数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。

AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下:

  • int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
  • boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
public class AtomicIntegerArrayTest {
    static int[] values = new int[]{1,2};
    static AtomicIntegerArray ai = new AtomicIntegerArray(values);
    public static void main(String[] args) {
        ai.getAndSet(0,3);
        System.out.println(ai.get(0));
        System.out.println(values[0]);
    }
}

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组(即原数组的值不会改变)

3.原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。

  • AtomicReference:原子更新引用类型。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
public class AtomicReferenceTest {

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

    public static void main(String[] args) {
        User user = new User("conan",15);
        atomicReference.set(user);
        User updateUser = new User("Apple",19);
        atomicReference.compareAndSet(user,updateUser);
        System.out.println(atomicReference.get().getName());
        System.out.println(atomicReference.get().getOld());
    }


    static class User{
        private String name;
        private int old;

        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

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

        public int getOld() {
            return old;
        }

        public void setOld(int old) {
            this.old = old;
        }
    }
}

4.原子更新字段类

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起 来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的 ABA 问题。

要想原子地更新字段类需要两步:

  • 第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法**newUpdater()**创建一个更新器,并且需要设置想要更新的类和属性。
  • 第二步,更新类的字段(属性)必须使用public volatile修饰符。
public class AtomicIntegerFieldUpdaterTest {

    private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class,"old");

    public static void main(String[] args) {
        User conan  = new User("conan",10);
        System.out.println(a.getAndIncrement(conan));
        System.out.println(a.get(conan));
    }

    static class User{
        private String name;
        private int old;

        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

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

        public int getOld() {
            return old;
        }

        public void setOld(int old) {
            this.old = old;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值