Java多线程与Concurrent并发包问题汇总

文章目录

  • 进程与线程的区别与联系?
  • 线程的操作状态
  • 并行与并发的区别?
  • Java虚拟机是多线程的吗?
  • 多线程的实现方式有哪几种?如何选择?
    • 1. 继承Thread类
    • 2. 实现Runnable接口
    • 3. 实现Callable接口
  • 启动一个线程是调用 run()方法还是 start()方法?
  • 线程类Thread的常用方法?
  • 同步线程及线程调度相关的方法?
  • wait() 和 sleep() 方法的不同?
  • 定时器Timer和TimerTask?
  • 线程的互斥与同步?
  • 死锁是怎样产生的?如何解决或者预防?
  • Java多线程同步操作如何实现?
  • 线程局部变量ThreadLocal
  • 什么是公平锁和非公平锁?
  • 如何控制某个方法允许并发访问线程的个数?
  • Java中多线程之间的通信是如何实现的?
  • 多线程共享数据如何实现?
  • synchronized 和 volatile 关键字的区别?
  • 什么是线程池,如何使用?
  • 常用的线程池有哪些?
  • 线程池可以带来哪些好处?
  • 线程池的启动策略?
  • 非阻塞算法CAS
    • 乐观锁与悲观锁
    • 为什么要使用CAS算法
    • CAS无锁实现原理
  • Java的线程并发库java.util.concurrent
    • Executors 线程池工厂类
      • 线程池的作用
      • Executors 创建线程池
      • Executor 执行器
      • ExecutorService 执行器服务
        • ThreadPoolExecutor 线程池执行者
        • ScheduledThreadPoolExecutor定时线程执行者
    • 并发队列
      • 并发队列之BlockingQueue阻塞队列
      • 并发队列之非阻塞队列
        • ConcurrentLinkedQueue 非阻塞无界链表队列
        • ConcurrentHashMap 非阻塞 Hash 集合
        • HashMap的死循环问题
        • 为什么不用HashTable?
        • ConcurrentHashMap 底层原理
    • java.util.concurrent.lock 包
    • java.util.concurrent.atomic 包

进程与线程的区别与联系?

进程就是程序的一次动态执行过程,通俗来讲,进程就是正在运行的程序,它是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。但进程的开启是非常耗费时间的,所以有必要对其进行进一步的划分以提高性能。一个进程可以同时有多个线程,相当于一个程序中同时进行多个任务,多个线程共享同一个进程的资源(堆内存和方法区)。所有的线程一定要依附于进程才能够存在,一旦进程消失,线程一定也会消失。多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。线程是处理器调度和分派的基本单位,而且多线程具有随机性,抢占CPU执行权的概率完全是随机的。

线程的操作状态

任何线程一般都具有五种状态,即:创建就绪运行堵塞终止

  1. 创建
    在程序中用构造方法创建一个线程对象后,新的线程就处于新建状态。此时的线程就已经拥有内存空间和其他资源了,但还处于不可运行状态。
  2. 就绪
    新建线程对象后,调用该线程的**start()**方法就可以启动线程。当线程启动时,线程就进入了就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明线程已经具备了运行条件。
  3. 运行
    当就绪状态的线程被调用并且获得处理器资源时,线程就已经进入运行状态。此时,会自动调用线程的**run()**方法。run()方法中定义了线程的操作与功能。
  4. 堵塞
    一个正在运行状态的线程在某些特殊情况下,比如被人为挂起或需要执行耗时的输入输出操作时,将让出CPU,并暂时中止自己的执行,进入堵塞状态。在可执行的状态下,如果调用sleep()suspend(),**wait()**等方法,线程都将进入阻塞状态。堵塞时,线程不能进入排队队列,只有当引起阻塞的院系被消除后,线程将重新进入就绪状态。
  5. 终止
    线程调用stop方法时或者run方法结束后,就处于终止状态。处于终止状态的线程不具有继续运行的能力。

并行与并发的区别?

  • 并发是逻辑上同时发生,指在某一个时间内同时运行多个程序。
  • 并行是物理上同时发生,指在某一个时间点同时运行多个程序。

当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。

当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

Java虚拟机是多线程的吗?

java命令会启动java虚拟机,相当于启动了一个应用程序,相当于启动了一个进程。虚拟机会开启一个主线程去寻找main方法,所以说main方法是运行在主线程中的。但是虚拟机在工作时还会启动垃圾回收机制,也就相当于开启了另一个线程。所以说,我们的JVM虚拟机是多线程的

多线程的实现方式有哪几种?如何选择?

在Java中,要想实现多线程,就必须依靠一个线程的主类,在主类中重写run()方法作为线程的主体。不管是以三种方式中的哪一个实现多线程,都是为了定义这个主类。

三种实现方式的选择:尽量避免继承Thread类,优先考虑实现接口(Runnable或Callable)的方法。因为Java采用的是单继承的模式,继承Thread类就会带来这种局限性,没法再继承其他类;另外,实现接口可以更方便的实现数据共享的概念。

申明一点,多线程启动的唯一方法就是Thread类中的start()方法

1. 继承Thread类

  • 线程主体类:
public class MyThread extends Thread {
    // 重写run方法,作为线程的主操作方法
    @Override
    public void run() {
       ... 
    }
}
  • 开启多线程:
MyThread threadA = new MyThread("ThreadA");
MyThread threadB = new MyThread("ThreadB");
threadA.start();
threadB.start();

2. 实现Runnable接口

Thread类也是Runnable类的接口。使用Runnable接口实现多线程:

  1. 避免了单继承带来的局限性;
  2. 可以更好的实现数据共享。
  • 主体类
public class MyThread implements Runnable {
	 // 重写run方法,作为线程的主操作方法
    @Override
    public void run() {
       ...
    }
}
  • 开启多线程:要用到Thread类的有参构造方法(public Thread(Runnable target)
MyThread mt1 = new MyThread();
new Thread(mt1).start();	// 多线程调用同一个Runnable对象,就可以实现数据共享
new Thread(mt1).start();

3. 实现Callable接口

使用Runnable接口实现的多线程可以避免单继承的局限,但是Runnable接口存在一个问题就是没有办法返回run方法的操作结果(public void run())。为了解决这个问题,从JDK1.5开始,引入了这个接口java.util.concurrent.Callable:

@FunctionalInterface
public interface Callable<V> { V call() throws Exception; }

这个接口中只定义了一个**call()**方法,而且在call()方法上可以实现线程操作数据的返回,返回类型由Callable接口上的泛型决定。但是注意,Callable接口并非Runnable接口的子类,意味着不能直接传入Thread构造器并开启线程

为了开启线程并获取这个返回值,靠Thread类是不可以的。为了解决这个问题,从JDK1.5起,引入了java.util.concurrent.FutureTask类,定义如下:

public class FutureTask<V> extends Object implements RunnableFuture<V>{...}

FutureTask类提供了Callable接口子类的构造方法,并定义了专门的方法来获取多线程中的返回值:public FutureTask(Callable callable)public V get()

同时,FutureTask是Runnable接口子类,可以使用public Thread(Runnable target)构造。所以开启Callable对象线程的步骤一般是 callable - FutureTask(callable) - Thread(futuretask).start()。

  • 主体类
import java.util.concurrent.Callable;

public class MyThread implements Callable<String> {
    private int ticket = 10;

    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            if (this.ticket > 0)
                System.out.println("ticket=" + this.ticket--);
        }
        return "售完";
    }
}
  • 开启多线程并获取返回值:
 // 实例化多线程对象
 MyThread myThread1 = new MyThread();
 MyThread myThread2 = new MyThread();
 // 使用public FutureTask(Callable callable)实例化FutureTask
 FutureTask<String> task1 = new FutureTask(myThread1);
 FutureTask<String> task2 = new FutureTask(myThread2);
 // FutureTask是Runnable接口子类,可以使用public Thread(Runnable target)构造
 new Thread(task1).start();
 new Thread(task2).start();
 // 调用获取返回值
 String msg1 = task1.get();
 String msg2 = task2.get();
 System.out.println("线程1返回的结果是:" + msg1 + "\t线程2返回的结果是:" + msg2);

启动一个线程是调用 run()方法还是 start()方法?

多线程启动的唯一方法就是Thread类中的start()方法。start()方法里面会调用一个start0()的方法,而且这个方法是用native声明的。java中调用本机操作系统提供的函数的技术叫做JNI(Java Native Interface ),这个技术离不开特定的操作系统,因为多线程必须由操作系统来分配资源。这项操作是根据JVM负责根据不同的操作系统实现的。start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,所以线程并不会会立即运行。而run()方法是线程启动后要进行回调(callback)的方法。

线程类Thread的常用方法?

  1. 获取和设置线程名称:public final String getName(),public final void setName(String name)
  2. 获取当前执行的线程:public static Thread currentThread();
  3. 线程的优先级设置:public final void setPriority(int newPriority);(1-10,默认是5)优先级只能说明抢占CUP执行权的概率大一些,并不能保证一定优先执行。
  4. 线程休眠:public static void sleep(long millis) ;单位是毫秒。
  5. 线程加入:public final void join(); 该线程执行完毕再执行其他线程。
  6. 线程礼让:public static void yield() ;暂停当前正在执行的线程对象(时间相当短),并执行其他线程。
  7. 线程守护:public final void setDaemon(boolean on) ;传入true设置为守护线程,必须在启动线程前调用,主线程死亡后,守护线程均死亡。
  8. 中断线程:public void interrupt() ;当线程调用wait(),sleep()方法的时候处于阻塞状态,可以通过这个方法清除阻塞状态。并不能中断正在运行的线程。

同步线程及线程调度相关的方法?

  1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
  3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
  4. notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
  5. JDK5 通过 Lock 接口提供了显示的锁机制, Lock 接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。

wait() 和 sleep() 方法的不同?

  1. wait()是Object类的方法,而sleep()是Thread类的方法;
  2. sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁);
  3. wait,notify(唤醒单个线程)和notifyAll(唤醒所有线程)(都是Object类的方法)只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围);
  4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常;
  5. wait 通常被用于线程间交互, sleep 通常被用于暂停执行

定时器Timer和TimerTask?

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。定时器在实际开发中应用场景不多,一般由第三方框架实现。在Java中,可以通过Timer + TimerTask 来实现定时器功能。

Timer用于在后台线程中计划执行任务,可安排任务执行一次,或者定期重复执行。

TimerTask是一个抽象类,实现了Runnable接口。它的子类代表一个可以被Timer计划的任务,具体的任务在TimerTask中run方法中实现(TimerTask相当于专门用来制定定时任务的Runnable对象)。

定时器的启动必须通过Timer的schedule()方法(相当于start方法开启线程):
schedule(TimerTask task, long delay); 指定任务执行的延迟时间
schedule(TimerTask task,long delay,long period); 指定任务执行的延迟时间和周期
schedule(TimerTask task, Date time); 指定任务执行的准确时间
schedule(TimerTask task, Date firstTime, long period);指定任务执行的准确时间和周期

线程的互斥与同步?

由于多线程执行的异步性,会给系统造成混乱,比如当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错。因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。

当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系:

  1. 间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待。
  2. 直接相互制约。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理,那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。

间接相互制约可以称为互斥,直接相互制约可以称为同步同步包括互斥,互斥其实是一种特殊的同步

死锁是怎样产生的?如何解决或者预防?

所谓死锁,指两个线程都在等待彼此先完成,造成程序的停滞,一般程序的死锁都在运行期产生的。

死锁产生的必要条件:

  1. 互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
  2. 不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
  3. 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

如何避免死锁:

  1. 加锁顺序(线程按照一定的顺序加锁)- 在主线程里使用 join() 方法按顺序执行子线程
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)- 使用 lock.tryLock(5000, TimeUnit.MILLISECONDS) 等待5秒获取不到就放回false,进一步操作就是放弃请求,释放锁。

Java多线程同步操作如何实现?

在多线程高并发编程的时候,最关键的问题就是保证临界区对象的安全访问。临界区指的是一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。

Java中实现同步的方式:

  1. 使用synchronized关键字
  • 同步代码块:利用synchronized包装的代码块,但是需要指定同步对象,一般指定为this。这个对象习惯叫做监视器,它本身就是锁。
@Override
public void run() {    
	synchronized (this) {
		...
	}
}

Java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。 线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁。

  • 同步方法:利用synchronized定义的方法。同步代码块的锁对象其实可以为任意一个对象,但同步方法的锁对象只能是this。还有一种静态同步方法的锁对象是当前类对应的字节码文件对象。
@Override
public void run() { //调用同步方法 }
// 同步方法
private synchronized void A() {...}
  1. 使用Volatile关键字修饰变量

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:a.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;b.禁止进行指令重排序(在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序,多线程下会影响正确性)。

注意 volatile 没有原子性,仅仅实现了对变量操作的可见性。volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取

  1. 使用并发库下的Lock锁

Lock比使用synchronized方法和语句可以获得的更广泛的锁定操作。使用Lock锁需要自己手动释放锁,灵活性更高。

所谓重入锁,是针对同一个线程而言的,指已经获得锁的情况下可以再次请求获取锁,但相应的也要释放。

ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

private static final Lock lock = new ReentrantLock(); //ReentrantLock是Lock的实现类
@Override
public void run() {
	lock.lock();  // 上锁,请求不成功则等待
	try{
	    //处理任务
	}catch(Exception ex){
	    //处理异常
	}finally{
	    lock.unlock();   //释放锁
	}
}
  1. 使用并发编程库下的atomic包下的具有原子性的数据类型

java.util.concurrent 下的 atomic 包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。其实现基于CAS非阻塞算法。

AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

AtomicIntegerArray:原子更新整型数组中的元素;
AtomicLongArray:原子更新长整型数组中的元素;
AtomicReferenceArray:原子更新引用类型数组中的元素

AtomicReference:原子更新引用类型;
AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
AtomicMarkableReference:原子更新带有标记位的引用类型;

AtomicIntegeFieldUpdater:原子更新整型字段类;
AtomicLongFieldUpdater:原子更新长整型字段类;
AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。

线程局部变量ThreadLocal

  • 参考文章1
  • 参考文章2

ThreadLocal是一个本地线程副本变量工具类

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本

在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景

同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路,同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想

ThreadLocal类提供了三个public方法:

  • get()方法用于获取当前线程的副本变量值。
  • set()方法用于保存当前线程的副本变量值。
  • remove()方法移除当前前程的副本变量值。

常见操作是将ThreadLocal设置为public static修饰的全局共享的,在多线程中的run()里面调用ThreadLoca对象的set(T value)方法,T是泛型,value就表示要设置的变量。

set(T value)方法会自动获取当前线程的 ThreadLocalMap 对象,然后往这个 map 中插入一条记录, key 其实是 ThreadLocal 对象(一个线程可能会遇到多个 ThreadLocal 形式的变量,用以区分), value 的 set 方法传进去的值。

在线程结束时可以调用 remove() 方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。

每个线程都有一个自己的ThreadLocal.ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,在ThreadLocalMap中也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。ThreadLocalMap中使用了独特的开发地址法来解决hash冲突。

什么是公平锁和非公平锁?

公平锁就是指线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。

非公平锁是一种获取锁的抢占机制,是随机获取锁的,和公平锁的区别就是先来的不一定先得到锁,导致某些线程可能一直拿不到锁,所以是不公平的。

如何控制某个方法允许并发访问线程的个数?

Semaphore(信号量) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池限制,或者对容器施加边界。

Semaphore两个构造器:

  • Semaphore(int permits)
    创建一个 Semaphore与给定数量的许可证,非公平锁。
  • Semaphore(int permits, boolean fair)
    创建一个 Semaphore与给定数量的许可证,fair为true时为公平锁。

通过构造器可以指定锁的个数。

获取锁:acquire() 如果请求不到就一直阻塞,直到请求通过或者线程被中断。

释放锁:release() 使用完毕,让出资源。

Java中多线程之间的通信是如何实现的?

  1. 以共享变量的形式(在共享对象供设置信号,通过获取信号和改变信号来通信)
  2. wait / notify 机制

多线程共享数据如何实现?

分为两种情况:

  1. 多个线程行为一致,共同操作一个数据源

如果每个线程执行的代码相同,可以使用同一个 Runnable 对象共享数据直接定义在这个 Runnable 对象中。之后创建多个Thread对象传入同一个Runnable对象开启线程即可。卖票系统就是这么做的。

  1. 多个线程行为不一致,共同操作一个数据源

如果每个线程执行的代码不同,这时候需要用不同的 Runnable 对象,比如存款和取款。有两种方式实现数据共享:

a. 将共享数据封装在另外一个对象中逐一传递给各个 Runnable 对象(Runnable的构造器接收)。每个Runnable对象通过传入的这个对象来操作共享数据。这样容易实现针对该数据进行的各个操作的互斥和通信。

b. 将这些 Runnable 对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,外部类实现针对共享数据的操作方法来供内部类Runnable对象调用,以便实现对共享数据进行的各个操作的互斥和通信。

synchronized 和 volatile 关键字的区别?

volatile实现了针对变量操作的可见性和禁止指令重排序,其本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;

synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

  1. volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
  2. volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可以保证变量的修改可见性和原子性
  3. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  4. volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化(指令重排序)。

什么是线程池,如何使用?

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,使用完之后再归还给线程池。线程池作用就是限制系统中执行线程的数量,这样可以节省了开辟子线程的时间,提高的代码执行效率。

在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法:

  1. public static ExecutorService newFixedThreadPool(int nThreads)
    创建固定数目线程的线程池。
  2. public static ExecutorService newCachedThreadPool()
    创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。
  3. public static ExecutorService newSingleThreadExecutor()
    创建一个单线程化的线程池。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  4. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

调用他们的 execute 方法即可。

常用的线程池有哪些?

参照上个问题。

线程池可以带来哪些好处?

线程池的关键在于限制系统中执行线程的数量,合理使用线程池可以带来以下好处:

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

线程池的启动策略?

线程池刚刚创建好的时候,里面并没有线程。任务队列是作为参数传进来的,执行任务前,线程池会进行一系列的判断,而不是立刻执行任务。当使用execute方法提交一个任务时:

  1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程并运行这个任务;
  2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入任务队列等待被执行。
  3. 如果任务队列已满,而正在运行的线程数小于 maximumPoolSize ,创建新线程并执行这个任务;
  4. 如果任务队列已满,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

当一个线程的任务执行完之后,它会从任务队列中取下一个任务来执行;当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于
corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

非阻塞算法CAS

乐观锁与悲观锁

悲观锁:假设并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。共即享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁:假定并发环境是乐观的,虽然有可能发生并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用CAS(乐观锁的一种实现)实现的。

乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。首先保证正确性,有必要的话,再去追求性能。

“使用 CAS 控制并发”与“使用乐观锁”并不等价。 CAS 只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。

为什么要使用CAS算法

在多线程高并发编程的时候,最关键的问题就是保证临界区(指的是一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性)的对象的安全访问。

对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。

CAS(compare and swap)基于乐观策略,有如下优势:

  1. 天生免疫死锁 (根本就无锁,何来死锁)
  2. 更优越的性能:使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销。

CAS无锁实现原理

CAS的实现往往需要硬件的支持,多数处理器都都实现了一个 CAS 指令,实现“Compare And Swap”的语义(这里的 swap 是“换入”,也就是 set),构成了基本的乐观锁。

CAS 包含 3 个操作数:内存位置(V)、预期原值(A)和新值(B) 。

A表示要进行比较的预期旧值 ,B是拟写入的新值 ,当且仅当位置 V 的值等于 A 时, CAS 才会通过原子方式用新值 B 来更新位置 V 的值;如果V不等于A,就说明值已经被其他线程修改过了,直接将V值返回。

CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需担心线程安全问题。

  • 版本问题

有一个特殊情况是,比如 V 本来是 = A的,但是经过了 A - B - A的过程,就会误以为其他线程没有改动过这个值,从而影响下一步决策。一个解决方法就是给值加上版本号,比如1A - 1B - 2A 这种,这样就很容易判断了。

Java的线程并发库java.util.concurrent

java.util.concurrent自JDK 5之后加入Java平台,使得Java下的并发编程变得更加简单,强大。java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块:

  1. java.util.concurrent.atomic (多线程的原子性操作提供的工具类

  2. java.util.concurrent.lock (多线程的锁机制)

Executors 线程池工厂类

工厂和工具方法Executor , ExecutorService , ScheduledExecutorService ,ThreadFactory和Callable在此包中定义。

线程池的作用

线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

线程池作用就是限制系统中执行线程的数量。

  1. 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存导致服务器死机,或过少导致效率低下。

Executors 创建线程池

Executors 类提供工厂方法用来创建不同类型的线程池:

  1. public static ExecutorService newFixedThreadPool(int nThreads)
    创建固定数目线程的线程池。
  2. public static ExecutorService newCachedThreadPool()
    创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。
  3. public static ExecutorService newSingleThreadExecutor()
    创建一个单线程化的线程池。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  4. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

返回类型均为ExecutorService类或者它的子类。

Executor 执行器

Executor 是 Java 线程池的顶级接口,仅仅提供了execute(Runnable command)方法用来提交任务,且没有返回值。

ExecutorService 执行器服务

ExecutorService接口继承了Executor接口,提供了线程生命周期管理的方法,常用该接口来实现和管理多线程。

ExecutorService接口的实现类有:

  • ThreadPoolExecutor 可调整线程池中存活的线程数量
  • ScheduledThreadPoolExecutor 具备定时功能
  1. ExecutorService 的创建

可以选择实例化其实现类,也可以选择使用Executors的静态方法(上述)。

  1. ExecutorService 任务的提交

ExecutorService 中的execute(Runnable command)方法与Executor并无区别,但是由于无法接受返回值,ExecutorService提供了特有的提交任务的方法submit,是基于其父接口Executor的execute(Runnable command)扩展而来。不同于execute只能接受Runnable对象,submit方法可以接收Callable对象,这就意味着submit可以接收线程执行完毕的返回值(返回值用Future类封装)。submit方法的定义如下:

  • Futuresubmit(Callable task)
  • Future submit(Runnable task)
  • Future submit(Runnable task, T result)

如果传入的是Runnable对象,则返回的Future为null,但是可以用它来检测run()方法是否执行完毕。

ExecutorService 中同样提供了针对Callable任务列表的提交方法,然后可以等待全部任务或者部分执行完毕:

  • List> invokeAll(Collection tasks)

  • T invokeAny(Collection tasks)

invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 – 只能表明其中一个已执行结束。如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。

invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。 invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。

记住,一个任务可能会由于一个异常而结束,因此它可能没有 “成功”。无法通过一个 Future 对象来告知我们任务是否是正常结束还是因为异常而结束。

  1. ExecutorService 关闭任务

使用 shutdown()shutdownNow() 可以关闭线程池。二者的区别:

shutdown 只是将空闲的线程 interrupt 了, shutdown()之前提交的任务可以继续执行直到结束。

shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,是因为 interrupt()方法能力有限。

ThreadPoolExecutor 线程池执行者

ThreadPoolExecutor类是 ExecutorService 接口的一个实现。ThreadPoolExecutor 中的连接池大小可以动态变化

池中线程的数量由以下变量决定:corePoolSizemaximumPoolSize,称为核心线程数最大线程数。这两个值可以通过有参构造指定,也可以通过set方法改变。

当一个新任务被提交时,如果池中正在运行的线程数小于corePoolSize,那么将会有一个新的线程被创建去执行这个任务;如果正在运行的线程数大于corePoolSize但是小于maximumPoolSize,任务进入队列等待分配线程,并且只有在任务队列已满的情况下才会去创建新线程。

这种机制的核心在于维护核心线程数的数量。当核心线程数和最大线程数相等时,等同于设置了一个固定大小的线程池。

ScheduledThreadPoolExecutor定时线程执行者

ScheduledExecutorService 是 ExecutorService 接口的子接口。它能够将任务延后执行,或者间隔固定时间多次执行,完全可以用来代替定时器。

ScheduledThreadPoolExecutor 是它的实现类。

  1. ScheduledExecutorService的创建:
  • 通过Executors的内置静态方法 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 来构造;

  • 直接构造ScheduledExecutorService的实现类对象(ScheduledThreadPoolExecutor)来实例化。

  1. ScheduledExecutorService 的任务执行:
  •  schedule (Callable task, long delay, TimeUnit timeunit)指定延迟后单次执行Future任务,timeunit为延迟参数时间
  •  schedule (Runnable task, long delay, TimeUnit timeunit)指定延迟后单次执行任务,timeunit为延迟参数时间
  •  scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)指定延迟和间隔时间,周期性执行任务。上一个任务开始后计算间隔时间开启下一次任务。
  •  scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit) 创指定延迟和间隔时间,周期性执行任务。上一个任务结束后计算间隔时间开启下一次任务。
  1. ScheduledExecutorService 的关闭:

没有什么特别的方法,都使用继承自ExecutorService的 shutdown() 或 shutdownNow()方法来关闭线程。

并发队列

常用的并发队列有阻塞队列和非阻塞队列。前者使用锁实现,后者使用CAS非阻塞算法实现。都是Java并发库Java util.concurrent 下的重要组成部分。

并发队列之BlockingQueue阻塞队列

BlockingQueue 提供了线程安全的队列访问方式:

  1. 当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;
  2. 从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。

并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。BlockingQueue是一个接口,所有方法使用内部锁或其他形式的并发控制在原子上实现其效果。

  • 应用场景

BlockingQueue实现被设计为主要用于生产者 - 消费者队列,但另外支持Collection接口。BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景:

Java多线程与Concurrent并发包问题汇总_第1张图片

一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。

  1. BlockingQueue的方法

BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:

方法\处理方式 抛出异常 返回特殊值 true / false 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用

BlockingQueue不接受null元素。 使用其实现类尝试插入null值时会抛出NullPointerException异常。

  1. BlockingQueue的实现类
  • ArrayBlockingQueue: ArrayBlockingQueue 是采用数组实现的有界阻塞线程安全队列。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
  • LinkedBlockingQueue: LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
  • DelayQueue: DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。DelayQueue 内部是使用 PriorityQueue 实现的。DelayQueue = BlockingQueue +PriorityQueue + Delayed。可以理解为DelayQueue 是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。
  • PriorityBlockingQueue : PriorityBlockingQueue 是一个无界的并发队列。它使用了和类java.util.PriorityQueue 一样的排序规则 。无法向这个队列中插入null值 。所有插入到PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。PriorityBlockingQueue 始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。
  • SynchronousQueue: SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素(没有容器)。一个生产线程,当它生产产品(即put 的时候),如果当前没有人想要消费产品(即当前没有线程执行 take),此生产线程必须阻塞,等待一个消费线程调用 take 操作, take 操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。

介绍几个实现类常用的方法:

  1. boolean offer(E e) 将元素插入队列末尾,并返回true | false
  2. E peek() 获取队列头的元素,但是不删除它,如果队列为空则返回 null 。
  3. E poll() 获取队列头的元素并从队列中删除它,如果队列为空则返回 null 。
  4. boolean remove(Object o) 从该队列中删除指定元素的单个实例(如果存在)。
  5. boolean contains(Object o) 如果此队列包含指定的元素,则返回 true 。
  6. int size() 返回此队列中的元素数。

并发队列之非阻塞队列

与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。在底层,非阻塞队列使用的是 CAS(compare and swap)来实现线程执行的非阻塞。

ConcurrentLinkedQueue 非阻塞无界链表队列

ConcurrentLinkedQueue 是一个线程安全的队列,基于链表结构实现,是一个无界队列,采用的也是先进先出(FIFO)入队规则。ConcurrentLinkedQueue 使用 CAS 非阻塞算法实现使用 CAS 解决了当前节点与 next 节点之间的安全链接和对当前节点值的赋值。由于使用 CAS 没有使用锁,所以获取 size 的时候有可能进行 offer, poll 或者 remove 操作,导致获取的元素个数不精确,所以在并发情况下 size 函数不是很有用。

ConcurrentLinkedQueue 中有两个 volatile 类型的 Node 节点分别用来存在列表的首尾节点,其中 head 节点存放链表第一个 item (为 null) 的节点, tail 则并不是总指向最后一个节点。 Node 节点内部则维护一个变量 item 用来存放节点的值, next 用来存放下一个节点,从而链接为一个单向无界列表。

public ConcurrentLinkedQueue() {
	head = tail = new Node<E>(null);
}

初始化时候会构建一个 item 为 NULL 的空节点作为链表的首尾节点。

  • 如何实现线程安全?

可知入队出队函数都是操作 volatile 变量: head, tail。所以要保证队列线程安全只需要保证对这两个 Node 操作的可见性和原子性,由于 volatile 本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性:对于 offer 操作是在 tail 后面添加元素,也就是调用 tail.casNext 方法,而这个方法是使用的 CAS 操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取 tail,然后执行 casNext 方法。对于 poll 也是这样的。

ConcurrentHashMap 非阻塞 Hash 集合

ConcurrentHashMap 是 Java 并发包中提供的一个线程安全且高效的 HashMap 实现, ConcurrentHashMap在并发编程的场景中使用频率非常之高。

HashMap的死循环问题

  • HashMap的死循环问题-参考博客

HashMap的底层数据结构是数组+链表,数组(table)充当索引,链表解决冲突

put(key,value):

  1. 判断table是否为null或者size是否为0,如果是就resize;
  2. 根据hash算法计算key值然后返回索引 i ,看table[i]处是否为null,如果为空,就把node放入table[i]中;
  3. 如果table处不为null,就要进一步判断key值是否一致,如果一致,覆盖掉value;
  4. 如果key值不一致,就会产生冲突,解决冲突的方式是链表法,先判断是否table[i]为Tree节点,如果是,就插入到红黑树中;
  5. 如果不是,就直接插入到链表中。
  6. 插值结束,进行扩容判断,如果size太大,达到了capacity (默认16)的0.75(默认加载因子),就resize()扩容至当前的2倍。
  • 为什么扩容大小是 2 的幂?

——HashMap在根据hash值来计算索引时,为了尽可能的使元素分布均匀且提高运算效率,使用了位运算:

static int indexFor(int h, int length) {  
    return h & (length-1);  
}  

h是通过K的hashCode最终计算出来的哈希值,length是目前容量。使用 & 运算(同为1得1),当容量是2^n时,h & (length - 1) == h % length (取余),得到的结果即为索引 i。

  • hash冲突的解决方法

可以看出,如果有元素根据hash值计算出了相同的索引 i ,并且key值不一致,就会产生冲突。冲突的元素会以链表的形式放在同一索引下,这种解决方法叫做链表法,另一种解决冲突的方式是开放地址法,由ThreadLocal采用。

在最坏的情况下,如果所有元素都存在冲突,那么HashMap就会变成由数组变为链表,复杂度由 O(1)变成O(n),性能变差。JDK8对此进行了改进,当链表的长度超过8时,此后就会变成红黑树结构(复杂度为O(logn)),使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。

  • resize的重要性

从上文分析看出,resize()方法对于HashMap来说非常重要。因为table的大小直接影响冲突发生的概率,越小就越容易发生冲突,链表长度越大,查找时性能越差。

resize意味着需要重新建一张大小是当前2倍的新table,然后遍历数组,遍历链表,把元素再挨个重新放入新table中。这是一个相当耗费资源的事情。JDK1.8以前的版本中不管是put还是resize使用的都是头插法(作者认为越靠后插入的元素被查找的概率越大,放在前面可以提高查找效率),也就是说,新元素会放在链表的头部

  • 死循环的产生

头插法是造成多线程下HashMap产生死循环的原因死循环是在get()时发生的,但是却是在put后扩容时产生错误的

问题出在如果是多线程同时操作同一个hashMap,在都需要resize的情况下,由于头插法会改变next指针,多线程操作就有可能导致新table中某处形成一个循环链表,也就是链表尾部的元素的next直接指向头部元素而非null值

HashMap的get(key)会根据key值先返回value所在索引 i ,然后去遍历 table[i] 处的链表(如果有的话),直到找出key对应的value值。

但是如果这个链表是循环链表,而查找的key值不存在,就会发生死循环(next一直不为null,相当于遍历无限长的全是重复元素的链表),导致cpu 空转。

JDK1.8之后对这一问题进行了修复,作者统一使用尾插法(不管是put还是resize),HashMap死锁的问题得到解决

为什么不用HashTable?

HashTable是线程安全版的HashMap,但是,也仅仅是给所有的关键方法加上synchronized关键字,相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差

ConcurrentHashMap 底层原理

HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁只负责锁一段数据,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样便可 以有效地提高并发效率。这就是ConcurrentHashMap 所采用的"分段锁"思想:

Java多线程与Concurrent并发包问题汇总_第2张图片

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 是一种可重入锁 ReentrantLock ,扮演锁的角色,HashEntry 用于存储键值对数据。

整个 ConcurrentHashMap 由一个 Segment 数组组成。Segment 是子哈希表,也是数组+链表结构。一个 Segment 里维护了一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素。

当对 HashEntry 的数据进行修改时,必须首先获得它对应的 Segment 锁。并发环境下,对于不同 Segment 的数据进行操作是不用考虑锁竞争的。所以,对于同一个 Segment 的操作才需考虑线程同步,不同的 Segment则无需考虑。以默认的 concurrencyLevel = 16 来说,相当于可以同时支持16个线程并发。

HashEntry是目前我们提到的最小的逻辑处理单元了,其内部维护着链表结构:

static final class HashEntry<K,V> {
	final int hash;
	final K key;
	volatile V value;
	volatile HashEntry<K,V> next;
	//其他省略
}

Segment 类似哈希表,也会有负载因子loadFactor,和阈值 threshold ,以及特有的 concurrencyLevel 变量。这些值都可以选择根据构造方法指定,否则使用默认的 0.75,16,16。

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel){}

Segment 数组的大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于concurrentLevel,ssize 一定是大于或等于 concurrentLevel 的最小的 2 的次幂,这种处理方式与HashMap中定义initialCapacity类似,都是为了通过按位与的散列算法来定位 Segment 的 index。

ConcurrentHashMap 的 get 方法没有加锁 ,其中涉及到的共享变量都使用 volatile 修饰, volatile 可以保证内存可见性,所以不会读取到过期数据。

ConcurrentHashMap 的 put 方法加锁,只不过是锁粒度更细。

总的来说,ConcurrentHashMap 作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable 的全表锁在性能上的提升非常之大。

java.util.concurrent.lock 包

不同于内置同步和监视器,lock框架允许更灵活地使用锁(锁是用于通过多个线程控制对共享资源的访问的工具)和条件。本包下有三大接口:

  • Lock 接口:Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。主要的实现是 ReentrantLock
  • ReadWriteLock 接口: 读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。 它利用了这样一个事实:一次只有一个线程( 写入线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(读取线程)。
  • Condition 接口: 描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。 Lock替换synchronized方法和语句的使用, Condition取代了对象监视器的使用。

java.util.concurrent.atomic 包

java.util.concurrent 下的 atomic 包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据。

atomic类是通过自旋CAS操作volatile变量实现的

在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值 load 到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

要保证多线程操作最后得到正确的变量值,就要保证操作的原子性,atomic 的存在意义就在于此。

具体使用-参考博客

你可能感兴趣的:(Review,&,Summary)