并发编程Thread

并发编程Thread

常见概念

线程,进程与纤程/协程

进程course在OS(操作系统)中运行的一个应用程序,通过OS申请运行时所需要的资源,操作系统资源分配的基本单位。
线程thread在进程中运行的一个独立的任务单元,由该进程分配所需要的资源,操作系统资源调度的基本单位。
纤程/协程quasar是用户态的线程,是线程中的线程,切换和调度不需要经过OS(操作系统)。轻量级的线程
管程monitor监视器。让多个进程或线程同时访问一个共享资源时能达到"互斥"和"同步"

纤程的优势:

1.占有的资源少,为什么说他占有资源少呢?举例:操作系统要启一个线程前后要为这个线程配的内存数据大概有1M,而纤程大概是4K

2.由于纤程非常的轻量级,所以切换比较简单

3.可以同时被启动很多个(10万个都没问题)
目前支持内置纤程的语言Kotlin Scala Go 等,可惜的是,Java没有官方的纤程支持,好在有个叫做Quasar的库可堪一用

1)Java有几个线程?

Java有2个线程:一个是main,一个是GC 。

2)Java能真正开启线程吗?

不能,因为java是运行在虚拟机上的,无法直接操作硬件。

public class Thread implements Runnable {
    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
          	// 调用本地方法
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
              
            }
        }
    }

  	// 本地方法,底层是c++开启线程,java是运行在虚拟机上的,无法直接操作硬件
    private native void start0();
}

线程的调度

线程的调度由JVM负责。多线程程序运行过程不可控,但是结果一定得可控

调度方式特点
分时调度让所有的线程轮流获得CPU的使用权,平均分配每个线程占用的CPU时长。
抢占式调度让处于可运行状态的优先级高的线程优先占用CPU,如果优先级相同,则随机选择。

同步Synchronous和异步Asynchronous

同步和异步通常用来形容一次方法调用。

同步方法调用一旦开始,调用者必须等到方法调 用返回后,才能继续后续的行为。

异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执 行。整个过程,不会阻碍调用者的工作。

在这里插入图片描述

并发Concurrency,并行Parallelism和串行

并行:同时执行。

并发:交替执行。

串行:时间上不可能发生重叠,前一个任务没结束,下一个任务只能等着。

在这里插入图片描述

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只 能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

阻塞和非阻塞

阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么 其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情 况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界 区上的线程都不能工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝 试不断前向执行。

时间片

CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

并发级别

由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把 并发的级别进行分类,大致上可以分为阻塞、无饥饿、无障碍、无锁、无等待几种。

并发级别
阻塞(Blocking)一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用 synchronized关键字,或者重入锁时,我们得到的就是阻塞的线程。无论是synchronized或者重入锁,都会试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需资源为止。
无饥饿(Starvation-Free)如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就说是,对于同一个资源的分配,是不公平的!如果加入公平锁,满足先来后到,那么饥饿就不会产生, 不管新来的线程优先级多高,要想获得资源,就必须乖乖排队。那么所有的线程都有机会执行。
无障碍(Obstruction-Free)无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界 区的问题导致一方被挂起。换言之,大家都可以大摇大摆地进入临界区了。那么如果大家一起 修改共享数据,把数据改坏了可怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它 就会立即对自己所做的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就 可以顺利完成自己的工作,走出临界区。
无锁(Lock-Free)无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但 不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。在无锁的调用中,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不 断尝试修改共享变量。如果没有冲突,修改成功,那么程序退出,否则继续尝试修改。但无论 如何,无锁的并行总能保证有一个线程是可以胜出的,不至于全军覆没。至于临界区中竞争失 败的线程,它们则必须不断重试,直到自己获胜。如果运气很不好,总是尝试不成功,则会出 现类似饥饿的现象,线程会停止不前。
无等待(Wait・Free)无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进 行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个 步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,它们之间的区别只是 对循环次数的限制不同。

CPU的术语

在这里插入图片描述

线程的调度方式

线程的调度由JVM负责。多线程程序过程不可控,但结果可控。

抢占式调度

每个线程由系统分配CPU时间,线程本身无法控制使用多次CPU时间。好处是线程的执行时间是可控的,不会造成因为一个线程导致进程长时间阻塞问题。

协同式调度

线程自己控制CPU时间。并且当前线程执行完毕后需要通知系统切换另外一个线程。最主要的问题是线程的切换取决于线程本身,若线程存在Bug导致切换线程不成功则会一直阻塞。

线程间通信与同步

线程虽然可以独立的执行。但是总会有需要不同的线程互相配合的情况,这就涉及到线程的通信与同步。目前有两种方式。

共享内存

共享内存的并发模型里线程通过显示的同步即通过互斥实现对公共空间的读写,将需要共享的数据同步到公共的内存中,这样便实现了间接的通信。即共享内存是显示同步,隐式通信。

消息传递

消息传递模型即消息传递,线程间无公共内存,因为消息通信天然具有先后关系所以间接实现类数据的同步。所以消息传递模型是显示通信,隐式同步。

线程模型

一对一模型
一对多模型
多对多模型

Reactor模型(反应堆模型)/Dispatcher模型(分发模式)

在这里插入图片描述

含义具体实现类
死锁Deadlock几个线程都在等着对方先执行。
饥饿Starvation某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。 比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。
活锁Livelock线程之间都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动, 而没有一个线程可以同时拿到所有资源而正常执行。
悲观锁死锁、饥饿和活锁它认为两个线程之间很有可能发生不幸的冲突,因此,以保护共享数据为第一优先级。synchronized,ReentrantLock
乐观锁死锁Deadlock它认为多个线程之间很有可能不会发生冲突,或者说这种概率不大。因此大家都应该无障碍的执行,在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。但是一旦检测到冲突,就应该进行回滚。atomic原子类

并发编程的三种场景

最初时为提高计算机的效率,当IO在等待时不让CPU空闲,于是就出现了分时操作系统,也就出现了【并发】。

后来多核CPU出现,不同的任务可以同时独立运行,于是就出现了并行【分工】。

有了分工后,效率得到了很大的提升,但是为了更合理的安排以及控制任务的进行,就需要让进程之间可以通信【同步】,让彼此知道进度的执行。

分工进行提高了效率,但是却带来了多线程访问共享资源会冲突的问题。于是对共享资源的访问又需要串行化。所以依据现实世界的做法,设计了锁等机制来使得多线程【互斥】访问共享资源。

分工-性能

如何高效拆解任务并分配给线程。

Java SDK并发包中的Executor、ForkJoin、Future本质上都是分工方法。

分工模型:

分工模式
生产者—消费者模式
MapReduce模式
Thread-Per-Message模式
Work Thread模式

同步-性能

线程间的协作。即当一个线程执行完了,该如何通知后续任务的线程展开工作。

线程协作问题都可以被描述为:当某个条件不满足时,线程需要等待,当某个条件满足使,线程需要被唤醒执行。

互斥-安全

互斥指的是:在同一时刻,只允许一个线程访问共享变量。

因为 可见性、有序性和原子性(后面会有文章介绍)问题,多个线程访问同一个共享变量会导致结果的不确定 。
为了解决这三个问题,Java语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则我们可以避免可见性问题、有序性问题,但是还不能完全解决线程安全问题。

解决线程安全问题的核心方案还是互斥。

实现互斥的核心技术就是锁。 Java语言中synchronized、SDK中的各种Lock都可以解决互斥问题,但是锁却会带来性能问题,于是我们就需要平衡。

主要方案有:分场景优化,优化读多写少场景:ReadWriteLock、StampledLock以及无锁结构Java SDK中的原子类;其他方案,原理为不共享变量或者变量只允许读,Java中提供了Thread Local和Final关键字和Copy-on-write 模式。

并发编程的三大挑战

上下文切换

Thread Context Switch

问题:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个 任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。 例如读英语书,查询英汉词典,遇到不认识的单词去查英汉词典前,需要记住這本英语书读到多少页多少行,好在查完词典后能够继续接着读這本英语书,这样的切换是会影响读书效率的。

解决方法:

1)无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同

2)CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
3)使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程样会造成大量线程都处于WAITING等待状态。
4)协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

死锁Deadlock

问题:几个线程都在等着对方先执行或者等待对方释放锁。

互斥条件

请求保持

不可剥夺

环路等待

解决方法:

1)避免一个线程同时获取多个锁。

2)避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

3)尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

4)对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

定位死锁用途
jps -l查看当前所有进程号
jstack 进程号查看进程内部的线程

资源限制

问题:程序的执行速度受限于计算机硬件资源或软件资源。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。

解决办法:

对于硬件资源限制,可以考虑使用集群并行执行程序。

对于软件资源限制,可以考虑使用资源池将资源复用。

进程:CPU的分时机制,一个CPU时只有一个程序在执行。
线程:进程的组成单元;

线程的执行是抢占式的。Java程序运行至少需要启动两个线程main与GC。

守护进程:保姆,提供便利服务,只要当前进程实例中存在任何一个非守护进程 没有结束,守护进程就在工作,只有当最后一个非守护进程结束时,守护进程才随着jvm一同结束工作。典型应用是GC

并发编程的三个问题

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

线程切换导致原子性

原子性:最小的操作单元。一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

1)银行账户转账

从账户 A 向账户 B 转 1000 元,那么必然包括 2 个操作:从账户 A 减去 1000 元,往账户 B 加上 1000 元。如果这 2 个操作不具备原子性,会造成什么样的后果。假如从账户 A 减去 1000 元之后,操作突然中止。然后又从 B 取出了 500 元,取出 500 元之后,再执行往账户 B 加上 1000 元 的操作。这样就会导致账户 A虽然减去了 1000 元,但是账户 B 没有收到这个转过来的 1000 元。所以这 2 个操作必须要具备原子性才能保证不出现一些意外的问题。

2)i++是原子性吗,为什么

不是。

保证原子性
synchronized关键字
lock锁
AtomicXxx 原子类

缓存导致可见性

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程 2 执行 j = i,它会先去主存读取i的值并加载到 CPU2 的缓存当中,注意此时内存当中i的值还是 0,那么就会使得 j 的值为 0,而不是 10。

这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立即看到线程 1 修改的值。

保证可见性
synchronized关键字
volatile关键字
final关键字

编译优化导致有序性

有序性:JVM进行代码编译时,不一定按照代码的先后顺序执行,有可能会指令重排(代码之间无依赖)。

保证有序性
synchronized关键字
volatile关键字

线程的状态

根据Thread.State线程状态枚举的源码,可以得知Java线程状态有6种。

在这里插入图片描述

请添加图片描述

1)NEW初始状态

新创建了一个线程对象,但还没有调用start()方法。

2)RUNNABLE运行状态

Runnable运行状态=Runnable就绪+Running运行

Runnable就绪:调用start()方法启动该线程。处于就绪状态,随时会被CPU调度执行。

Running运行:分配到时间片后开始运行,自动调用run()方法,执行线程体。

3)BLOCKED阻塞状态

线程因为某种原因放弃CPU使用权,暂时停止运行。阻塞的情况分三种:
(01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

4)WAITING等待

入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5)TIMED_WAITING超时等待

该状态不同于WAITING死死的等,它可以在指定的时间后自行返回。

6)TERMINATED终止状态

线程执行完run()方法,throws异常或者人为终止结束该线程生命周期。

终止线程的方法

(01)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

(02) 使用interrupt(打了一个停止标记,并不是真的停止线程)。interrupt+throw new interruptexception(推荐使用,catch块中还可以将异常向上抛,使线程停止的事件得以传播。)

interrupt+return

(03) stop()不推荐 。因为在stop()的时候会产生死锁,并且有可能使一些清理性的工作得不到完成 。

注:

方法用途作用线程
interrupt()给线程设置中断标志作用于调用该方法的对象所表示的线程
interrupted()检测中断并清除中断状态作用于当前线程
isInterrupted()检测中断作用于调用该方法的对象所表示的线程

实现并发编程

继承Thread类

//重写run方法
public void run(){
  //线程主体
}

//启动线程;有且只能调用一次start()方法启动线程。若调用多次会出现IllegalThreadStateException
对象.start(); 
//为什么我们调用 start() 方法时会执行 run() 方法?
//在运行状态中,首先调用start()方法,启动该线程进入就绪状态。分配到时间片后开始运行,自动调用run()方法,执行线程体。
方法用途
currentThread()获取正在运行的线程
getName()获取线程名
setName()设置线程名
getPriority()获取线程的优先级
setPriority()设置线程的优先级。范围1~10。10表示最高优先级,1表示最低优先级,5是普通优先级。线程的优先级的设定,应该在线程开始被调用之前。优先级高只代表获取CPU时概率更大。
isDaemon()测试该线程是否为后台守护线程。
setDaemon()将该线程设置为后台线程(守护线程或用户线程)。1.在start之前设置。2.设置不在运行状态的线程。
isAlive()测试该线程是否在活动,是true否false。
isInterrupted()测试该线程是否被中断,是true否false。但不清除状态标志。 不能中断在运行中的线程;它只能改变中断状态而已。
interrupt()测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能。 不能中断在运行中的线程;它只能改变中断状态而已。
Interrupted()恢复中断状态。
join()强制该线程运行,等待其死亡。
join(long millis)强制该线程运行millis毫秒后死亡。
sleep(long millis)使正在运行的线程休眠millis毫秒,该线程睡眠到期后不一定就开始执行,需要跟其他线程竞争cpu时。sleep(0)表示该线程与其他线程一起竞争
wait()当前线程等待
notify()唤醒等待的线程
notifyAll()唤醒所有等待的线程
yield()暂停当前正在执行的线程对象,并执行其他线程。
run()执行线程主体
start()开始执行线程

实现Runnable接口无返回值

重写run方法或者重写run方法,通过实现类Thread的构造函数,接收Runnable接口的子类对象,去调用start()方法启动线程。

public class ByImplementsRunnable {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new MyThreadByRunnable(), "thread-" + i).start();
        }
    }
}

/**
 * 通过实现Callable接口实现多线程
 */
class MyThreadByRunnable implements Runnable {

    /**
     * 重写call方法
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

Thread与Runnable的异同

相同Thread/Runnable
代理设计模式
区别ThreadRunnable
资源共享不能可以

实现Callable接口有返回值

执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程。

原理就是 futuretask传入callable 并重写了run方法 里面调用了 callable的call方法

public class ByImplementsCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 自定义线程类
        MyThreadByCallable myThread = new MyThreadByCallable();
        // 适配器
        FutureTask futureTask = new FutureTask(myThread);
        // 调用
        new Thread(futureTask,"thread-").start();
        // 获取Callable返回值,会产生阻塞
        String o = (String) futureTask.get();
    }
}

/**
 * 通过实现Callable接口实现多线程
 */
class MyThreadByCallable implements Callable<String> {

    /**
     * 重写call方法
     * @return
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        return "call";
    }
}
1.实现Callable<返回类型>接口

2.重写call方法

3.使用Future适配器
  FutureTask futureTask = new FutureTask();
  
  
  创建目标对象

4.创建执行服务:ExecutorService ser = Executor.newFixedThreadPool();

5.提交执行:Future<返回类型> result = ser.submit();

6.获取结果:返回类型 r = result.get();

7.关闭服务:ser.shutdownNow();

Runnable与Callable的异同

相同Runnable/Callable
都是单方法接口@FunctionalInterface修饰,可以用lambda表达式
都可以编写多线程程序
都采用Thread.start()启动线程
区别RunnableCallable
是否有返回值Runnable 接口 run 方法无返回值Callable 接口 call 方法有返回值(泛型,和Future、FutureTask配合使用)可获取异步执行的结果
是否可以捕获异常信息Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理Callable 接口 call 方法允许抛出异常,可以获取异常信息

线程池Executor

5.提交执行:
execute
submit

7.关闭关闭线程池
//将线程池的状态设置为 SHUTDOWN,然后中断所有没有执行的线程,无法再添加线程。
shutdown
//将线程池设置为 STOP,然后尝试停止 所有线程,并返回等待执行任务的列表。
shutdownNow

/**注:shutdown与shutdownNow
不同点:shutdown() 只结束未执行的任务;shutdownNow() 结束全部。
共同点:都是通过遍历线程池中的线程,逐个调用 Thread.interrup() 来中断线程,所以一些无法响应中断的任务可能永远无法停止(比如 Runnable)。
*/
池化思想特点
线程池降低资源消耗,提高响应速度,提高线程的可管理性
内存池(Memory Pooling)预先申请内存,提升申请内存速度,减少内存碎片。
连接池(Connection Pooling)预先申请数据库连接,提升申请连接的速度,降低系统的开销。
实例池(Object Pooling)循环使用对象,减少资源在初始化和释放时的昂贵损耗。

为什么要用线程池:

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

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

在这里插入图片描述

在这里插入图片描述

ThreadPoolExecutor自定义线程池
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
  this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
       Executors.defaultThreadFactory(), handler);
}
7大参数
参数说明
corePoolSize核心线程数线程池维护线程的最少数量,就算是空闲状态,也会保持该数量线程。
maximumPoolSize线程最大数线程池维护线程的最大数量,线程的增长始终不会超过该值。
keepAliveTime线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁
unitkeepAliveTime的时间单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS
workQueue阻塞队列Runnable任务的等待场所,该参数主要影响调度策略,如公平与否,是否产生饿死(starving)。
threadFactory线程工厂用于创建线程,一般使用默认。如果有自定义的需要则需要自己实现ThreadFactory接口并作为参数传入。
handler拒绝策略线程池对拒绝任务的处理策略。

maximumPoolSize线程最大数如何设置?

CPU 密集型IO密集型
特点需要进行大量计算、处理主要时间都在 I/O,CPU 空闲时间比较多
maximumPoolSizeCPU + 12 * CPU
原因避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况更高地压榨 CPU。
//CPU数
Runtime.getRuntime().availableProcessors();

workQueue工作队列

常见workQueue实现原理特点
ArrayBlockingQueue有界数组容量固定的有界阻塞队列
LinkedBlockingQueue有界单链表未指明容量时,容量默认为Integer.MAX_VALUE。
SynchronousQueue有界队列实现公平性的调度,栈实现非公平的调度内部容量为零,不能缓存数据,适合做交换数据用;
PriorityBlockingQueue有界优先队列

为了错误避免创建过多线程导致系统奔溃,建议使用有界队列。因为它在无法添加更多任务时会拒绝任务,这样可以提前预警,避免影响整个系统。

执行时间、顺序有要求的话可以选择优先级队列,同时也要保证低优先级的任务有机会被执行。

4大拒绝策略

1:通过实现RejectedExecutionHandler接口自定义拒绝策略方式。

2:ThreadPoolExecutor提供的四种拒绝策略

ThreadPoolExecutor拒绝策略方法特点
AbortPolicy丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。
DiscardPolicy丢弃任务,但是不抛出异常。
DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
CallerRunsPolicy由调用线程处理该任务
Executors线程工厂类

线程池不允许使用Executor去创建,而是通过ThreadPoolExecutor。

Executor去创建线程池对象的弊端:

1)请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,导致oom

2)可能会堆积大量的线程,导致oom

Executors线程工厂类应用场景
newCachedThreadPool()可伸缩的线程池用于并发执行大量短期的小任务,或者是负载较轻的服务器。
newFixedThreadPool()指定线程数量的线程池用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
newSingleThreadExecutor()单个线程用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
newScheduledThreadPoolExecutor()延时定时用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
newCachedThreadPool缓存型线程池

-缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中 -缓存型池子通常用于执行一些生存期很短的异步型任务 因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。 -能reuse的线程,必须是timeout IDLE内的池中线程,缺省 timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。 注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。

newFixedThreadPool固定大小线程池

与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程,即固定大小的线程池 -其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子 -和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器 -从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同: fixed池线程数固定,并且是0秒IDLE(无IDLE) cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE

newSingleThreadExecutor单线程线程池

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newScheduledThreadPool大小无限线程池

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程同步

互斥锁

volatile

可重入锁

有可能阻塞线程,即上下文切换。

自旋锁

不会有上下文切换。

Do {

}

While()

多个线程访问一个数据时,为了数据的安全性或者一致性。

可以通过加锁的方式解决多个线程资源共享问题。

同步可以解决资源共享同步问题,但是过多的同步会产生死锁。所以同步的范围越小越好

volatile 可见性共享变量

Java虚拟机提供的轻量级的synchronized。

特点:

1)保证可见性

2)不保证原子性

3)禁止指令重排序

如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。
保证可见性

可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

JMM Java内存模型

JMM定义了JVM在计算机内存(RAM)中的工作方式。

在这里插入图片描述

// 示例
private volatile boolean initFlag = false;

请添加图片描述

JMM原子操作用途特点
read读取从主内存读取数据
load加载将主内存的数据写入工作内存
use使用从工作内存读取数据计算
assign赋值将计算好的值重新赋值到工作内存中
store存储将工作内存的值传递到主内存
write写入将store传递的变量值写入主内存中的变量
lock加锁将主内存变量加锁,标记为线程独占状态
unlock解锁将主内存变量解锁,解锁后其他线程可以锁定该变量

JMM面临的问题

可见性:如何保证共享变量的副本变化后其他线程可见?

有序性:如何保证第一步到第六步按顺序执行?

原子性:如何保证第一步到第六步完整执行?

volatile关键字会开启总线的mesi缓存一致性协议

mesi缓存一致性协议:多个CPU从主内存读取同一个数据到各自到高速缓存L1,L2,L3,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据得变化从而将自己缓存里的数据失效。

流程如下:

请添加图片描述

1.线程2修改值后,会经过总线,然后才写回主内存。
2.volatile开启总线mesi缓存一致性协议,每个cpu 都会监听总线
3.当知道其他cpu修改了变量值,立刻会失效自己工作内存中得值。
4.重新去主内存取值。

第四步 重新去主内存取值,怎么保障读取是最新得值呢?

汇编LOCK前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。

1.CPU会将当前处理器缓存行的数据立即写回到系统内存
2.这个写回内存得操作会引起在其他CPU里缓存了该内存地址得数据无效(MESI协议)

不保证原子性

num++不是原子性,底层是三步:获取值,+1,设置值。

保证原子性
synchronized关键字
lock锁
AtomicXxx 原子类
禁止指令重排序

内存屏障。cpu指令。

synchronized 互斥锁

互斥锁 /悲观锁/独占锁/排他锁/可重入锁/重量级锁

synchronized是 JVM 层面实现的自动加锁和自动释放锁的同步锁。

当前线程如果获取到锁,会导致其它所有需要锁该的线程等待,一直等待持有锁的线程释放锁才继续进行锁的争抢。

对象锁

有多少个对象就有相对应的多少把锁。

修饰普通方法
访问权限 synchronized 方法返回值 方法名(参数列表){
  
}
修饰代码块
synchronized(this|object){
    //代码块
}
this:this;.this。
object:new
类锁/全局锁

不管有多少个对象都共用一把锁。

修饰静态方法
访问权限 synchronized static 方法返回值 方法名(参数列表){
    //同步方法体
} 
锁升级

在 JDK1.5 (含)之前, synchronized 的底层实现是重量级的,所以之前一致称呼它为"重量级锁",在 JDK1.5 之后,对 synchronized 进行了各种优化,它变得不那么重了,实现原理就是锁升级的过程。

Java 对象内存模型

在这里插入图片描述

对象头区域

1、MarkWord对象自身的运行时数据

锁状态nbit存储内容1bit偏向锁标志位2bit锁标志位
无锁hashCode、分代年龄001
偏向锁当前线程ID、分代年龄101
轻量级/自旋锁指向线程栈中 锁记录Lock Record ( LR ) 的指针00
重量级锁指向对象监视器monitor的指针10
GC标记11

2、Class Pointer对象类型的指针

对象指向它的类元数据的指针、 JVM 就是通过它来确定是哪个 Class 的实例。

实例数据区域

此处存储的是对象真正有效的信息,比如对象中所有字段的内容

对齐填充区域

JVM 的实现 HostSpot 规定对象的起始地址必须是 8 字节的整数倍,换句话来说,现在 64 位的 OS 往外读取数据的时候一次性读取 64bit 整数倍的数据,也就是 8 个字节,所以 HotSpot 为了高效读取对象,就做了"对齐",如果一个对象实际占的内存大小不是 8byte 的整数倍时,就"补位"到 8byte 的整数倍。所以对齐填充区域的大小不是固定的。

锁状态锁升级
无锁
偏向锁有且只有一个线程无竞争-同一时刻最多存在一个线程申请获取锁markword
轻量级线程之间存在伪竞争无竞争-同一时刻绝对不会存在多个线程申请获取锁,自旋获取锁cas自旋失败则升级为重量级锁
重量级锁线程之间存在实质性竞争

在这里插入图片描述

无锁
偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

在 JDK1.8 中,其实默认是轻量级锁,但如果设定了 -XX:BiasedLockingStartupDelay = 0 ,那在对一个 Object 做 syncronized 的时候,会立即上一把偏向锁。当处于偏向锁状态时, markwork 会记录当前线程 ID 。

偏向锁回退到无锁
无锁升级到轻量级锁

当下一个线程参与到偏向锁竞争时,会先判断 markword 中保存的线程 ID 是否与这个线程 ID 相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁。

每个线程在自己的线程栈中生成一个 Lock Record ( LR ),然后每个线程通过 CAS自旋的操作将锁对象头中的 markwork 设置为指向自己的 LR 的指针,哪个线程设置成功,就意味着获得锁。

轻量级锁升级到重量级锁

如果锁竞争加剧(如线程自旋次数或者自旋的线程数超过某阈值, JDK1.6 之后,由 JVM 自己控制该规则),就会升级为重量级锁。

需要做用户态到内核态的转换,而这个过程中需要消耗较多时间,也就是"重"的原因之一。

JDK1.6之前自旋锁spin lock

自身循环尝试获取锁。

优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

但是,如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。

自旋次数 > 最大值 || 自旋的线程数>cpu/2。JDK1.6之前这两个值之前都是可以设置的。

JDK1.6之后自适应自旋锁

jvm自动管理。

线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。

比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少或者不自选,避免CPU空转。

JUC并发包

几乎 java.util.concurrent 中的所有类都是在 ReentrantLock 之上构建的,ReentrantLock 则是在原子变量类的基础上构建的。所以,虽然仅少数并发专家使用原子变量类,但 java.util.concurrent 类的很多可伸缩性改进都是由它们提供的。

在这里插入图片描述

JUC源码目录
java.util.concurrent.atomic原子类
java.util.concurrent.lockslock锁
java.util.concurrent
CyclicBarrier加法计数器
CountDownLatch减法计数器

CountDownLatch允许一个或者多个线程去等待其他线程完成操作。CountDownLatch构造器接收一个int型参数,表示要等待的工作线程的个数。当然也不一定是多线程,在单线程中可以用这个int型参数表示多个操作步骤。

给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后所有await的线程都将被唤醒。

对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

方法说明
await()使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。
await(long timeout, TimeUnit unit)带超时时间的await()。
countDown()使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。
getCount()获得latch的数值。
Semaphore信号量

设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

ForkJoin分支合并

实现:递归

大数据量计算

ForkJoinPool
ForkJoinTask
CompletableFuture异步回调
//发起一个异步请求
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(()->{
    System.out.println(Thread.currentThread().getName());
});
// 获取阻塞执行结果
runAsync.get();
//发起一个异步请求,有返回值
CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(()->{
    System.out.println(Thread.currentThread().getName());
    return 1024;
});
supplyAsync.get();
集合线程安全
//方式一,使用Collections工具类添加线程安全关键字
Collections.synchronizedXxx(new 集合类());

//方式二:使用juc包中的集合类
CopyOnWriteArrayList写时复制List
CopyOnWriteArraySet写时复制Set
ConcurrentHashMap写时复制Map

BlockingQueue阻塞队列
为空或已满抛出异常不抛出异常阻塞等待超时等待
队尾插入add()offer()put()offer(E e, long timeout, TimeUnit unit)
获取队首并删除remove()poll()take()poll(long timeout, TimeUnit unit)
获取队首element()peek()
ArrayBlockingQueue阻塞队列

指定size大小

LinkedBlockingQueue阻塞队列
SynchronousQueue同步队列

无需指定size大小,因为不会存储元素,put()一个元素进去,必须take()取出来后,才能在put其他元素进去。

LinkedBlockingDeque双端阻塞队列
Atomic 原子类

底层是CAS。在内存中修改值。

java是无法操作内存的,Java如何操作内存。

1)native关键字。可以通过调用c++,让c++去操作内存。

2)Unsafe类

CAS比较再设置算法

英文全称:Compare and Swap

实现原理:volatile 变量 + unsafe.compareAndSwapInt();

假设有三个操作数: 内存值V、预期原始值A、新值B。

当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false 。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

CAS是乐观锁。因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是则更新,如果不是则自旋无限循环执行,直到当前值与更新前的值一致为止才执行更新。

在这里插入图片描述

CAS能很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

ABA 问题:

1)使用版本号。AtomicStampedReference

2)使用Boolean来标记有没有改动过。AtomicMarkableReference

循环时间长开销大问题:

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

只能保证一个共享变量的原子操作问题:

多个共享变量合并成一个共享变量来操作。AtomicReference

AtomicStampedReference版本号原子引用

引用变量被修改了几次

// 核心:版本号
final int stamp;
public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        // Integer [-128,128)
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(127,1);

        System.out.println(atomicStampedReference.compareAndSet(127, -128,
                atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

        System.out.println(atomicStampedReference.compareAndSet(-128, 127,
                atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

    }
}
AtomicMarkableReference布尔标记原子引用

不关心引用变量被修改了几次,只是单纯的关心是否更改过

// 核心:布尔标记是否被修改
final boolean mark;
AtomicReference引用原子性

CAS只能保证一个共享变量的原子操作问题。

AtomicReference可以将多个共享变量合并成一个共享变量来操作。

LongAdder

在高并发的情况下,我们对一个 Integer 类型的整数直接进行 i++ 的时候,无法保证操作的原子性,会出现线程安全的问题。为此我们会用 juc 下的 AtomicInteger ,它是一个提供原子操作的 Interger 类,内部也是通过 CAS 实现线程安全的。但当大量线程同时去访问时,就会因为大量线程执行 CAS 操作失败而进行空旋转,导致 CPU 资源消耗过多,而且执行效率也不高。 Doug Lea 大神应该也不满意,于是在 JDK1.8 中对 CAS 进行了优化,提供了 LongAdder ,它是基于了 CAS 分段锁的思想实现的。

线程去读写一个 LongAdder 类型的变量时,流程如下:

在这里插入图片描述

LongAdder 也是基于 Unsafe 提供的 CAS 操作 +valitale 去实现的。在 LongAdder 的父类 Striped64 中维护着一个 base 变量和一个 cell 数组,当多个线程操作一个变量的时候,先会在这个 base 变量上进行 cas 操作,当它发现线程增多的时候,就会使用 cell 数组。比如当 base 将要更新的时候发现线程增多(也就是调用 casBase 方法更新 base 值失败),那么它会自动使用 cell 数组,每一个线程对应于一个 cell ,在每一个线程中对该 cell 进行 cas 操作,这样就可以将单一 value 的更新压力分担到多个 value 中去,降低单个 value 的 “热度”,同时也减少了大量线程的空转,提高并发效率,分散并发压力。这种分段锁需要额外维护一个内存空间 cells ,不过在高并发场景下,这点成本几乎可以忽略。分段锁是一种优秀的优化思想, juc 中提供的的 ConcurrentHashMap 也是基于分段锁保证读写操作的线程安全。

Lock 可重入锁

可重入锁/递归锁。

利用 AQS 的的 state 状态来判断资源是否已锁,同一线程重入加锁, state 的状态 +1 ; 同一线程重入解锁, state 状态 -1 (解锁必须为当前独占线程,否则异常); 当 state 为 0 时解锁成功。

//实现Lock可重入锁

//step1 创建Lock对象
Lock lock = new ReentrantLock();

//step2 加锁
lock.lock();
try {
  //业务代码
} finally {
  //step3 解锁
  lock.unlock();
}

为何在try外加锁?

  1. 如果没有获取到锁就抛出异常,最终释放锁肯定是有问题的,因为还未曾拥有锁谈何释放锁呢
  2. 如果在获取锁时抛出了异常,也就是当前线程并未获取到锁,但执行到 finally 代码时,如果恰巧别的线程获取到了锁,则会被释放掉(无故释放)
synchronizedLock
锁的实现JVM实现JDK实现
加锁解锁自动手动。lock() 和 unlock() 方法配合 try/finally 语句块
锁的状态无法判断可以判断是否获取到锁
设置锁的超时时间无法设置。如果一个获得锁的线程内部发生死锁,那么其他线程就会一直进入阻塞状态tryLock 方法。如果超时则跳过,不进行任何操作,避免死锁的发生。
可重入同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。利用 AQS 的的 state 状态来判断资源是否已锁,同一线程重入加锁, state 的状态 +1 ; 同一线程重入解锁, state 状态 -1 (解锁必须为当前独占线程,否则异常); 当 state 为 0 时解锁成功。
公平锁非公平锁构造方法中设置 true/false 来实现公平与非公平锁。
如果设置为 true ,则线程获取锁要遵循"先来后到"的规则,每次都会构造一个线程 Node ,然后到双向链表的"尾巴"后面排队,等待前面的 Node 释放锁资源。
可中断锁不可中断lockInterruptibly() 方法使得线程可以在被阻塞时响应中断,比如一个线程 t1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间的任务,另一个线程通过 interrupt() 方法就可以立刻打断 t1 线程的执行,来获取t1持有的那个可重入锁。
特性描述API
能响应中断如果不能自己释放,那可以响应中断也是很好的。Java多线程中断机制 专门描述了中断过程,目的是通过中断信号来跳出某种状态,比如阻塞lockInterruptbly()
非阻塞式的获取锁尝试获取,获取不到不会阻塞,直接返回tryLock()
支持超时给定一个时间限制,如果一段时间内没获取到,不是进入阻塞状态,同样直接返回tryLock(long time, timeUnit)
AbstractQueuedSynchronizer队列同步器

在这里插入图片描述

AQS

AQS = volitale + CAS。

AQS 中维护一个 valitale 类型的变量 state 来做一个可重入锁的重入次数,加锁和释放锁也是围绕这个变量来进行的。

一个是获取,一个是释放。获取释放的时候该有一个东西来记住他是被用还是没被用,这个东西就是一个状态。如果锁被获取了,也就是被用了,还有很多其他的要来获取锁,总不能给全部拒绝了,这时候就需要他们排队,这里就需要一个队列。

AQS的核心思想是:通过一个volatile修饰的int属性state代表同步状态,例如0是无锁状态,1是上锁状态。多线程竞争资源时,通过CAS的方式来修改state,例如从0修改为1,修改成功的线程即为资源竞争成功的线程,将其设为exclusiveOwnerThread,也称【工作线程】,资源竞争失败的线程会被放入一个FIFO的队列中并挂起休眠,当exclusiveOwnerThread线程释放资源后,会从队列中唤醒线程继续工作,循环往复。

在这里插入图片描述

ReentrantLock可重入互斥锁

ReentrantLock 从字面可以看出是一把可重入锁,这点和 synchronized 一样,但实现原理也与 syncronized 有很大差别,它是基于经典的 AQS(AbstractQueueSyncronized) 实现的, AQS 是基于 volitale 和 CAS 实现的,其中 AQS 中维护一个 valitale 类型的变量 state 来做一个可重入锁的重入次数,加锁和释放锁也是围绕这个变量来进行的。 ReentrantLock 也提供了一些 synchronized 没有的特点,因此比 synchronized 好用。

public class ReentrantLock implements Lock, java.io.Serializable { 
    public ReentrantLock() {
        sync = new NonfairSync();
    }

		//设置悲观锁,乐观锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}
ReentrantLock方法作用
lock()获得锁,如果锁被占用,进入等待。
lockInterruptibly()获得锁,但优先响应中断。
unLock()释放锁
newLondition()
getHoldCount ()当前线程调用 lock() 方法的次数
getQueueLength ()当前正在等待获取 Lock 锁的线程的估计数
getWaitQueueLength(Condition condition)当前正在等待状态的线程的估计数,需要传入 Condition 对象
hasWaiters(Condition condition)查询是否有线程正在等待与 Lock 锁有关的 Condition 条件
hasQueuedThread(Thread thread)查询指定的线程是否正在等待获取 Lock 锁
hasQueuedThreads()查询是否有线程正在等待获取此锁定
isFair()判断当前 Lock 锁是不是公平锁
isHeldByCurrentThread()查询当前线程是否保持此锁定
isLocked()查询此锁定是否由任意线程保持
tryLock()线程尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false
tryLock(long timeout,TimeUnit unit)线程如果在指定等待时间内获得了锁,就返回true,否则返回 false
lockInterruptibly()如果当前线程未被中断,则获取该锁定,如果已经被中断则出现异常
ReentrantReadWriteLock可重入读写锁

维护一对关联的locks,一个用于读readLock,一个用于写writeLock。 读可以被多线程同时读,写只能有一个线程写。

/**
 * 读写锁
 * 独占锁:写锁,只能一个线程去写
 * 共享锁:读锁,多个线程可同时读
 * @author liubo
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp, temp);
            }).start();
        }

        for (int i = 0; i < 10; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp);
            }).start();
        }
    }

}

class MyCache {
    private volatile Map<Integer, Object> map = new HashMap<>();

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 写,只能一个线程去写
     *
     * @param key
     * @param value
     */
    public void put(Integer key, Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 读,多个线程可同时读
     *
     * @param key
     */
    public Object get(Integer key) {
        Object o = null;
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始读取" + key);
            o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完毕" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
        return o;
    }
}

ReentrantReadWriteLock (读写锁)其实是两把锁,一把是 WriteLock (写锁),一把是读锁, ReadLock 。读写锁的规则是:读读不互斥、读写互斥、写写互斥。在一些实际的场景中,读操作的频率远远高于写操作,如果直接用一般的锁进行并发控制的话,就会读读互斥、读写互斥、写写互斥,效率低下,读写锁的产生就是为了优化这种场景的操作效率。一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高,因此需要根据实际情况选择使用。

ReentrantReadWriteLock 的原理也是基于 AQS 进行实现的,与 ReentrantLock 的差别在于 ReentrantReadWriteLock 锁拥有共享锁、排他锁属性。读写锁中的加锁、释放锁也是基于 Sync (继承于 AQS ),并且主要使用 AQS 中的 state 和 node 中的 waitState 变量进行实现的。实现读写锁与实现普通互斥锁的主要区别在于需要分别记录读锁状态及写锁状态,并且等待队列中需要区别处理两种加锁操作。 ReentrantReadWriteLock 中将 AQS 中的 int 类型的 state 分为高 16 位与第 16 位分别记录读锁和写锁的状态,如下图所示:

在这里插入图片描述

WriteLock(写锁)是悲观锁(排他锁、互斥锁)

通过计算 state&((1<<16)-1) ,将 state 的高 16 位全部抹去,因此 state 的低位记录着写锁的重入计数。

获取写锁源码:

public class WriteLock {
	/**
	 * 获取写锁 Acquires the write lock. 如果此时没有任何线程持有写锁或者读锁,那么当前线程执行CAS操作更新status,
	 * 若更新成功,则设置读锁重入次数为1,并立即返回
	 * <p>
	 * Acquires the write lock if neither the read nor write lock are held by
	 * another thread and returns immediately, setting the write lock hold count to
	 * one. 如果当前线程已经持有该写锁,那么将写锁持有次数设置为1,并立即返回
	 * <p>
	 * If the current thread already holds the write lock then the hold count is
	 * incremented by one and the method returns immediately.
	 * 如果该锁已经被另外一个线程持有,那么停止该线程的CPU调度并进入休眠状态, 直到该写锁被释放,且成功将写锁持有次数设置为1才表示获取写锁成功
	 * <p>
	 * If the lock is held by another thread then the current thread becomes
	 * disabled for thread scheduling purposes and lies dormant until the write lock
	 * has been acquired, at which time the write lock hold count is set to one.
	 */
	public void lock() {
		sync.acquire(1);
	}

	/**
	 * 该方法为以独占模式获取锁,忽略中断 如果调用一次该“tryAcquire”方法更新status成功,则直接返回,代表抢锁成功
	 * 否则,将会进入同步队列等待,不断执行“tryAcquire”方法尝试CAS更新status状态,直到成功抢到锁
	 * 其中“tryAcquire”方法在NonfairSync(公平锁)中和FairSync(非公平锁)中都有各自的实现
	 *
	 * Acquires in exclusive mode, ignoring interrupts. Implemented by invoking at
	 * least once {@link #tryAcquire}, returning on success. Otherwise the thread is
	 * queued, possibly repeatedly blocking and unblocking, invoking
	 * {@link #tryAcquire} until success. This method can be used to implement
	 * method {@link Lock#lock}.
	 *
	 * @param arg the acquire argument. This value is conveyed to
	 *            {@link #tryAcquire} but is otherwise uninterpreted and can
	 *            represent anything you like.
	 */
	public final void acquire(int arg) {
		if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
			selfInterrupt();
	}

	protected final boolean tryAcquire(int acquires) {
		/*
		 * Walkthrough: 1、如果读写锁的计数不为0,且持有锁的线程不是当前线程,则返回false 1. If read count nonzero or
		 * write count nonzero and owner is a different thread, fail.
		 * 2、如果持有锁的计数不为0且计数总数超过限定的最大值,也返回false 2. If count would saturate, fail. (This
		 * can only happen if count is already nonzero.)
		 * 3、如果该锁是可重入或该线程在队列中的策略是允许它尝试抢锁,那么该线程就能获取锁 3. Otherwise, this thread is
		 * eligible for lock if it is either a reentrant acquire or queue policy allows
		 * it. If so, update state and set owner.
		 */
		Thread current = Thread.currentThread();
		// 获取读写锁的状态
		int c = getState();
		// 获取该写锁重入的次数
		int w = exclusiveCount(c);
		// 如果读写锁状态不为0,说明已经有其他线程获取了读锁或写锁
		if (c != 0) {
			// 如果写锁重入次数为0,说明有线程获取到读锁,根据“读写锁互斥”原则,返回false
			// 或者如果写锁重入次数不为0,且获取写锁的线程不是当前线程,根据"写锁独占"原则,返回false
			// (Note: if c != 0 and w == 0 then shared count != 0)
			if (w == 0 || current != getExclusiveOwnerThread())
				return false;
			// 如果写锁可重入次数超过最大次数(65535),则抛异常
			if (w + exclusiveCount(acquires) > MAX_COUNT)
				throw new Error("Maximum lock count exceeded");
			// 到这里说明该线程是重入写锁,更新重入写锁的计数(+1),返回true
			// Reentrant acquire
			setState(c + acquires);
			return true;
		}
		// 如果读写锁状态为0,说明读锁和写锁都没有被获取,会走下面两个分支:
		// 如果要阻塞或者执行CAS操作更新读写锁的状态失败,则返回false
		// 如果不需要阻塞且CAS操作成功,则当前线程成功拿到锁,设置锁的owner为当前线程,返回true
		if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
			return false;
		setExclusiveOwnerThread(current);
		return true;
	}
}

释放写锁源码:

public class WriteLock {
	/*
	 * Note that tryRelease and tryAcquire can be called by Conditions. So it is
	 * possible that their arguments contain both read and write holds that are all
	 * released during a condition wait and re-established in tryAcquire.
	 */
	protected final boolean tryRelease(int releases) {
		// 若锁的持有者不是当前线程,抛出异常
		if (!isHeldExclusively())
			throw new IllegalMonitorStateException();
		// 写锁的可重入计数减掉releases个
		int nextc = getState() - releases;
		// 如果写锁重入计数为0了,则说明写锁被释放了
		boolean free = exclusiveCount(nextc) == 0;
		if (free)
			// 若写锁被释放,则将锁的持有者设置为null,进行GC
			setExclusiveOwnerThread(null);
		// 更新写锁的重入计数
		setState(nextc);
		return free;
	}
}

ReadLock(读锁)是共享锁(乐观锁)

通过计算 state>>>16 进行无符号补 0 ,右移 16 位,因此 state 的高位记录着写锁的重入计数.

读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为 0 并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程 firstReader 和 firstReaderHoldCount ;若当前线程线程为第一个读线程,则增加 firstReaderHoldCount ;否则,将设置当前线程对应的 HoldCounter 对象的值,更新成功后会在 firstReaderHoldCount 中 readHolds ( ThreadLocal 类型的)的本线程副本中记录当前线程重入数,这是为了实现 JDK1.6 中加入的 getReadHoldCount ()方法的,这个方法能获取当前线程重入共享锁的次数( state 中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用 ThreadLocal ,直接往 firstReaderHoldCount 这个成员变量里存重入数,当有第二个线程来的时候,就要动用 ThreadLocal 变量 readHolds 了,每个线程拥有自己的副本,用来保存自己的重入数。

获取读锁源码:

public class ReadLock {
	/**
	 * 获取读锁 Acquires the read lock. 如果写锁未被其他线程持有,执行CAS操作更新status值,获取读锁后立即返回
	 * <p>
	 * Acquires the read lock if the write lock is not held by another thread and
	 * returns immediately.
	 *
	 * 如果写锁被其他线程持有,那么停止该线程的CPU调度并进入休眠状态,直到该读锁被释放
	 * <p>
	 * If the write lock is held by another thread then the current thread becomes
	 * disabled for thread scheduling purposes and lies dormant until the read lock
	 * has been acquired.
	 */
	public void lock() {
		sync.acquireShared(1);
	}

	/**
	 * 该方法为以共享模式获取读锁,忽略中断 如果调用一次该“tryAcquireShared”方法更新status成功,则直接返回,代表抢锁成功
	 * 否则,将会进入同步队列等待,不断执行“tryAcquireShared”方法尝试CAS更新status状态,直到成功抢到锁
	 * 其中“tryAcquireShared”方法在NonfairSync(公平锁)中和FairSync(非公平锁)中都有各自的实现
	 * (看这注释是不是和写锁很对称) Acquires in shared mode, ignoring interrupts. Implemented by
	 * first invoking at least once {@link #tryAcquireShared}, returning on success.
	 * Otherwise the thread is queued, possibly repeatedly blocking and unblocking,
	 * invoking {@link #tryAcquireShared} until success.
	 *
	 * @param arg the acquire argument. This value is conveyed to
	 *            {@link #tryAcquireShared} but is otherwise uninterpreted and can
	 *            represent anything you like.
	 */
	public final void acquireShared(int arg) {
		if (tryAcquireShared(arg) < 0)
			doAcquireShared(arg);
	}

	protected final int tryAcquireShared(int unused) {
		/*
		 * Walkthrough: 1、如果已经有其他线程获取到了写锁,根据“读写互斥”原则,抢锁失败,返回-1 1.If write lock held by
		 * another thread, fail. 2、如果该线程本身持有写锁,那么看一下是否要readerShouldBlock,如果不需要阻塞,
		 * 则执行CAS操作更新state和重入计数。 这里要注意的是,上面的步骤不检查是否可重入(因为读锁属于共享锁,天生支持可重入) 2. Otherwise,
		 * this thread is eligible for lock wrt state, so ask if it should block because
		 * of queue policy. If not, try to grant by CASing state and updating count.
		 * Note that step does not check for reentrant acquires, which is postponed to
		 * full version to avoid having to check hold count in the more typical
		 * non-reentrant case. 3、如果因为CAS更新status失败或者重入计数超过最大值导致步骤2执行失败
		 * 那就进入到fullTryAcquireShared方法进行死循环,直到抢锁成功 3. If step 2 fails either because
		 * thread apparently not eligible or CAS fails or count saturated, chain to
		 * version with full retry loop.
		 */

		// 当前尝试获取读锁的线程
		Thread current = Thread.currentThread();
		// 获取该读写锁状态
		int c = getState();
		// 如果有线程获取到了写锁 ,且获取写锁的不是当前线程则返回失败
		if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
			return -1;
		// 获取读锁的重入计数
		int r = sharedCount(c);
		// 如果读线程不应该被阻塞,且重入计数小于最大值,且CAS执行读锁重入计数+1成功,则执行线程重入的计数加1操作,返回成功
		if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
			// 如果还未有线程获取到读锁,则将firstReader设置为当前线程,firstReaderHoldCount设置为1
			if (r == 0) {
				firstReader = current;
				firstReaderHoldCount = 1;
			} else if (firstReader == current) {
				// 如果firstReader是当前线程,则将firstReader的重入计数变量firstReaderHoldCount加1
				firstReaderHoldCount++;
			} else {
				// 否则说明有至少两个线程共享读锁,获取共享锁重入计数器HoldCounter
				// 从HoldCounter中拿到当前线程的线程变量cachedHoldCounter,将此线程的重入计数count加1
				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;
		}
		// 如果上面的if条件有一个都不满足,则进入到这个方法里进行死循环重新获取
		return fullTryAcquireShared(current);
	}

	/**
	 * 用于处理CAS操作state失败和tryAcquireShared中未执行获取可重入锁动作的full方法(补偿方法?) Full version of
	 * acquire for reads, that handles CAS misses and reentrant reads not dealt with
	 * in tryAcquireShared.
	 */
	final int fullTryAcquireShared(Thread current) {
		/*
		 * 此代码与tryAcquireShared中的代码有部分相似的地方,
		 * 但总体上更简单,因为不会使tryAcquireShared与重试和延迟读取保持计数之间的复杂判断 This code is in part
		 * redundant with that in tryAcquireShared but is simpler overall by not
		 * complicating tryAcquireShared with interactions between retries and lazily
		 * reading hold counts.
		 */
		HoldCounter rh = null;
		// 死循环
		for (;;) {
			// 获取读写锁状态
			int c = getState();
			// 如果有线程获取到了写锁
			if (exclusiveCount(c) != 0) {
				// 如果获取写锁的线程不是当前线程,返回失败
				if (getExclusiveOwnerThread() != current)
					return -1;
				// else we hold the exclusive lock; blocking here
				// would cause deadlock.
			} else if (readerShouldBlock()) {// 如果没有线程获取到写锁,且读线程要阻塞
				// Make sure we're not acquiring read lock reentrantly
				// 如果当前线程为第一个获取到读锁的线程
				if (firstReader == current) {
					// assert firstReaderHoldCount > 0;
				} else { // 如果当前线程不是第一个获取到读锁的线程(也就是说至少有有一个线程获取到了读锁)
					//
					if (rh == null) {
						rh = cachedHoldCounter;
						if (rh == null || rh.tid != getThreadId(current)) {
							rh = readHolds.get();
							if (rh.count == 0)
								readHolds.remove();
						}
					}
					if (rh.count == 0)
						return -1;
				}
			}
			/**
			 * 下面是既没有线程获取写锁,当前线程又不需要阻塞的情况
			 */
			// 重入次数等于最大重入次数,抛异常
			if (sharedCount(c) == MAX_COUNT)
				throw new Error("Maximum lock count exceeded");
			// 如果执行CAS操作成功将读写锁的重入计数加1,则对当前持有这个共享读锁的线程的重入计数加1,然后返回成功
			if (compareAndSetState(c, c + SHARED_UNIT)) {
				if (sharedCount(c) == 0) {
					firstReader = current;
					firstReaderHoldCount = 1;
				} else if (firstReader == current) {
					firstReaderHoldCount++;
				} else {
					if (rh == null)
						rh = cachedHoldCounter;
					if (rh == null || rh.tid != getThreadId(current))
						rh = readHolds.get();
					else if (rh.count == 0)
						readHolds.set(rh);
					rh.count++;
					cachedHoldCounter = rh; // cache for release
				}
				return 1;
			}
		}
	}
}

释放读锁源码:

public class ReadLock {

	/**
	 * Releases in shared mode. Implemented by unblocking one or more threads if
	 * {@link #tryReleaseShared} returns true.
	 *
	 * @param arg the release argument. This value is conveyed to
	 *            {@link #tryReleaseShared} but is otherwise uninterpreted and can
	 *            represent anything you like.
	 * @return the value returned from {@link #tryReleaseShared}
	 */
	public final boolean releaseShared(int arg) {
		if (tryReleaseShared(arg)) {// 尝试释放一次共享锁计数
			doReleaseShared();// 真正释放锁
			return true;
		}
		return false;
	}

	/**
	 * 此方法表示读锁线程释放锁。 首先判断当前线程是否为第一个读线程firstReader,
	 * 若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,
	 * 若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;
	 * 若当前线程不是第一个读线程, 那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),
	 * 若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器, 如果计数器的计数count小于等于1,则移除当前线程对应的计数器,
	 * 如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。 无论何种情况,都会进入死循环,该循环可以确保成功设置状态state
	 */
	protected final boolean tryReleaseShared(int unused) {
		// 获取当前线程
		Thread current = Thread.currentThread();
		if (firstReader == current) { // 当前线程为第一个读线程
			// assert firstReaderHoldCount > 0;
			if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
				firstReader = null;
			else // 减少占用的资源
				firstReaderHoldCount--;
		} else { // 当前线程不为第一个读线程
			// 获取缓存的计数器
			HoldCounter rh = cachedHoldCounter;
			if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
				// 获取当前线程对应的计数器
				rh = readHolds.get();
			// 获取计数
			int count = rh.count;
			if (count <= 1) { // 计数小于等于1
				// 移除
				readHolds.remove();
				if (count <= 0) // 计数小于等于0,抛出异常
					throw unmatchedUnlockException();
			}
			// 减少计数
			--rh.count;
		}
		for (;;) { // 死循环
			// 获取状态
			int c = getState();
			// 获取状态
			int nextc = c - SHARED_UNIT;
			if (compareAndSetState(c, nextc)) // 比较并进行设置
				// Releasing the read lock has no effect on readers,
				// but it may allow waiting writers to proceed if
				// both read and write locks are now free.
				return nextc == 0;
		}
	}

	/**
	 * 真正释放锁 Release action for shared mode -- signals successor and ensures
	 * propagation. (Note: For exclusive mode, release just amounts to calling
	 * unparkSuccessor of head if it needs signal.)
	 */
	private void doReleaseShared() {
		/*
		 * Ensure that a release propagates, even if there are other in-progress
		 * acquires/releases. This proceeds in the usual way of trying to
		 * unparkSuccessor of head if it needs signal. But if it does not, status is set
		 * to PROPAGATE to ensure that upon release, propagation continues.
		 * Additionally, we must loop in case a new node is added while we are doing
		 * this. Also, unlike other uses of unparkSuccessor, we need to know if CAS to
		 * reset status fails, if so rechecking.
		 */
		for (;;) {
			Node h = head;
			if (h != null && h != tail) {
				int ws = h.waitStatus;
				if (ws == Node.SIGNAL) {
					if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
						continue; // loop to recheck cases
					unparkSuccessor(h);
				} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
					continue; // loop on failed CAS
			}
			if (h == head) // loop if head changed
				break;
		}
	}
}

通过分析可以看出:

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

锁优化

减少锁持有时间

加锁的范围尽可能小

减小锁粒度

将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。

最最典型的减小锁粒度的案例就是ConcurrentHashMap。

锁消除lock eliminate

无用的锁进行删除

StringBuffer sb = new StringBuffer();
sb.append(1).append(2).append(3)....append(n)

虚拟机通过逃逸分析,判断对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。

锁粗化lock coarsening

连续使用做合并

虚拟机探测到有一串零碎操作都是对同一对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。按上述例子是在第一个和最后一个append加锁。

锁分离

最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥。即保证了线程安全,又提高了性能。

ThreadLocal 线程局部变量

1)保存线程上下文信息,在任意需要的地方可以获取

2)线程安全,线程数据隔离

用途:数据库连接、Session管理。

每一个Thread类包含一个Map,每一个Map的key对应一个ThreadLocal

ThreadLocalMapkeyvalue
数组实现。
开放地址法来解决hash 冲突。
内部类Entry中的key是弱引用,value 是强引用。
当前ThreadLocal对象要存放的值

解决内存泄漏

thread→threadmap→entry→value

经典的同步问题

生产者-消费者
有两个进程:制造商和消费者,共享一个固定大小的缓存。制造商的工作是制造一段数据,放进缓存,如此反复。同时,消费者则一次消费一段数据(即将其从缓存中移出)。问题的核心就是要保证不让制造商在缓存还是满的时候仍然要向内写数据,不让消费者试图从空的缓存中取出数据。
制造商的方案是:如果缓存是满的就去睡觉。消费者从缓存中取走数据后就叫醒制造商,让它再次将缓存填满。这样,消费者发现缓存是空的,它就去睡觉了。下一轮中制造商将数据写入后就叫醒消费者。通过使用“进程间通信”就可以解决这个问题,人们一般使用“信号标”(semaphore)。不完善的解决方案会造成“死锁”,即两个进程都在“睡觉”等着对方来“唤醒”。

解决步骤:

  • 循环等待
  • 业务
  • 唤醒
/**
 * 生产者消费者
 * 循环等待
 * 业务
 * 唤醒
 * @author liubo
 */
public class ProducerConsumer {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

class Data{
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        //STEP1 循环等待
        //不能使用if判断,会导致虚假唤醒,需要使用while循环
//        if (number!=0) {
//            this.wait();
//        }
        while (number!=0) {
            this.wait();
        }

        //STEP2 业务
        number++;
        System.out.println(Thread.currentThread().getName()+number);

        //STEP3 通知
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        //STEP1 循环等待
        while (number==0) {
            this.wait();
        }

        //STEP2 业务
        number--;
        System.out.println(Thread.currentThread().getName()+number);

        //STEP3 通知
        this.notifyAll();
    }
}
哲学家就餐

线程指定顺序执行

有5位哲学家,围绕一张圆桌吃饭,桌子上放着5根筷子,每两个哲学家之间放一支。哲学家的动作包括思考和进餐,进餐时需要同时拿到左右两边的叉子,思考时将两支叉子放回原处。
问题:如何保证哲学家的动作有序进行?
熟睡的理发师
假设有一个理发店只有一个理发师,一张理发时坐的椅子,若干张普通椅子顾客供等候时坐。没有顾客时,理发师就坐在理发的椅子上睡觉。顾客一到,他不是叫醒理发师,就是离开。如果理发师没有睡觉,而在为别人理发,他就会坐下来等候。如果所有的枯木都坐满了人,最后来的顾客就会离开。
三个烟鬼
假设一支香烟需要:1、烟草;2、卷烟纸;3、一根火柴。
假设一张圆桌上围座着三烟鬼。他们每个人都能提供无穷多的材料:一个有无穷多的烟草;一个有无穷多的卷烟纸;一个有无穷多的火柴。
假设还有一个不吸烟的协调人。他每次都会公正地要求两个人取出一份材料放到桌,然后通知第三个人。第三个人从桌上拿走另外两个人的材料,再加上自己的一份,卷一枝烟就会抽起来。这时,协调人看到桌上空了,就会再次随机叫两人向桌上贡献自己的材料。这个过程会无限地进行下去。

不会有人把桌上的东西藏起来。只有当他抽完一枝烟后,才会再卷另一枝。如果协调人将烟草和卷烟纸放到桌上,而那个有火柴的人仍在吸烟,那么烟草、卷烟纸就会原封不动地放在桌上,走到有火柴的人抽完烟取走桌上的材料。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值