Java 多线程并发总结

目录

1.volatile关键字

1.1 保证多个线程运行时的可见性问题

1.2 禁止指令重排

2 synchronized 关键字

2.1用法

2.2 实现原理

2.3 锁升级过程

3.Lock锁

3.1Lock()

3.2tryLock()

3.3tryLock(long time, TimeUnit unit)

3.4 lockInterruptibly() 可以响应中断

3.5unLock()

4.Java锁的分类

4.1悲观锁和乐观锁

4.2公平锁和非公平锁

4.2.1为什么要有非公平锁

4.2.2优缺点对比

4.2.3源码分析

4.3读写锁

4.3.2 比读写锁效率更高的StampedLock锁

4.4自旋锁

4.4.1什么自旋锁

4.4.2源码解析

4.4.3适用场景

4.5 CAS

4.5.3 缺点:ABA问题

4.6死锁

4.6.1概述

4.6.2代码演示

5.线程安全的容器

5.1HashMap线程为啥是不安全的

5.2 ConcurrentHashMap1.7和和1.8的区别

6.ThreadLocal

6.1两个主要的使用场景

6.2注意事项

7.Future 接口

7.1 概述

7.2 get() 获取结果

7.3 用 FutureTask 来创建 Future

7.4 CompletionService批量执行异步任务

7.4.1 概述

7.4.2 常用方法

7.4.3 利用CompletionService快速实现 Forking 集群模式

8.线程池

8.1核心参数

8.2工作原理

8.3四种拒绝策略

9.并发容器工具类

9.1 Semaphore 信号量

9.1.1 概述

9.1.2 主要方法

9.1.3 代码示例

9.2 CountDownLatch 倒计时门闩

9.2.1 概述

9.2.2主要方法:

9.2.3两个典型用法:

9.3 CyclicBarrier循环栅栏

9.3.1 概述

9.3.2 代码演示

9.3.3和 CountDownLatch 的异同

10 AQS

10.1概述

10.2实现原理


1.volatile关键字

1.1 保证多个线程运行时的可见性问题

  • 造成可见性问题是因为,所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据,当修改完毕后,再把修改后的结果放回到主内存中,线程间变量值的传递需要通过主内存来完成。
  • 在单线程的环境下是没有问题的,但在多线程的环境下可能会因为没有第一时间把工作内存中更新过的共享变量刷新到主内存中而出现脏数据,volatile的作用就是解决这个问题

1.2 禁止指令重排

禁止指令重排就是代码书写的顺序与实际执行的顺序不同,指令重排序是编译器为了提供程序的性能而做的优化

典型的应用示例就是双重检查锁实现一个线程安全的单例模式。
用volatile关键字修饰 singleton 对象,禁止指令重排
在 singleton = new Singleton();  创建对象的时候,实际执行了三步

  1. 给 singleton 分配内存空间;
  2. 调用 Singleton 的构造函数等,来初始化 singleton;
  3. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。


2和3之间,可能会被重排序,对象指向了分配的内存地址,但是还没有初始化,直接造成空指针异常

public class Singleton {
      private volatile static Singleton singleton;
      private Singleton() {}
      public static Singleton getSingleton() {
        if (singleton == null) {
          synchronized (Singleton.class) {
            if (singleton == null) {
              singleton = new Singleton();
            }
          }
        }
        return singleton;
      }
    }

2 synchronized 关键字

能够保证在同一时刻最多只有一个线程执行该代码,以达到保证并发安全的效果

2.1用法

分别是对象锁和类锁
对象锁:
    包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
类锁:
    指synchronizd修饰静态的方法或指定锁为Class对象,Java类可能有多个对象,但只有1个Class对象
    类锁有两种形式:
    形式一:synchronizd加在static方法上
    形式二:synchronizd(*.class)代码块


2.2 实现原理

  • Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的
  • monitor 有两个指令monitorenter和monitorexit,可以理解为加锁和解锁,当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的所有者。
  • 它是可重入的,如果你已经是这个monitor的所有者了,你再次进入,就会把进入数+1。同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
  • 其实,就是看你能否获得monitor的所有权


2.3 锁升级过程

无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁

  • 偏向锁的思想就是如果自始至终,这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
  • JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
  • 重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
  • JDK 1.6中的synchronized 就是重量级的锁

3.Lock锁


 Lock 接口加解锁相关的主要有 5 个方法

  •  lock()
  • tryLock()
  • tryLock(long time, TimeUnit unit)
  •  lockInterruptibly()
  • unlock()

3.1Lock()

  • Lock() 是最基础的获取锁的方法。在线程获取锁时如果锁已被其他线程获取,则进行等待。Lock获取锁和释放锁都是显式的,因此最佳实践是执行 lock() 后,首先在 try{} 中操作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中使用unlock()方法释放锁,以保证发生异常时锁一定被释放
  • 如果我们不遵守在 finally 里释放锁的规范,这样就会有问题,因为你不知道未来什么时候由于异常的发生,导致跳过了 unlock() 语句,使得这个锁永远不能被释放了,其他线程也无法再获得这个锁。以前有个大哥就因为没释放,造成了线上问题

代码示例:

Lock lock = new ReentrantLock();
lock.lock();
try{
    //获取到了被本锁保护的资源,处理任务
    //捕获异常

}finally{
    lock.unlock();   //释放锁
}

3.2tryLock()

  • lock() 方法不能被中断,这会带来很大的隐患,一旦陷入死锁,lock() 就会陷入永久等待,所以一般我们用 tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false,代表获取锁失败.
  • tryLock() 方法可以解决死锁问题
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    if (lock2.tryLock()) {
                        try {
                            System.out.println("获取到了两把锁,完成业务逻辑");
                            return;
                        } finally {
                            lock2.unlock();
                        }
                    }

                } finally {
                    lock1.unlock();
                }

            } else {
                Thread.sleep(new Random().nextInt(1000));
            }

        }

    }

如果代码中我们不用 tryLock() 方法,那么便可能会产生死锁,比如有两个线程同时调用这个方法,传入的 lock1 和 lock2 恰好是相反的,那么如果第一个线程获取了 lock1 的同时,第二个线程获取了 lock2,它们接下来便会尝试获取对方持有的那把锁,但是又获取不到,于是便会陷入死锁,但是有了 tryLock() 方法之后,我们便可以避免死锁的发生,首先会检测 lock1 是否能获取到,如果能获取到再尝试获取 lock2,但如果 lock1 获取不到也没有关系,我们会在下面进行随机时间的等待,这个等待的目标是争取让其他的线程在这段时间完成它的任务,以便释放其他线程所持有的锁,以便后续供我们使用,同理如果获取到了 lock1 但没有获取到 lock2,那么也会释放掉 lock1,随即进行随机的等待,只有当它同时获取到 lock1 和 lock2 的时候,才会进入到里面执行业务逻辑,比如在这里我们会打印出“获取到了两把锁,完成业务逻辑”,然后方法便会返回。


3.3tryLock(long time, TimeUnit unit)

  • tryLock() 的重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。
  • 这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时,在等待了一段指定的超时时间后,线程会主动放弃这把锁的获取,避免永久等待;在等待的期间,也可以随时中断线程,这就避免了死锁的发生。

3.4 lockInterruptibly() 可以响应中断

  • 我们可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过lockInterruptibly() 永远不会超时。
  • 这个方法本身是会抛出 InterruptedException 的,所以使用的时候,如果不在方法签名声明抛出该异常,那么就要写两个 try 块
public void lockInterruptibly() {
        try {
            lock.lockInterruptibly();
            try {
                System.out.println("操作资源");
            } finally {
                lock.unlock();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

3.5unLock()

unlock() 方法,用于解锁,对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了,如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数

4.Java锁的分类

4.1悲观锁和乐观锁

  • 悲观锁比较悲观,它认为如果不锁住这个资源,别的线程就会来争抢,就会造成数据结果错误,所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据,这样就可以确保数据内容万无一失。
  • Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,我们以 Lock 接口为例,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想。
  • 乐观锁比较乐观,认为自己在操作资源的时候不会有其他线程来干扰,所以并不会锁住被操作对象,不会不让别的线程来接触它,同时,为了确保数据正确性,在更新之前,会去对比在我修改数据期间,数据有没有被其他线程修改过:如果没被修改过,就说明真的只有我自己在操作,那我就可以正常的修改数据;如果发现数据和我一开始拿到的不一样了,说明其他线程在这段时间内修改过数据,那说明我迟了一步,所以我会放弃这次修改,并选择报错、重试等策略。
  • 乐观锁的实现一般都是利用 CAS 算法实现的

4.2公平锁和非公平锁

公平锁指的是按照线程请求的顺序,来分配锁;而非公平锁指的是不完全按照请求的顺序,在一定情况下,可以允许插队

4.2.1为什么要有非公平锁

让我们考虑一种情况,假设线程 A 持有一把锁,线程 B 请求这把锁,由于线程 A 已经持有这把锁了,所以线程 B 会陷入等待,在等待的时候线程 B 会被挂起,也就是进入阻塞状态,那么当线程 A 释放锁的时候,本该轮到线程 B 苏醒获取锁,但如果此时突然有一个线程 C 插队请求这把锁,那么根据非公平的策略,会把这把锁给线程 C,这是因为唤醒线程 B 是需要很大开销的,很有可能在唤醒之前,线程 C 已经拿到了这把锁并且执行完任务释放了这把锁。相比于等待唤醒线程 B 的漫长过程,插队的行为会让线程 C 本身跳过陷入阻塞的过程,如果在锁代码中执行的内容不多的话,线程 C 就可以很快完成任务,并且在线程 B 被完全唤醒之前,就把这个锁交出去,这样是一个双赢的局面,对于线程 C 而言,不需要等待提高了它的效率,而对于线程 B 而言,它获得锁的时间并没有推迟,因为等它被唤醒的时候,线程 C 早就释放锁了,因为线程 C 的执行速度相比于线程 B 的唤醒速度,是很快的,所以 Java 设计者设计非公平锁,是为了提高整体的运行效率。

4.2.2优缺点对比

公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点就在于整体执行速度更慢,吞吐量更小,相反非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。

4.2.3源码分析

在 ReentrantLock 类包含一个 Sync 类,这个类继承自AQS(AbstractQueuedSynchronizer),Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类。
 

public class ReentrantLock implements Lock, java.io.Serializable {

private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;

Sync 类的代码:

abstract static class Sync extends AbstractQueuedSynchronizer {...}

Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类:

static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}

公平锁获取锁的源码:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()
                compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }

    } else if (current == getExclusiveOwnerThread()) {
     
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值