目录
回顾线程
并发编程
并发编程
Java 内存模型(JMM)
编程核心问题--可见性,原子性,有序性
可见性
有序性
原子性
valatile 关键字
CAS(Compare-And-Swap,比较并交换)
原子类
java中的锁
乐观锁/悲观锁
可重用锁(递归锁)
读写锁
分段锁
自旋锁
独占锁/共享锁
公平锁/非公平锁
偏向锁/轻量级锁/重量级锁
Synchronized锁实现
AQS(AbstractQueuedSynchronizer)抽象同步队列
AQS的锁模式:独占和共享
ReentrantLock实现
JUC常用类
ConcurrentHashMap(并发安全的map)
CopyOnWriterArrayList
CopyOnWriterArraySet
辅助类 CountDownLatch(递减计数器)
编辑 CyclicBarrier
线程池
线程池参数
线程池的执行
4种拒绝策略
关闭线程池
对象引用
ThreadLocal 线程变量
线程与进程之间的关系
进程:是操作系统分配系统资源的基本单位。
线程:一个进程里面有多个线程,线程是进程内的执行单位,是进行系统调度的最小单位。
如何创建线程?
继承Thread类
实现Runnable接口:
实现Callable接口:可以抛出异常,需要借助FutureTask类,获取返回结果,支持泛型的返回值
常用方法
run();sleep();wait();start();stop();wait();join();notify();notifyAll();yield();
wait()和sleep()方法的区别
wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。
线程状态
创建状态、就绪状态、运行状态、阻塞状态、销毁/死亡状态
线程由运行状态如何转换为阻塞状态?
IO阻塞、wait()、join()、等待同步锁、sleep();
多线程
在同一个进程中,有多个线程可以访问同一共享资源,java语言支持多线程
优点:提高程序的处理速度,提高CPU的利用率。
缺点:多个线程对同一共享资源数据进行访问
如何解决多线程访问同一共享资源?
加锁:
1. 使用synchronized关键字
静态方法:锁对象是类的Class对象
非静态方法:锁对象是this
修饰代码块:对某一段代码进行加锁控制,需要自己传入锁对象,并且要求锁对象是唯一的。
2. 使用Lock接口下的实现类
是类,只能通过修饰代码块,显式的加锁和释放锁,底层通过java代码赖进行控制实现,Lock(),unLock();只能手动的添加,手动的释放。
守护线程和用户线程
守护线程:任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作; 只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。例如:垃圾回收器
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
注意:设置线程为守护线程必须在启动线程之前,否则会跑出一个 IllegalThreadStateException异常。
线程死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁。
线程间的通信
指多个线程通过相互牵制,相互调度,即线程间的相互作用。
wait()、notify()和notifyAll() 必须在同步代码块中执行
生产者、消费者模式(消息队列组件 MQ)
多线程访问共享数据,会出现安全问题。
并发(concurrent)与并行 (Parallel)
并行:微观上同时执行,在同一时间点上,同时做多件事情。
并发:宏观上同时执行,多件事情在同一时间段内,交替执行。
在很多线程对共享资源进行访问,需要通过控制,让多个线程并发的对共享数据访问。
多线程执行本质问题:
由于CPU、内存、硬盘三者之间的读写速度不一样。
先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回主内存中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
一个线程对共享变量的修改,另一个变量也会立即会看到,这种情况我们称为可见性。如今的多核处理器,每个 CPU 内核都有自己的缓存,而缓存仅仅对它所在的处理器内核可见,CPU 缓存与内存的数据不容易保证一致。
一个或多个操作在 CPU 执行的过程中不被中断的特性,我们称为原子性。 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。 CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。线程切换导致了原子性问题。
两个线程 A 和 B 同时执行 count++, 即便 count 使用 volatile 修辞,我们预期的结果值是 2,但实际可能是 1。
缓存(CPU中)导致的可见性问题,编译优化带来的有序化问题,线程切换带来的原子性问题。
修饰的变量 可以保证可见性(一个内存更改的数据)
禁止指令重排序
不能保证原子性?
解决:原子性(CAS),加锁
volatile 底层是如何实现的?
在对volatile修饰的变量读写操作前,可以添加内存屏障指令,禁止在该条指令执行前插入其他指令,在工作内存修改后,结合缓存一致性协议,将工作内存数据更新到主内存,其他内存读取更新。
CAS是乐观锁(没有采用加锁的方式)的一种实现方式,使用自旋锁(一遍一遍的获取)思想去比较。
内部三个值:
内存值 V:操作前先将内存值读到工作内存。
预期值 A:在工作内存修改了变量后,将要将修改后的值向主内存写入的时候,再次读取的内存数据。
更新值 B:内部操作后的变量值。当向主内存写入数据时,必须满足V==A,就将V=B,否则就再次读入内存值。
优点:没有加锁,效率高于锁
缺点:CAS是无锁的,采用自旋的方法,线程不会阻塞,如果有大量的线程进行尝试,那么CPU就消耗较大(适合低并发量较小的情况)
ABA问题
就是内存中某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值与内存值相同,当前线程的 CAS 操作无法分辨,当前 V 值是否发生过变化。
解决:加入版本号,每次修改都要更新版本号。
解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。
如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。
不全是指锁,有的指的是锁的特性,有的是锁的状态,有的是锁的设计
乐观锁:采用CAS机制,乐观认为不加锁是没有问题的。(适用于读操作,例:原子类)
悲观锁:采用加锁的方式实现,悲观的认为不加锁是会有问题的。(适用于写操作)
指当一个线程进入外层方式获取锁后,如果内存调用另一个需要获取该锁的方法时,会自动的获取该锁修饰的方法,那么线程是可以进入的。
优点:可以一定程度上避免死锁。
里面维护两个锁的实现,一个是读锁,一个是写锁,如果使用的写锁,一次只能有一个线程进入;如果使用的是读锁,可以允许多个线程同时存在;写锁的优先级高于读锁。
不是具体的所,将锁的粒度分的更小,以提高并发效率
不断尝试去尝试获得锁,不会让线程进入阻塞状态,提高效率,但是耗CPU
独占锁
ReentrantLock、synchronized,读写锁中的写锁或者互斥锁,一个只允许一个线程获取锁。
共享锁
读写锁中读锁是共享的,可以有多个线程同时获取锁
公平锁
可以按照请求顺序分配锁。ReentrantLock中有公平锁实现,里面维护一个队列,按顺序排队获取锁。
非公平锁
不按照请求顺序分配锁。synchronized非公平锁和ReentrantLock默认为非公平锁(可变为公平锁)
偏向锁:只有一个线程访问一段同步代码,此时会将线程的id存入到对象头中,下次该线程来获取锁的时候,直接分配即可。
轻量级锁:当锁的状态为偏向锁时,又有线程访问,那么锁状态升级为轻量级锁,不会让线程进入阻塞状态,而是自旋尝试获得锁,以提高效率。
重量级锁:当锁的状态为轻量级锁,如果线程数量的太多,线程的自旋次数达到一定数量,锁的状态变为重量级锁,线程进入阻塞状态,等待操作系统调度分配。
在synchronized中的锁有三种状态,为了优化synchronized,在jdk 6之后提出的,针对不同的状态进行不同处理。
synchronized是关键字,可以修饰方法、代码块、一次只允许一个线程进入。
synchronized如果修饰方法
在编译后的指令中添加ACC_SYNCHRNIZED表示次方法是同步方法,有线程进入后其他线程不能进入,在对象头中锁标志+1,方法运行结束或出现异常,锁标志 -1。
synchronized如果修饰代码块
在进入代码之前加入monitorenter指令,对象锁标志+1,同步代码块运行结束或出现异常,执行monitorexit指令,锁标志-1。
JUC java.util.concurrent java并发包 FIFO 先进先出队列
实现原理
AQS是JUC中实现线程安全的核心组件,是从java代码级别实现内部维护锁的状态,用volatile关键字修饰state,内部维护一个FIFO队列,保存未获取到锁的线程。多个线程来访问,如果有一个线程访问到了state,就将其改为1,其他线程获取失败后,就会添加到队列中,Node(Thread)。
就是说,多个线程来访问,如果有一个线程访问到了volatile关键字修饰state(标志位),就将其改为1,这时,其他的线程获取失败后,就会添加到FIFO队列中,等待该线程结束后,将state改为0,然后等待下一个线程访问state。队列由 Node 对象组成,Node 是 AQS 中的内部类。
它还维护一些获取锁,添加线程到队列,释放锁的一些方法。
private transient volatile Node head ;private transient volatile Node tail ;使用变量 state 表示锁状态,0-锁未被使用,大于 0 锁已被使用共享变量 state,使用 volatile 修饰保证线程可见性private volatile int state ;
状态信息通过 getState , setState , compareAndSetState 进行操作.
获得锁状态protected final int getState () {return state ;}设置锁状态protected final void setState ( int newState) {state = newState;}使用 CAS 机制设置状态protected final boolean compareAndSetState ( int expect, int update) {return unsafe .compareAndSwapInt( this , stateOffset , expect, update);}
获取锁的方式有两种
AQS 操作重点方法
acquire: 表示一定能获取锁
public final void acquire ( int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter( Node . EXCLUSIVE ), arg))selfInterrupt();}
tryAcquire:尝试获取锁,如果tryAcquire获取锁成功,那么!tryAcquire(arg)为false,说明已经获取了锁,不用向后执行,直接返回。
addWaiter:尝试获取锁失败后,将当前线程封装到一个Node对象中,添加到队尾,并返回Node节点。
acquireQueued:将线程添加到队列中,以自旋的方式去获取锁。
release 释放锁
tryRelease:释放锁,将state值进行修改为0
unparkSuccessor:唤醒节点的后继者(如果存在)
独占锁:每次只能有一个线程持有锁,比如 ReentrantLock 就是以独占方式实现的互斥锁
ReentrantLock 是向外界提供的所实现的类,内部包含三个内部类(Sync,FairSync,NonfairSync)。
构造方法 无参方法 默认为非公平锁 public ReentrantLock() { sync = new NonfairSync(); } 有参方法 fair == true 为公平锁,fair == false 为非公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }NonFairSync extends Sync 非公平实现
static final class NonfairSync extends Sync {加锁final void lock () {//若通过 CAS 设置变量 state 成功,就是获取锁成功,则将当前线程设置为独占线程。//若通过 CAS 设置变量 state 失败,就是获取锁失败,则进入 acquire 方法进行后续处理。if (compareAndSetState( 0 , 1 )) // 没有排队,直接尝试去获取锁setExclusiveOwnerThread( Thread . currentThread ());elseacquire( 1 );// 获取锁,表示一定能获取锁,获取不到就继续}//尝试获取锁,无论是否获得都立即返回protected final boolean tryAcquire ( int acquires) {return nonfairTryAcquire(acquires);}}
Lock():尝试获取锁,肯定是能获取到锁的,直到有线程获取到锁,lock()方法就运行结束了。
unlock():释放锁
JUC java.util.concurrent java并发包
HashMap:线程不安全的,使用于单线程情况,键值可为空。
HashTable:是线程安全的,支持多线程并发访问(低),但是锁是加在put()方法上的,效率低,一次只能有一个线程进入到put()方法中进行操作,键值都不能为空。
ConcurrentHashMap
多线程并发访问安全的,将锁不在put()方法上添加,而是将每个位置看作是独立的空间,添加元素,通过Hash值计算它的位置,如果位置上还没有任何元素,采用CAS机制判断,添加元素到第一个位置,如果有元素,则使用头结点作为锁标记对象。
优点:实现了锁粒度变小,提高了并发效率。
为什么HashMap可以有null值,HashTable不能有空值?
Hashtable:
Hashtable不支持存储 key == null 或 value == null的值,则抛出空指针异常。
HashMap:
当key == null ,存储在哈希值为0的位置;
ConcurrentHashMap 不支持存储 null 键和 null 值。
从底层源码我们可以看出,当键或值为null值的时候,我们会抛出空指针异常。
原因:ConcurrentHashMap用于多线程,为了消除歧义,无法分辨key是没有找到的null还是key值为null,当get(key)获取对应的value时,如果获取到的是null时你无法判断它是put(K,V)的时候value是空的,还是这个key没有对应的值(无映射)。
Vector
由源码我们可以看出来,Vector线程安全的 但是对读(get())和写(add())操作也加了锁, 读和写用的是同一把锁。
添加、查询的方法都添加了同步锁,在写操作时,其他线程是不能读的,效率很低。操作时经常读操作是比较多的,但是读不会改变数据。一次只允许一个线程读数据。
CopyOnWriterArrayList
CopyOnWriterArrayList读(get())和写(add())进行分离,读操作完全不用加锁,读不影响数据,写操作加锁,写操作时不影响读操作,只有两个线程同时添加时会互斥。
给add(),set()会影响数据的操作方法添加锁,保证修改数据是安全的,写入数据时,会先创建新的数组,将新添加的元素写入到新的数组中,写完之后在将新数组赋给底层原来数组的引用。而读没有做任何的控制。
使一个线程,等待其他线程执行结束后再执行,相当于一个线程计数器,是一个递减的计数器。先指定一个数量,当有一个线程执行结束后就减一 直到为0,关闭计数器,这样线程就可以执行了。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch downLatch = new CountDownLatch(6);//计数
for (int i = 0; i <6 ; i++) {
new Thread(
()->{
System.out.println(Thread.currentThread().getName());
downLatch.countDown();//计数器减一操作
}
).start();
}
downLatch.await();//关闭计数
System.out.println("main线程执行");
}
}
执行结果:
让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,是一个加法计数器,当线程数量到达指定数量时,开门放行。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier c = new CyclicBarrier(5, ()->{
System.out.println("大家都到齐了 该我执行了");
});
for (int i = 0; i < 5; i++) {
new Thread(
()->{
System.out.println(Thread.currentThread().getName());
try {
c.await();//加一计数器
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
).start();
}
}
}
执行结果:
为什么要用池?
每次连接数据库创建连接对象,用完销毁,比较费时,增大了时空开销。而使用池,可以事先创建出一些连接对象放入池中,每次使用都从池子获取,用完还回到池子里面。
线程池:jdk5之后提供java内置版本,提供ThreadPoolExecutor类实现创建线程(推荐使用)
优点:
线程中也有一些参数对线程池进行设置
线程队列:
execute 和 submit 区别
相同点:都用来执行任务
不同点:excecute是没有返回值的,而submit是有返回值的
概述
除了垃圾回收标记垃圾对象之外,还需要对垃圾对象进行撞他分类管理。
强引用:有引用指向的对象,不是垃圾对象。
例:Object object = new Object();
软引用(内存不足即回收):使用SoftReference来管理对象。已经是垃圾了,如果内存够用,不回收,否则,将软引用管理的对象进行回收。
弱引用(发现即回收):使用WeakReference来管理对象,只能存活到下一次垃圾回收
虚引用(对象回收跟踪机制):使用PhantomReference来管理对象,需要提供一个队列维护,随时可以被回收,主要就是记录跟踪对象是否被回收。
创建一个ThreadLocal对象,复制用来为每个线程会存一份变量,实现线程封闭。
底层实现原理
ThreadLocal内部维护了一个Map,ThreadLocal实现了一个叫做 ThreadLocalMap 的静态内部类。
当有一个线程进入,在每个线程中,都会创建一个ThreadLocalMap对象,将ThreadLocalMap放入线程中,第一次创建的时候,在set()方法中,会将当前线程ThreadLocal作为键(this),将value做为值,存入到ThreadLocalMap中。
在调用get方法时,会调用当前线程对象的来在ThreadLocalMap里面进行查找对应的值。
有什么问题?
存在内存泄露的问题
如何解决内存泄露问题?
原因:ThreadLocal作为key,被弱引用(WeakReference)进行管理(存活到下一次垃圾回收),而值value是强引用的,与ThreadLocalMap和Thread强关联。key回收而value去还存在,造成大量无用的对象存在,但却又回收不掉,造成内存泄露。
解决:用完变量之后,调用THreadLocal中的remove()删除键值对象(用完即删)。
在Java中多线程额外开销最主要的原因就是锁竞争,因为获取锁会导致串行操作同时获取锁失败会导致线程挂起出现上下文竞争。所以减少锁竞争是降低多线程开销最主要的手段。
而在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。而影响锁竞争的因素:锁请求频率,每次持有锁的时间,如果两个的乘积很小,说明获取锁和持有锁的总时间就很少,那么大多数获取锁的操作都不会发生竞争。
所以减少锁竞争的主要方法是:减少锁的只有时间、降低锁的请求频次、或者使用带有协调机制的独占锁替代,因为这些机制允许更高的并发性。
第一个是缩小锁的范围:将与锁无关代码移除同步代码块,尤其是那些可能发生阻塞的操作比如I/O;
第二个是减少锁的粒度:使用多个相互独立锁管理独立的状态变量,改变某个变量只用获取对应变量锁,而不用获取整体锁,其他线程仍然能使用其他变量。但是使用锁越多,那么发生死锁的风险也就越高。
第三个是锁分段:比如ConcurrentHashMap底层的链表数组,对数组中每一个数组元素进行加锁,数组长度是多少就有多少个锁,也就最大支持多少并发。不过在对数组扩张的时候就会更加复杂;