Java并发编程

基础知识

形成死锁的四个必要条件是什么

  1. 互斥条件:线程对于所分配到的资源具有排他性,即一个资源只能被一个线程占用,直到该线程释放;
  2. 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对以获得的资源保持不放;
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源;
  4. 循环等待条件:当发生死锁时,所等待的线程必定会形成一个环路,造成永久阻塞

创建线程的4种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用Executors工具类创建线程池
public class ThreadTest {
    /**
     * 方法一:继承Thread
     */
    class MyThread extends Thread {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "继承Thread的线程正在工作");
        }
    }

    /**
     * 方法二:实现Runnable
     */
    class MyRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "实现Runnable的线程正在工作");
        }
    }

    /**
     * 方法三:实现Callable
     */
    class MyCallable implements Callable<Integer>{

        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName() + "实现Callable的线程正在工作");
            return 1;
        }
    }

    /**
     * 方法四:利用Executors工具类创建线程池
     */
    class MyRunnable2 implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "Executors创建的线程池正在工作");
        }
    }

    @Test
    public void test(){
        //创建线程
        MyThread myThread = new MyThread();

        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);

        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        Thread callableThread = new Thread(futureTask);

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable2 myRunnable2 = new MyRunnable2();

        //开启线程
        myThread.start();
        thread.start();
        callableThread.start();
        executorService.execute(myRunnable2);
    }
}

线程的状态和基本操作

在这里插入图片描述

  1. 新建(new):新创建了一个线程对象。

  2. 可运行(runnable):线程对象创建后,当调用线程对象的start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。

  3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。

    阻塞的情况分三种:
    (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
    (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

  5. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

sleep() 和 wait() 有什么区别?

  • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法,当然也可以使用wait(long timeout)超时后线程会自动苏醒。sleep() 方法执行完成后,线程会自动苏醒。

为什么要将wait()方法放在while循环中

因为处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

Thread类的yield方法有什么作用?

使当前线程从运行状态转为就绪状态,以便给相同优先级或更高优先级的线程运行的机会。

sleep()和yield()方法为什么是静态的?

Thread类的sleep()和yield()方法都为静态方法,说明只有正在执行的线程才能执行,在其他处于等待状态的线程上调用这些方法是没有意义的,如果设置为非静态的,那么程序员就可能错误的认为可以在其他非运行线程上调用这些方法。

sleep、yield、join、wait的比较

  1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
  3. t.join()/t.join(long millis),让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。具体而言,可以通过线程t的join()方法来等待线程t的结束,或者使用线程t的join(millis)方法来等待线程t的结束,但最多只等待2ms。
  4. obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
  5. obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

wait(),notify(),notifyAll()

  • 如果一个线程调用了对象的wait()方法,那么线程便会进入该对象的等待池中,等待池中的线程不会去竞争该对象的锁;
  • notifyAll()会唤醒所有的线程,notify()只会唤醒一个线程;
  • notifyAll()调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争,notify()只会唤醒一个线程,具体唤醒哪个线程由虚拟机控制。

线程同步和互斥

  • 当一个线程对共享的数据进行操作时,应使之成为一个“原子操作”,即在没有完成相关操作之前,不允许其他线程打断它,否则就会破坏数据的完整性,这就是线程的同步;
  • 线程互斥是指对于共享的进程资源,当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用该资源的线程释放资源;
  • 线程互斥可以看成是一种特殊的线程同步。

并发理论

Java内存模型

1. 线程间如何实现通信以及如何同步?

  • 线程之间的通信机制有两种:共享内存和消息传递;
  • 在共享内存的并发模型中,线程之间通过写-读内存中的公共状态来隐式进行通信,而在消息传递的模型中,线程之间必须通过明确的发送消息来显式进行通信;
  • Java采用的是共享内存的方式来实现通信,通过synchronized和lock关键字加锁的方式来实现同步

谈谈你对Java内存模型的理解

处理器和内存不是同数量级,所以需要在中间建立中间层,也就是高速缓存,这会引出缓存一致性问题。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory),有可能操作同一位置引起各自缓存不一致,这时候需要约定协议在保证一致性。
Java 内存模型(Java Memory Model,JMM):屏蔽掉了各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致性的内存访问效果。
在这里插入图片描述

2.Java内存区域和Java内存模型区别

Java内存区域:在这里插入图片描述

  • 方法区:
    主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,方法区里面有一个运行时常量池,用于存放编译器生成的各种字面量和符号引用。
  • JVM堆
    主要用于存放对象实例,是垃圾收集器管理的主要区域。
  • 程序计数器
    代表了当前线程锁执行的字节码行号指示器。
  • 虚拟机栈
    代表了Java方法执行的内存模型,每个方法执行时都会创建一个栈帧来存储方法的变量表、操作数栈、动态链接方法、返回值、返回地址等信息,每个方法从调用到结束对应了一个栈帧在虚拟机栈中的入栈和出栈过程。
  • 本地方法栈
    和本地方法有关。

Java内存模型:
是一种抽象的概念,并不真实存在,用于定义程序中各个变量的访问方式。
在这里插入图片描述
JVM运行程序的实体是线程,每个线程创建时,JVM都会为其创建一个工作内存,用于存储线程私有的数据,而JVM内存模型中规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行
因此首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本,因为无法访问其他线程的工作线程,所以线程之间的通信必须通过主内存来完成。

JMM存在的必要性

如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题,JMM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,JMM是围绕着程序执行的原子性,有序性,可见性展开的,对于原子问题,JMM自身提供了对基本数据类型读写操作的原子类型,可见性问题可以通过synchronized或者volatile关键字来解决,Happens-before原则也保证了多线程环境下两个操作间的可见性,有序性,同时volatile还能禁止指令重排,synchronized和Lock操作来实现有序性。

as-if-serial规则和happens-before规则

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义和happens-before都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

happens-before定义:
1、如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;
2、两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序执行,只要重排序的结果与按照happens-before执行的结果一致,那么JMM允许这种重排序;

happens-before规则有哪些:

  • 程序顺序原则:同一个线程内必须按照代码顺序执行;
  • 锁规则:
  • volatile规则:volitile变量的“写”先发生于“读”
  • 线程启动规则:线程的start()方法先于它的每个动作
  • 线程终止规则:线程的所有操作先于线程的结束
  • 线程中断规则:对线程interrupt()方法的调用先于被中断线程的代码检测到中断事件的发生
  • 传递性

并发关键字

synchronized

synchronized关键字的三种使用方式

  • 修饰实例方法
  • 修饰静态方法
  • 修饰代码块
public class SynchronizedUseTest {
    private static int sum = 0;
    /**
     * synchronized修饰实例方法
     */
    class AccountSync implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                increase();
            }
            //使用同步代码块进行同步操作,所对象是instance
            synchronized(this){
            	for(int i = 0; i < 10000; i++){
            		sum++;
            	}
            }
        }

		//如果修饰的是实例方法,那么只有同一对象实例绑定的不同线程访问才能输出正确结果
		//如果修饰的是类方法,只要是当前类下的实例,都能正常得到正确结果
        private synchronized void increase() {
            sum++;
        }
    }
    @Test
    public void test() throws InterruptedException {
        AccountSync instance = new AccountSync();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);//

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(sum);
    }
}

synchronized底层语义原理

  • Java虚拟机中的同步是基于进入和退出管程(Monitor)对象实现的。
  • 在JVM中,对象在内存中的布局分为:对象头、实例数据和对齐填充三部分,实例数据用来存放类的属性数据信息,Java对象头中就存放了synchronized使用的锁对象;
  • synchronized修饰的代码块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
  • synchronized修饰的方法通过ACC_SYNCHRONIZED标识来指明该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
  • 重入锁是因为底层维护了一个计数器,当计数器值为0时,表明该锁未被任何线程所持有,其他线程可以竞争获得锁

使用synchronized关键字实现单例模式

public class SingletonBySynchronized {
	//因为指令重排的特性,所以这里必须加volatile修饰
    private volatile static SingletonBySynchronized uniqueInstance;
    
    public SingletonBySynchronized() {
    }

    public static SingletonBySynchronized getUniqueInstance() {
        if(uniqueInstance == null){
            synchronized (SingletonBySynchronized.class){
                if(uniqueInstance == null){
                    uniqueInstance = new SingletonBySynchronized();
                }
            }
        }
        return uniqueInstance;
    }
}

锁优化

锁消除
对检测出不可能存在竞争的共享数据的锁进行消除,主要是通过逃逸分析来判断,如果堆上的共享数据不可能逃逸出去被其他线程访问到,那么就可以把它们当做私有数据来对待,从而消除锁。

偏向锁
偏向锁是JDK6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提高程序的性能。

所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁
轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

这是因为很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时如果所有等待的线程都进入阻塞队列,那么会产生用户态和内核态切换的问题,造成的开销非常大,因此可以在synchronized 的边界做忙循环(自旋),如果循环多次还没有获得锁,再阻塞,由于忙循环也要占用CPU时间,所以自旋锁只适用于共享数据的锁定状态很短的场景。

锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。如果一串零碎的操作都是对同一个对象加锁,可以将加锁的范围扩展,提高性能。

线程中断与synchronized

“中断”是指线程在运行过程中打断其运行,在Java中,提供了3个有关线程中断的方法:

//中断线程(实例方法)
public void Thread.interrupt();
 
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
 
//判断线程是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方法可以中断该线程,此时将抛出一个InterruptedException异常,同时中断状态将会被复位(由中断状态变为非中断状态)

public class InterruptSleepThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    while(true){
                        TimeUnit.SECONDS.sleep(2);
                        System.out.println("没有进入中断");
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interrupted When Sleep");
                    boolean interrupted = this.isInterrupted();
                    System.out.println("interrupt:"+interrupted);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();
    }
}

在这里插入图片描述
如果是运行期非阻塞状态的线程,那么直接调用Thread.interrupt()中断线程是不会得到响应的,因为处于非阻塞状态的线程需要手动进行检测并结束程序,如下所示:

public class InterruptSleepThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while(true){
                    if (this.isInterrupted()) {
                        System.out.println("线程中断");
                        break;
                    }
                }
                System.out.println("已跳出循环,线程中断");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();
    }
}

在这里插入图片描述
总结:

  • 当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将抛出interruptException异常(该异常必须捕获,无法向外抛出),并将中断状态复位;
  • 当线程处于运行状态时,我们也可以调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(结束run方法的代码)

volatile简介

Java提供了volatile关键字来保证内存可见性和禁止指令重排,volatile提供happens-before的保证,确保一个线程的修改能对其他线程是可见的,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

volatile不能保证原子性,所以一般与CAS结合使用;

synchronized和volatile的区别是什么

synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程;
volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取,保证多线程环境下变量的可见性,以及禁止指令重排序。

主要区别如下:

  • volatile是变量修饰符;synchronized可以修饰类,方法,变量,代码块;
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
  • volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞;
  • volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化;

final和并发的关系

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

Lock体系

Lock和synchronized的比较

Lock接口比同步方法和同步代码块提供了更具扩展性的锁操作,主要优势有:

  1. 可以使锁更公平
  2. 可以使线程在等待锁的时候响应中断;
  3. 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;
  4. 可以在不同的范围,以不同的顺序获取和释放锁

乐观锁如何实现

1、使用版本标识来确定读到的数据与提交时的数据是否一致,提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略;
2、CAS,当多个线程尝试使用CAS同时修改同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争失败了,可以再次尝试。CAS操作包含三个操作数–需要读取的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B),如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B,否则处理器不做任何操作。

CAS会产生什么问题

1、ABA问题:
JDK1.5的atomic包里提供了一个类AtomicStampedReference来解决ABA问题;
2、循环时间长开销大:
对于资源竞争严重的情况,CAS自旋的概率比较大,会浪费很多CPU资源,效率低于synchronized;
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以使用锁。

死锁、活锁、饥饿的区别

  • 死锁:是指两个或以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的现象,若无外力作用,它们都将无法推进下去
  • 活锁:处于活锁的实体在不断的改变状态,活锁有可能自行解开
  • 饥饿:一个或者多个线程因为种种原因无法获得所需的资源,导致一直无法执行的状态。主要原因是:高优先级线程吞噬所有的低优先级线程的CPU时间,线程在等待一个本身也处于等待完成的对象,线程被永久阻塞在一个等待进入同步快的状态,

AQS(AbstractQueuedSynchronizer)

AQS又称队列同步器,是一个用来构建锁和同步器的框架,有:ReentrantLock、Semphore、ReentrantReadWriteLock、FutureTask等。

原理:
AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS对资源的共享方式

  • Exclusive(独占):只有一个线程能执行,又可以分为公平锁和非公平锁
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch等。

读写锁

ReadWriteLock是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读与读之间不会互斥,读与写,写与读,写与写之间才会互斥,提升了读写的性能。

并发容器

ConcurrentHashMap

HashMap为什么不安全:
HashMap在并发执行put操作时会发生死循环,是因为多线程会导致HashMap的Entry链表形成环型数据结构,一旦形成环型数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现,JDK1.6版本关键要素:

  • segment继承了ReentrantLock充当锁的角色,为每个segment提供了线程安全的保障;
  • segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

JDK1.8后,ConcurrentHashMap抛弃了原有的Segment锁,采用CAS+synchronized来保证并发安全性。

CopyOnWriteArrayList

设计思想:

  • 读写分离,读和写分开
  • 最终一致性
  • 使用另外开辟空间的思路来解决并发冲突

优缺点:
优点:
当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set
    操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是没法满足实时性要求。
  • 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

ThreadLocal

ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一变量,而ThreadLocal是为每个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本,可以这么说,ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。

ThreadLocal定义了四个方法:

  • get():返回此线程局部变量的当前线程副本中的值;
  • initialValue():返回此线程局部变量的当前线程的“初始值”;
  • remove():移除此线程局部变量的当前线程的值;
  • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值,采用的是开放地址法

ThreadLocal内部还有一个静态内部类ThreadLocalMap,它提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本,ThreadLocal定义的四个方法都是对ThreadLocalMap来进行操作,ThreadLocal实例本身并不存储值,它只是提供了一个在当前线程中找到副本值的key。

在这里插入图片描述
ThreadLocal的内存泄漏问题
每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系,如下图所示:
在这里插入图片描述
由于存在这个强引用关系,会导致value无法回收。如果这个线程对象不会销毁,那么这个强引用关系则会一直存在,就会出现内存泄漏情况。
解决方法:
在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以在使用完ThreadLocal方法后调用remove()方法进行处理。

BlockingQueue

阻塞队列是一个支持两个附加操作的队列,在队列为空时,获取元素的线程会等待队列变为非空,当队列满时,存储元素的线程会等待队列可用。

阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

线程池

什么是线程池

因为在面向对象编程中,创建和销毁对象很费时,为了提高程序效率,可以利用线程池来减少创建和销毁对象的次数。

线程池就是实现创建若干个可执行的线程放入一个容器中,需要的时候从池中获取线程,使用完毕后放回池中,从而减少了线程的创建和销毁带来的开销。

public interface Executor {
    void execute(Runnable command);
}

Executors是一个工具类,提供了一些静态工厂方法用于生成一些常用的线程池,如:

  • newSingleThreadExecutor:创建一个单线程的线程池,这个线程池只有一个线程在工作,保证所有任务的执行顺序按照任务的提交顺序执行;
  • newFixedThreadPool:创建固定大小的线程池,线程池的大小一旦到达固定值,就会保持不变,适合在服务器上使用(性能好)。
  • newCachedThreadPool:创建一个可缓存的线程池,线程池的线程数量可以随着任务数自行加减,线程池的大小由JVM决定;
  • newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

线程池的优点

  • 降低资源消耗:减少线程创建,销毁带来的开销;
  • 提高响应速度:能够提高系统资源的使用率;
  • 提高线程的可管理性:线程资源很宝贵,如果随意创建线程,会带来一些不好的影响,利用线程池可以进行统一的分配、调度和监控;
  • 附加功能:提供定时执行、单线程、并发数控制等功能

线程池的状态

  • RUNNING:正常状态
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务
  • STOP:不接受新的任务提交,也不再处理等待队列中的任务,同时还中断正在执行任务的线程 ;
  • TIDYING:
  • TERMINATED:执行terminated()方法以后

Executor和Executors的区别

  • Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求
  • Executor接口对象能够执行我们的线程任务
  • ExecutorSerivice接口继承了Executor接口并扩展了功能,主要是我们能获得任务执行的状态并且可以获得任务的返回值
  • Future表示异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,可以使用get()方法来获取计算的结果。

线程池中submit()和execute()方法的区别

  • 接收参数:execute只能执行Runnable类型的任务,submit可以执行Runnable和Callable类型的任务;
  • 返回值:submit方法可以返回持有计算结果的Future对象,而execute没有
  • 异常处理:submit更方便异常处理

ThreadPoolExecutor和Executors的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,这是因为:

  • newFixedThreadPool 和 newSingleThreadExecutor:
    主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:
    主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

而ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

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);
    }

核心参数

  • corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize :线程池中允许存在的工作线程的最大数量
  • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
    其他参数
  • keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
  • unit :keepAliveTime 参数的时间单位。
  • threadFactory:为线程池提供创建新线程的线程工厂
  • handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

ThreadPoolExecutor饱和策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经满了时,就会触发饱和策略,主要有:

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。(默认使用)
  • ThreadPoolExecutor.CallerRunsPolicy:通过增加队列容量来不丢弃任何一个任务请求,会降低新任务提交的速度,影响整体性能。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
    在这里插入图片描述

FutureTask

public class FutureTaskDemo {
    public static void main(String[] args) throws InterruptedException, TimeoutException, ExecutionException {
        long startTime = System.currentTimeMillis();

        ExecutorService executorService = Executors.newFixedThreadPool(3);

        FutureTask<String> heatUpWaterFuture = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("烧开水");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("烧水用时:"+(System.currentTimeMillis()-startTime)+"ms");
                return "ok";
            }
        });

        FutureTask<String> cookMealsFuture = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("煮饭");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("煮饭用时:"+(System.currentTimeMillis()-startTime)+"ms");
                return "ok";
            }
        });

        executorService.submit(heatUpWaterFuture);
        executorService.submit(cookMealsFuture);

        System.out.println("炒菜");

        TimeUnit.SECONDS.sleep(2);
        System.out.println("菜炒好了");

        if(heatUpWaterFuture.get(50,TimeUnit.SECONDS) == "ok"
                && cookMealsFuture.get(50,TimeUnit.SECONDS) == "ok"){
            System.out.println("开饭了");
        }

        long endTime = System.currentTimeMillis();
        System.out.println("做饭用时:"+ (endTime-startTime) + "ms");
        
        System.exit(0);
    }
}

在实际开发过程中,将那些耗时较长,且可以并行的操作都封装成一个FutureTask,该类提供了Future的基本实现,提供了启动和取消计算、查询计算是否完成以及检索计算结果的方法,只有在计算完成后才可检索结果;如果计算尚未完成,get方法将阻塞。
FutureTask的实现是基于 AbstractQueuedSynchronizer,FutureTask 声明了一个内部私有的继承于 AQS 的子类 Sync,对 FutureTask 所有公有方法的调用都会委托给这个内部子类。

原子操作类atomicXXX的原理

AtomicInteger类主要利用CAS+volatile和native方法来保证原子操作,避免了使用synchronized带来的高开销,执行效率大大提高。
部分源码:

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset
		(AtomicInteger.class.getDeclaredField("value"));
	} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

CAS的原理是拿期望的值和原本的一个值做比较,如果相同则更新成新的值,UnSafe类的objectFieldOffset()方法是一个本地方法,用来拿到“原来的值”的内存地址,返回值是valueOffset,另外value是一个volatile变量,保证任何时刻线程总能拿到该变量的最新值。

并发工具

CountDownLatch和CyclicBarrier

CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
  • 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
  • CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;
  • CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

Semaphore

Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数(允许多个线程同时访问),Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。

Exchanger

Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值