今天是Java并发
1.并发编程的三要素
原子性(Synchronized, Lock):
一个或者多个操作,要么都执行,要么都不执行。
有序性(Volatile,Synchronized, Lock):
即程序执行的顺序按照代码的先后顺序执行。
可见性(Volatile,Synchronized,Lock):
当多个线程访问同一个变量的时候,如果该变量的值被修改,name它要保证被修改的值要立刻能看到修改的值
2.实现可见性的方法有哪些?
1.final
初始化字段为final,被final修饰的变量无法修改
final boolean isRunning = true;
2.volatile
读写volatile确保可见性
volatile boolean isRunning = true;
2.1.1保证可见性,不保证原子性,一个线程对共享变量修改,其他线程可以马上看到共享变量的最新值
2.1.2.禁止进行指令重排序
3.synchronized
同步块内读写字段确保可见性,也是计算机操作系统中生产者消费者1常用的方法块修饰。
synchronized (this){
demo.i++;
}
class CubbyHole {
//仓库类
private int contents;
private boolean available = false;
public synchronized int get() {
while (available == false) {
try {
wait();
}
catch (InterruptedException e) {
}
}
available = false;
notifyAll();
return contents;
}
public synchronized void put(int value) {
while (available == true) {
try {
wait();
}
catch (InterruptedException e) {
}
}
contents = value;
available = true;
notifyAll();
}
}
3.多线程的价值?
1、发挥多核 CPU 的优势
多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多 线程的方式去同时完成几件事情而不互相干扰。
2、防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因 为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但 是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使 用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未 返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数 据阻塞,也不会影响其它任务的执行。
3、便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么 就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成 几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运 行这几个任务,那就简单很多了。
4.创建线程有哪些方法?
一、继承Thread类创建线程类
二、通过Runnable接口创建线程类
三、通过Callable和Future创建线程
5.创建线程的三种方式比较?
采用实现Runnable、Callable接口的方式创见多线程时,优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
6.线程的状态流转图
Java的线程可以有多种状态,在Thread.State类中定义了6个常量来表示线程的状态,分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED,下面是比较详细的一幅状态流转图:
7.Java线程具有五种基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread thread1 = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法[ 如:thread1 .start(); ],线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了thread1 .start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;(wait()会释放持有的锁)
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意:sleep()是不会释放持有的锁)
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
8.什么是线程池?有哪几种创建方式?
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,
而是仍然在线程池中等待下一个任务。
-
通过Executors工厂方法创建
-
通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)自定义创建
ThreadPoolExecutor构造函数重要参数分析
- 最大线程数maximumPoolSize
- 核心线程数corePoolSize
- 活跃时间keepAliveTime
- 阻塞队列workQueue
- 拒绝策略RejectedExecutionHandler
9.线程池的优点
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
10.常用的并发工具类有哪些?
-
CountDownLatch(闭锁,我觉得叫门闩更好理解)
CountDownLatch是一个同步计数器,初始化的时候传入需要计数线程的等待数,可能是等待执行完的线程数,或者大于;调用多个线程之间的同步,或者说起到线程之间的通信(不是互斥)一组线程等待其他线程完成工作之后在执行,相当于加强的join
-
CyclicBarrier(栅栏)
CyclicBarrier字面意思就是栅栏,是多线程中的重要的类,主要用于线程之间互相等待的问题,初始化的时候传入需要等待的线程数作用:让一组线程达到某个屏障被阻塞直到一组内最后一个线程达到屏蔽时,屏蔽开放,所有被阻塞的线程才会继续运行其中:
-
Semophore(信号量)
semaphore又名信号量,是操作系统的一个概念,在Java并发编程中,信号量控制的是线程并发的数量
作用:semaphore管理一系列许可每个acquire()方法阻塞,直到有一个许可证可以获得,然后拿走一个许可证,每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法,然而并没有实际的许可保证这个对象,semaphore只是维持了一个可获取许可的数量,主要控制同时访问某个特定资源的线程数量,多用在流量控制
其他semaphore的底层实现就是基于AQS的共享锁实现的
如果一个线程要访问共享资源,必须先获得信号量,如果信号量的计数器数值大于1,意味着有共享资源可以访问,使其计数器减1,再访问共享资源,如果计数器为0,线程进入休眠,当某个线程使用完共享资源后,释放信号量,并将信号量的内部计数器加1,之前进入休眠的线程将被唤醒并再次试图获取信号量
-
Exchanger(交换器)
Exchange类似于一个交换器可以在队中元素进行配对和交换的线程的同步点,用于两个线程之间的交换
具体来说,Exchanger类允许两个线程之间定义同步点当两个线程达到同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程当中,第二个线程的数据结构进入到第一个线程当中。
11.synchronized关键字的作用?
-
并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
-
然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
-
尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
-
第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
-
以上规则对其它对象锁同样适用
12.volatile关键字的作用
被volatile关键字修饰的变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
13.什么是CAS
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
14.CAS的问题
-
容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。
-
造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。
15.什么是Future?
在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。
Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
16.什么是AQS
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
17.AQS支持的两种同步方式?
1、独占式
2、共享式
18.什么是悲观锁和乐观锁
悲观锁:悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁主要分为共享锁和排他锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
CAS是一种乐观锁,简单翻译就是compare and swap,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。但是还是会存在ABA问题,就是该位置数值由A变为B再变为A,实际上改变了,但在ComPare过程中认为没发生改变,就开始Swap,解决办法就是版本号控制
变成 1A→2B→3A,就可以避免ABA这种情况。