Java 多线程知识点总结

1,649 阅读27分钟

1.1.多线程基础

  • 什么是线程和进程? 线程与进程的关系,区别及优缺点?
  • 说说并发与并行的区别?
  • 为什么要使用多线程呢?
  • 使用多线程可能带来什么问题?(内存泄漏、死锁、线程不安全等等)
  • 创建线程有哪几种方式?(a.继承 Thread 类;b.实现 Runnable 接口;c. 使用 Executor 框架;d.使用 FutureTask)
  • 说说线程的生命周期和状态?
  • 什么是上下文切换?
  • 什么是线程死锁?如何避免死锁?
  • 说说 sleep() 方法和 wait() 方法区别和共同点?
  • 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
  • 多线程条件变量为什么要在while体里
  • 重点问了Java线程锁:synchronized 和ReentrantLock相关的底层实现 线程池的底层实现以及常见的参数
  • java有哪些锁?(乐观锁&悲观锁、可重入锁&Synchronize等)
  • J.U.C下的常见类的使用。Threadpool的深入考察;blockingQueue的使用
  • Volatile关键字有什么用(包括底层原理)
  • 线程池的调优策略
  • 谈谈多线程和并发工具的使用
  • Java中的多线程,以及线程池的增长策略和拒绝策略了解么。 ......
什么是线程死锁?如何避免死锁?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

我上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:

  • 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

 new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();

输出:

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

说说 sleep() 方法和 wait() 方法区别和共同点?

  • 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
  • 两者都可以暂停线程的执行。
  • Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

1.2.多线程知识进阶

volatile 关键字(非阻塞算法)

  • Java 内存模型(JMM);
  • 重排序与 happens-before 原则了解吗?
  • volatile 关键字的作用;
  • 说说 synchronized 关键字和 volatile 关键字的区别; ......

在这里使用synchronized和使用volatile是等价的,都解决了共享变量value的内存可见性问题,但是前者是独占锁,同时只能有一个线程调用get()方法,其他调用线程会别阻塞,同时会存在线程上下切换和线程重新调度的开销,这也是使用锁方式不好的地方。而后者是非阻塞算法,不会造成线程上下文切换的开销。

使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太重,因为它会带来线程上下文的切换开销。对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用volatile关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

volatile的内存定义和synchronized有相似之处,当线程写入了volatile变量时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)

volatile虽然提供了可见性保证,但并不保证操作的原子性。

那么一般在什么时候才使用volatile关键字呢?

  • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取——计算——写入三步操作,这三步操作不是原子性的,而volatile不保证原子性。
  • 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。

Java中的原子性操作

所谓原子性操作:执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。在设计计数器时一般都先读取当前值,然后+1,再更新。这个过程是读——改——写的过程,如果不能保证这个过程的原子性,那么就会出现线程安全问题。

ThreadLocal

有啥用(解决了什么问题)?怎么用? 原理了解吗? 内存泄露问题了解吗?

线程池

  • 为什么要用线程池?
  • 你会使用线程池吗?
  • 如何创建线程池比较好? (推荐使用 ThreadPoolExecutor 构造函数创建线程池)
  • ThreadPoolExecutor 类的重要参数了解吗?ThreadPoolExecutor 饱和策略了解吗?
  • 线程池原理了解吗?
  • 几种常见的线程池了解吗?为什么不推荐使用FixedThreadPool?
  • 如何设置线程池的大小? ......

脏读

一个常见的概念。在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的。

多线程线程安全问题示例

看一段代码:

public class ThreadDomain13
{
    private int num = 0;
    
    public void addNum(String userName)
    {
        try
        {
            if ("a".equals(userName))
            {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }
            else
            {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(userName + " num = " + num);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

写两个线程分别去add字符串"a"和字符串"b":

public class MyThread13_0 extends Thread
{
    private ThreadDomain13 td;
    
    public MyThread13_0(ThreadDomain13 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum("a");
    }
}
public class MyThread13_1 extends Thread
{
    private ThreadDomain13 td;
    
    public MyThread13_1(ThreadDomain13 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum("b");
    }
}

看一下运行结果:

a set over!
b set over!
b num = 200
a num = 200
  • 按照正常来看应该打印"a num = 100"和"b num = 200"才对,现在却打印了"b num = 200"和"a num = 200",这就是线程安全问题。我们可以想一下是怎么会有线程安全的问题的:

  • mt0先运行,给num赋值100,然后打印出"a set over!",开始睡觉 mt0在睡觉的时候,mt1运行了,给num赋值200,然后打印出"b set over!",然后打印"b num = 200"

  • mt1睡完觉了,由于mt0的num和mt1的num是同一个num,所以mt1把num改为了200了,mt0也没办法,对于它来说,num只能是100,mt0继续运行代码,打印出"a num = 200" 分析了产生问题的原因,解决就很简单了,给addNum(String userName)方法加同步即可:

public class ThreadDomain13
{
    private int num = 0;
    
    public synchronized void addNum(String userName)
    {
        try
        {
            if ("a".equals(userName))
            {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }
            else
            {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(userName + " num = " + num);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

看一下运行结果:

a set over!
a num = 100
b set over!
b num = 200

多个对象多个锁

在同步的情况下,把main函数内的代码改一下:

public static void main(String[] args)
{
    ThreadDomain13 td0 = new ThreadDomain13();
    ThreadDomain13 td1 = new ThreadDomain13();
    MyThread13_0 mt0 = new MyThread13_0(td0);
    MyThread13_1 mt1 = new MyThread13_1(td1);
    mt0.start();
    mt1.start();
}

看一下运行结果:

a set over!
b set over!
b num = 200
a num = 100

打印结果的方式变了,打印的顺序是交叉的,这又是为什么呢?

这里有一个重要的概念。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:既然锁叫做对象锁,那么势必和对象相关,所以多个线程访问的必须是同一个对象。

如果多个线程访问的是多个对象,那么Java虚拟机就会创建多个锁,就像上面的例子一样,创建了两个ThreadDomain13对象,就产生了2个锁。既然两个线程持有的是不同的锁,自然不会受到"等待释放锁"这一行为的制约,可以分别运行addNum(String userName)中的代码。

synchronized方法与锁对象

上面我们认识了对象锁,对象锁这个概念,比较抽象,确实不太好理解,看一个例子,在一个实体类中定义一个同步方法和一个非同步方法:

public class ThreadDomain14_0
{
    public synchronized void methodA()
    {
        try
        {
            System.out.println("Begin methodA, threadName = " + 
                    Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("End methodA, threadName = " + 
                    Thread.currentThread().getName() + ", end Time = " + 
                    System.currentTimeMillis());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    
    public void methodB()
    {
        try
        {
            System.out.println("Begin methodB, threadName = " + 
                    Thread.currentThread().getName() + ", begin time = " + 
                    System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("End methodB, threadName = " + 
                    Thread.currentThread().getName());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

一个线程调用其同步方法,一个线程调用其非同步方法:

public class MyThread14_0 extends Thread
{
    private ThreadDomain14_0 td;
    
    public MyThread14_0(ThreadDomain14_0 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodA();
    }
}
public class MyThread14_0 extends Thread
{
    private ThreadDomain14_0 td;
    
    public MyThread14_0(ThreadDomain14_0 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodA();
    }
}

写一个main函数去调用这两个线程:

public static void main(String[] args)
{
    ThreadDomain14_0 td = new ThreadDomain14_0();
    MyThread14_0 mt0 = new MyThread14_0(td);
    mt0.setName("A");
    MyThread14_1 mt1 = new MyThread14_1(td);
    mt1.setName("B");
    mt0.start();
    mt1.start();
}

看一下运行效果:

Begin methodA, threadName = A
Begin methodB, threadName = B, begin time = 1443697780869
End methodB, threadName = B
End methodA, threadName = A, end Time = 1443697785871

从结果看到,第一个线程调用了实体类的methodA()方法,第二个线程完全可以调用实体类的methodB()方法。但是我们把methodB()方法改为同步就不一样了,就不列修改之后的代码了,看一下运行结果:

Begin methodA, threadName = A
End methodA, threadName = A, end Time = 1443697913156
Begin methodB, threadName = B, begin time = 1443697913156
End methodB, threadName = B

从这个例子我们得出两个重要结论:

A线程持有Object对象的Lock锁,B线程可以以异步方式调用Object对象中的非synchronized类型的方法 A线程持有Object对象的Lock锁,B线程如果在这时调用Object对象中的synchronized类型的方法则需要等待,也就是同步

ReetTrantLock

在java多线程中,可以使用synchronized关键字实现线程之间同步互斥,但在JDK1.5中新增加了ReentranLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比synchronized更加的灵活。

首先回答一个问题?线程的三大特性?什么时候我们需要锁?java中已经提供了synchronized,为什么还要使用ReentrantLock?AQS原理。

线程的三大特性:原子性、可见性、有序性。也就是说满足这个三个特性的操作都是可以保证安全的,如Atomic包、volatile、通过happensBefore原则可以进行线程的安全的判断,这个依据通常是为了避免jvm指令重排。比如通常我们知道的配置信息,如果有多个线程去进行配置信息的修改,则需要进行上锁。或者多个线程修改一个变量时,此时就需要进行上锁了,或者读写分离时,可以考虑ReentrantReadWriteLock等。其本质是解决并行中的问题,将并行转成串行问题进行解决。那怎么上锁才有用呢?锁的状态大部分情况下是互斥的。当然也有特例:ReentrantReadWriteLock的读读是不会互斥的,其读写,写写是互斥的,当然可重入锁执行一个线程调用另外一个线程也不会互斥。之所以使用RenntranLock,是因为它适用于并发场景较为激烈的情况,同时其是经过优化了的。当然synchronized自JDK1.6之后也进行了优化,将其分为了偏向锁、轻量级锁、重量级锁

同时ReentrantLock是基于AQS(AbstractQueuedSynchronizer)实现的,其目前也是唯一实现lock接口的可重入锁。其优点在于将锁进行细化,将锁分为两种锁,公平锁和非公平锁,也即独占锁与抢占锁。当进入公平锁时,是直接返回获取锁成功的,而没有获取锁时,首先会将其封装成node,放入到addWaiter中,进行阻塞,等待上一个线程完成,在进行请求,如果上一个线程完成了,则进行状态的waitStatus的变化,将其变成可执行状态,进行操作。再进行锁的获取。同时Condition采用await和singnal的方式,当然也是将其封装到队列中,进行唤醒队列。调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相关联的锁。调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

  • 1.AQS数据结构及变量
//Node数据结构
//FIFO的双向链表,每个数据结构都有两个指针,
//分别指向后继节点和前驱节点。
//每个node都是由线程封装的,当线程抢占锁失败
//后会封装成node加入到AQS队列中去,当获取锁的线程释放锁以后,会从
//队列中唤醒一个阻塞的节点(线程)
static final class Node {
//waitStatus的5种状态:CANCELLED=1、
//SIGNAL=-1、CONDITION=-2、
//PROPAGATE=-3、0:默认状态

//CANCELLED=1,结束状态,进入该状态后的节点将不会再变化
static final int CANCELLED =  1;
//SIGNAL=-1,只要前置节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
//一个线程通信工具类似于synchronized的wait/notify
//可以使某些线程一起等待某个条件(condition),
//只有满足条件时,线程才会被唤醒
//主要有两个值得关注的:await、signal
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
//共享模式下,PROPAGATE状态的线程处于可运行状态
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev; //前驱节点
volatile Node next;  //后驱节点
volatile Thread thread;  //当前线程
Node nextWaiter; //存储在condition队列中的后继节点
  //是否为共享锁
  final boolean isShared() {
       return nextWaiter == SHARED;
   }
   final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marke
    }

    //addWaiter中的信息
    Node(Thread thread, Node mode) {     // Used by addWaite
        this.nextWaiter = mode;
        this.thread = thread;
    }

    //通常condition中包含的信息
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

//头结点
private transient volatile Node head;
//尾节点
private transient volatile Node tail;
//CAS中的属性,取0或者大于0,其中0表示无锁状态,
//>0表示已经有线程获得锁,state可以递增,也即重入的次数
private volatile int state;
  • 2.方法 获取锁
static final class NonfairSync extends Sync {
 
    //锁分为公平锁fairSync和非公平锁NonFairSync,我们可以
    //知道synchronized是公平锁,也就是fairSync
    //而公平锁是独占锁,因此可以知道synchronized是独占锁
    //而非公平锁为抢占锁。不管有没有线程排队,上来cas去抢占一下锁
    //cas成功,则表示成功获取锁,进行成功返回
    //否者cas失败,调用acquire(1)走锁竞争逻辑
    //其中cas调用底层的unsafe.compareAndSwapInt(this,stateOffset, expect, update);
    //进行更新操作,同时由于操作是原子性操作,因此不会出现线程安全问题
    //state=0,表示无锁状态
    //state>0时,也就是为1时,说明有线程获得了锁。
    //由于ReentrantLock允许重入,因此同一个线程多次获取同步锁的时候,state会递增,比如重入5次
    //namstate为5,同时需要释放5次,其他线程才可以获取锁。
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //进入非公平锁逻辑,重点关注
            acquire(1);
    }

    //尝试获取锁,如果成功返回true,
    // 不成功返回false,它是重写AQS队列
    //类中的tryAcquire方法
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

//执行cas操作,调用unsafe下的compareAndSwapInt:当前的值,偏移量、期望值、更新值
//同时注意偏移量是2的次幂
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);


//进入非公平所,抢占锁逻辑
//传入1是为了通过tryAcquire获取抢占锁,
//如果成功返回true,否则返回false
//如果tryAcquire失败,则会通过
//addWaiter方法将当前线程封装成Node添加 到AQS队列队尾
//acquireQueued,将node作为参数,通过自旋去尝试获取锁
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

  //通过addWriter方法会把线程添加到链表中,
    //接着会把node作为参数传递给acquireQueued方法,去竞争锁
    //将node作为参数,通过自旋去尝试获取锁
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //不进行中断
            boolean interrupted = false;
            //进行自旋
            for (;;) {
                //获取当前节点的prev节点
                final Node p = node.predecessor();
                //如果是head节点,说明有资格去争抢锁
                if (p == head && tryAcquire(arg)) {
                    //获取锁成功,说明前一个线程已经释放锁,
                    //然后设置head为当前线程执行权限
                    setHead(node);
                    //把原来head节点从链表中移除
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //前一个线程还没有释放锁,
                // 使得当前线程在执行tryAcquire时返回false
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //通过 cancelAcquire 取消获得锁的操作
            if (failed)
                cancelAcquire(node);
        }
    }

 final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

 //如果前一个线程还没有释放,此时当前线程和下一个线程都会来争抢锁会失败
    //那么失败以后会调用shouldParkAfterFailedAcquire方法
    //node中waitStatus有5种状态CANCELLED
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //拿到前置节点的等待状态
        int ws = pred.waitStatus;
        //如果状态等于SIGNAL,
        // 只要前置节点释放锁,
        //就会通知标识为SIGNAL状态的后续节点的线程
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
         //如果ws>0,则说明处于CANCELLED(1)的状态,说明CANCELLLED
        //在同步队列中等待的线程等待超时或被中断,
        //需要从同步队列中取消该Node的节点,处于结束状态
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do { //采用循环从队列中移除CANCELLED的节点
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {  //否者只有两种状态默认状态0或者PROPAGATE
            //也即初始化状态或者处于可执行状态,利用 cas
            // 设置 prev 节点的状态为 SIGNAL(-1)
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }


private final boolean parkAndCheckInterrupt() {
        //挂起当前线程变成WATING状态
        //park方法等待许可,unpark方法为线程提供许可
        LockSupport.park(this);
        //返回当前线程是否被其它线程触发
        // 过中断请求,如果有触发过中断请求,
        // 则返回当前的中断标识true
        //并且对中断标识进行复位标识已经响应过了中断请求
        //如果返回true,则意味着在acquire方法中会执行selfInterrput()
        //因为线程在调用acquireQueued方法的时候是不会响应中断请求的
        return Thread.interrupted();
    }

   public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

final boolean acquireQueued(final Node node, int arg) {
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      final Node p = node.predecessor();//获取当前节点的 prev 节点
      if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁
        setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限
        p.next = null; //把原 head 节点从链表中移除
        failed = false;
        return interrupted;
      }
      //ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
      if (shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt())
        interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}
  • 释放锁
public void unlock() {
    sync.release(1);
}

 //进行unlock会调用release方法释放锁
    public final boolean release(int arg) {
        //如果释放锁成功
        if (tryRelease(arg)) {
            //拿到AQS的head节点
            Node h = head;
            //如果头结点不为空,同时等待状态不为0
            //调用unparkSUcessor方法唤醒后续节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

 //唤醒节点的后继节点(如果存在)。
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        //获取head节点的状态
        int ws = node.waitStatus;
        //如果等待状态<0,则设置head节点的状态为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        //得到head节点的下一个节点
        Node s = node.next;
        //如果下一个节点为null或者status>0表示canacelled状态
        //通过从尾部节点开始扫描,找到距离head最近的一个waitStatus<=0的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //next节点不为空,直接唤醒这个线程即可
        if (s != null)
            LockSupport.unpark(s.thread);
    }

private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

Condition

关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现/通知模式,类ReentrantLock也可以实现同样的功能,但是需要借助于Condition对象。Condition类是在JDK5出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择地进行线程通知,在调度线程上更加灵活。

在使用notify()/notify()方法进行线程通知时,被通知的线程却是由JVM随机选择的。但使用ReentrantLock结合Condition类是可以实现前面介绍过的"选择性通知",这个功能是非常重要的,而且在Condition类中是默认提供的。

而synchronized就相当于整个Lock对象中只有单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的WAITING线程,没有选择权,会出现相当大的效率问题。

正确使用Condition实现等待/通知

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyService {
    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();
    public void await(){
        try {
        lock.lock();
        System.out.println("await 时间为:" + System.currentTimeMillis());
        condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void signal(){
        try{
        lock.lock();
        System.out.println("signal时间为:" + System.currentTimeMillis());
        condition.signal();
        }finally {
            lock.unlock();
        }
    }
}

public class ThreadA extends Thread{
       private MyService service;
       public ThreadA(MyService service){
           super();
           this.service = service;
       }

        @Override
        public void run() {
           service.await();
        }
}
public class TestRun {
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        ThreadA threadA = new ThreadA(service);
        threadA.start();
        threadA.sleep(3000);
        service.signal();
    }
}

成功实现等待/通知模式

Object类中的wait()方法相当于Condition类中的await()方法 Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法 Object类中的notify()方法相当于Condition类中的signal()方法。 Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

使用多个Condition实现通知部分线程

实现生产者/消费者:一对一交替打印

实现生产者/消费者:多对多交替打印

公平锁与非公平锁

公平锁与非公平锁:锁Lock分为公平锁与非公平锁,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进新出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果就是不公平了。

使用ReetrantReadWriteLock类

类使用ReetrantReadWriteLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReetrantLock.lock()方法后面的任务。这样保证了实例变量的线程安全,但效率却非常低下的。所以在SDK中提供了一种读写锁ReetrantReadWriteLock类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁ReetrantReadWriteLock来提升该方法的代码运行速度。

读写锁:一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。多个读锁之间不互斥,读锁与写锁互斥。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进入写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个thread进行写操作。

类ReetrantReadWriteLock的使用:读读共享


public class Service {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void read() throws InterruptedException {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName() + "" + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

类ReetrantReadWriteLock的使用:写写互斥

类ReetrantReadWriteLock的使用:读写互斥

类ReetrantReadWriteLock的使用:写读互斥

阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁

释放:signal()后,节点会从 condition 队列移动到 AQS 等待队列,则进入正常锁的获取流程

await:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
      throw new InterruptedException();
    Node node = addConditionWaiter(); //创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
    int savedState = fullyRelease(node); //释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
    int interruptMode = 0;
    //如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
    while (!isOnSyncQueue(node)) {//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
      LockSupport.park(this); // 第一次总是 park 自己,开始阻塞等待
      // 线程判断自己在等待过程中是否被中断了,如果没有中断,则再次循环,会在 isOnSyncQueue 中判断自己是否在队列上.
      // isOnSyncQueue 判断当前 node 状态,如果是 CONDITION 状态,或者不在队列上了,就继续阻塞.      // isOnSyncQueue 判断当前 node 还在队列上且不是 CONDITION 状态了,就结束循环和阻塞.
      if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
    }
    // 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
    // interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
    // 将这个变量设置成 REINTERRUPT.
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
    // 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
    // 如果是 null ,就没有什么好清理的了.
    if (node.nextWaiter != null) // clean up if cancelled
      unlinkCancelledWaiters();
    // 如果线程被中断了,需要抛出异常.或者什么都不做
    if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);
}

signal

public final void signal() {
  if (!isHeldExclusively()) //先判断当前线程是否获得了锁
    throw new IllegalMonitorStateException();
  Node first = firstWaiter; // 拿到 Condition 队列上第一个节点
  if (first != null)
    doSignal(first);
}

private void doSignal(Node first) {
  do {
    if ( (firstWaiter = first.nextWaiter) == null)// 如果第一个节点的下一个节点是 null, 那么, 最后一个节点也是 null.
      lastWaiter = null; // 将 next 节点设置成 null
      first.nextWaiter = null;
  } while (!transferForSignal(first) && (first = firstWaiter) != null);
  }

final boolean transferForSignal(Node node) {
  /*
  * If cannot change waitStatus, the node has been cancelled.
  */
  if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    return false;
  Node p = enq(node);
  int ws = p.waitStatus;
  // 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞),
  if (ws > 0 || !compareAndSetWaitStatus(p, ws,Node.SIGNAL))
    LockSupport.unpark(node.thread); // 唤醒输入节点上的线程.
  return true;
}

final boolean transferForSignal(Node node) {
  /*
  * If cannot change waitStatus, the node has been cancelled.*/
  if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    return false;
  Node p = enq(node);
  int ws = p.waitStatus;
  // 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞),
  if (ws > 0 || !compareAndSetWaitStatus(p, ws,Node.SIGNAL))
    LockSupport.unpark(node.thread); // 唤醒输入节点上的线程.
  return true;
} 

为什么要用线程池?

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:

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

线程池的使用

在任务与执行策略之间隐性耦合

  • 依赖性任务
  • 使用线程封闭机制的任务
  • 对响应时间敏感的任务
  • 使用ThreadLocal的任务

线程饥饿死锁(待完成)

运行时间较长的任务

设置线程的大小

线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.auailableProcessors来动态计算。

管理队列任务

Java 提供了哪几种线程池?他们各自的使用场景是什么?

Java 主要提供了下面 4 种线程池

  • FixedThreadPool: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • ScheduledThreadPoolExecutor: 主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor 又分为:ScheduledThreadPoolExecutor(包含多个线程)和 SingleThreadScheduledExecutor (只包含一个线程)两种。 各种线程池的适用场景介绍

FixedThreadPool: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器;

SingleThreadExecutor: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景;

CachedThreadPool: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器;

ScheduledThreadPoolExecutor: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景;

SingleThreadScheduledExecutor: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。

创建的线程池的方式

(1) 使用 Executors 创建

我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用 Java 提供好的线程池,另外在《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

(2) ThreadPoolExecutor 的构造函数创建

我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。示例如下:

private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(13));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出 java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。

(3) 使用开源类库

Hollis 大佬之前在他的文章中也提到了:“除了自己定义 ThreadPoolExecutor 外。还有其他方法。这个时候第一时间就应该想到开源类库,如 apache 和 guava 等。”他推荐使用 guava 提供的 ThreadFactoryBuilder 来创建线程池。下面是参考他的代码示例:

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}

通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。

参考: