目录
一.多线程
1.多线程的优点
提高程序的响应处理速度,提高CPU的利用率,压榨硬件的剩余价值。
2.多线程带来的问题?
多线程访问同一资源的问题。
现在的cpu是多内核的,理论上来说一个该是可以同时处理多个线程的。
3.并行执行和并发执行
并行执行:在同一个时间多个线程同时执行。
并发执行:在一个时间段内,多个线程交替执行 微观上是一个一个的执行,宏观上是同时执行。
二.并发编程的核心问题
也就是多个线程同时访问共享资源出现的问题。
不可见性,乱序性,非原子性
1.不可见性
在一个线程中对共享变量进行修改的时候,在另一个线程中调用不能够及时得到数据的更新。
2.乱序性
为了优化性能,有的时候会改变程序中语句的执行顺序。顺序发生变化之后就可能会对最终的结果发生改变,脱离原本的轨道。
3.非原子性
原子性表示着 "不可分"。一个或者多个操作在cpu执行的过程中不被中断的特征,称为原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。
CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。线程切换导致了原子性问题。
eg:++操作,在java中也就是一条语句,一次就执行完了,但是在底层他就要三条cpu指令。(共享变量读到工作内存-----执行+1操作------再将变量写回去)
4.JMM内存模型
Java内存模型(Java Memory Model)规范了Java虚拟机与计算机内存是如何协同工作的。
Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型--又称为Java内存模型。
JVM主内存与工作内存
JMM规定所有的变量都存放在JVM的主内存中的,而且每个线程都还有自己的工作内存,线程对主内存中的变量的操作都必须在工作内存中进行,不可以直接读写主内存中的变量。
工作内存中也就是存储了主内存中共享变量的一个副本。
就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。
5.如何解决不可见性和乱序性问题?
使用volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后:
在一个工作内存中操作共享变量之后,底层会将工作内存中的数据同步到其他现成的工作内存当中,使其能够立即可见(同步共享内存的值),就解决了不可见行的问题。
volatile关键字修饰后的变量,在执行时,不会让优化重排序,解决了乱序性的问题。
但是volatile无法解决非原子性的问题。
6.如何解决原子性问题?
首先加锁肯定是可以实现原子性,一次只保证一个线程对共享资源进行修改。
加锁:synchronized 和 ReentrantLock都可以实现。
在Java中还有一种不需要加锁,只靠原子类就能实现原子性的方法。eg:AtomicInteger
一般低并发的情况下,使用AtomicInteger。
在java.util.concurrent包下,定义了许多与并发编程相关的处理类,此包一般称为JUC。
在原子类中也存在值被volatile修饰的共享变量,保证了可见性。
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
//不加锁的方式,在多线程中实现++操作,满足原子性的
System.out.println(atomicInteger.incrementAndGet());
}
}.start();
}
原子类实现原子性:使用CAS+volatile
CAS(自旋)
CAS(Compare-And-Swap)比较并交换。
在一个线程中拿到主内存的共享变量放到工作内存当中,将拿到的这个值称为预期值,将在工作内存中修改后的值称为更新值,主内存中的之称为内存值。
假设++操作中,一个线程先从主内存中读取了共享变量存到了自己的工作内存中,然后在自己的工作内存中进行++操作,进行一顿操作后,当需要将更新值重新传会主内存时,先判断期望值和内存值是否一致,如果一致那就将更新值写回主内存,如果不一致,那么证明在此内存更新共享变量之前已经有其他的线程进行修改了共享变量,这个时候就不能再将更新值写回主内存,而是将期望值进行重新更新,再次进行操作,直到发现期望值与内存之一致时写回即可。
由于采用自旋方式实现,使得线程都不会阻塞,一直自旋,适合并发量低的情况.
如果并发了过大,线程一直自旋,会导致cpu开销大.
还会有一个ABA问题: 线程A拿到主内存值后,期间有其他线程已经多次修改内存数据,最终又修改的和线程A拿到值相同,
可以通过带版本号的原子类,每次操作时改变版本号即可.
CAS的缺点:
使用自旋锁的方式,由于该锁会不断循环判断,因此不会像synchronized那样线程阻塞导致线程切换。但是不断自旋会导致cpu消耗,在并发量大的时候容易导致 CPU 跑满。
三.锁分类
锁分类,不全是指java中的锁,有的指的是所的特征,有的指的是锁的状态。
1.乐观锁/悲观锁
乐观锁:是一种不加锁的实现,例如原子类,不加锁采用自旋的方式尝试修改共享数据,是不会有问题的。
悲观锁:是一种加锁实现,例如 synchronized 和 ReentrantLock,认为不加锁修改共享数据会出现问题。
2.可重入锁
可重入锁又名递归锁,当同一个线程获取锁进入到完成方法后,可以在内存中进入另一个方法(内层方法与外层方法使用的是同一把锁)。
class Demo{
public synchronized void testA(){
testB();
}
public synchronized void testB(){
}
}
synchronized和ReentrantLock都是可重入锁。
3.读写锁
ReentrantReadWriteLock
ReentrantReadWriteLock.ReadLock 读锁
ReentrantReadWriteLock.WriteLock 写锁
读读不互斥
读写互斥
写写互斥
只要是有线程在写,就不能进行其他操作,尽快地保证读的效率。
4.分段锁
将锁的粒度进一步细化,提高并发效率。
HashTable上也加了锁,但是它是在方法前面加的锁,加入有两个线程同时读,也只能一个一个地读,效率很低。
ConcurrentHashMap不是在方法前面加锁,它是使用hash表中的每个位置上的第一个对象做为锁对象,这样就可以多个线程对不同位置进行操作,相互不影响,只有对同一个位置操作时才互斥。
有多把锁对象,可以提高并发操作的效率。
5.自旋锁
线程不断尝试获取锁,当第一次获取不到时,线程不阻塞,尝试继续获取锁,有可能后面几次尝试之后,有其他线程释放了锁,此时就可以获得锁对象,当尝试次数达到一定次数时(默认为10次),仍然获得不了锁对象,那么就进入阻塞状态。
synchronized是自旋锁。
并发量低的情况下适合自旋锁。
6.共享锁/独占锁
共享锁:多个线程可以同时使用,被多个线程共享,eg:读写锁中的读锁。
独占锁:一次只能支持一个线程使用,eg:读写锁中的写锁,synchroniezd和ReentrantLock都是独占锁。
7.非公平锁/公平锁
公平锁:可以按照请求获得锁的顺序来得到锁。
非公平锁:不按照请求获得锁的顺序来得到锁。
synchronized是公平锁。
ReentrantLock默认是非公平锁。
调用无参构造默认指明一个非公平锁对象。
但是可以通过构造方法传递参数进行设置:
8.锁状态
java中为了对synchronized进行优化,提供了三种锁状态。
偏向锁: 一段同步代码块一直由一个线程执行,那么会在锁对象中记录下了线程信息,可以直接获得锁.
轻量级锁: 当锁状态为偏向锁时,此时又有其他线程访问,锁状态升级为轻量级锁,线程不阻塞,采用自旋方式获取锁.
重量级锁: 当锁状态为轻量级锁时,如果有大量的线程到来,大量的线程自旋,锁状态升级为重量级锁,自旋的线程会进入到阻塞状态,由操作系统去调度管理.
四.synchronized锁实现
synchronized是一个关键字,实现同步,需要提供锁对象,记录锁的状态,记录线程信息。
控制同步,是依靠底层的指令实现的。
如果是同步方法,在指令中会为方法添加ACC_SYNCHRONIZED标志
如果是同步代码块,在进入到同步代码块时,会执行monitorenter, 离开同步代码块时或者出异常时,执行monitorexit(两次monitorexit)
五.AQS
AQS全称为(AbstractQueuedSynchronizer)抽象同步队列,这 个 类 在 java.util.concurrent.locks 包下面。
AQS就是一个构建锁和同步器的框架,并发包(JUC)下面很多类都使用到了AQS,是JUC中的核心组件,ReentrantLock,CountDownLatch都使用到了AQS。
下面是AQS的大致结构,底层的队列是用双向链表实现,存在头尾节点,同时还有锁状态,在没有线程拿到锁时,锁状态是0,有线程拿到时,锁状态为1。
state使用volatile修饰,确保锁状态的可见性。
Node是AQS中的内部类。
public abstract class AbstractQueuedSynchronizer{
//内部类 节点,节点中存储的就是每个进来的线程
static final class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
private transient volatile Node head; //头节点
private transient volatile Node tail; //尾节点
private volatile int state; //锁的状态
//修改状态的方法
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
六.ReentrantLock锁实现
完全由java代码实现,ReentrantLock是Java中的一个类。
ReentrantLock是java.util.concurrent.locks包下的类,实现Lock接口。
ReentrantLock其中有三个内部类。
Sync NonfairSync(非公平锁) FairSync(公平锁)
class ReentrantLock{
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
//非公平锁
static final class NonfairSync extends Sync {
void lock(){
}
}
//公平锁
static final class FairSync extends Sync {
void lock(){
}
}
}
ReentrantLock可以在构造方法中确定使用非公平锁还是公平锁,但是无参构造方法中默认规定的是非公平锁。
非公平锁和公平锁的区别:
非公平锁获取锁对象:
公平锁获取锁对象:
七.JUC常用类
在集合类中,像Vector,HashTable这些类加锁都是在方法前面加synchronized,性能很低,在并发量小的情况下还可以使用,在并发量大的情况下,性能就太低了。
1.ConcurrentHashMap
HashMap是单线程的,不允许多个线程同时访问操作,同时访问的话会出异常。
HashTable是线程安全的,允许多个线程同时访问操作,但是是在方法前面加的锁,效率低。
ConcurrentHashMap也是线程安全的,但是不在方法前面加锁,用哈希表上的第一个位置作为锁对象。
哈希表的长度是16,就有16把锁对象,锁住自己的位置即可,这样多个线程访问的是不同的位置就不会等待,只有访问的是同一个哈希表位置时才会等待。
如果位置上没有任何元素,那么久使用CAS机制插入到位置上即可。
注意:ConcurrentHashMap和HashTable 的键值都不能为null。
为什么要设计键值不能为null?
为了消除歧义。
eg:System.out.println(map1.get("a")); 打印出来是null,这个时候就不知道是不存在键为“a”的数据还是键为“a”的value为null。
2.CopyOnWriteArrayList
ArrayList是单线程的
Vector是线程安全的,但是还是在方法前面加的锁,甚至get()方法上面也加了锁,读读也互斥。CopyOnWriteArrayList 写方法操作加了锁(ReentrantLock实现的),在写入数据时,先把原数组做了**备份**,把要添加的数据写入到备份数组中,当写入完成后,再把修改的数组赋值到原数组中去给写加了锁,读没有加锁,读的效率变高了, 这种适合写操作少,读操作多的场景.
3.CopyOnWriteArraySet
底层是和CopyOnWriteArrayList底层是一样的,只不过不能存储重复元素。
4.辅助类 CountDownLatch
CountDownLatch 允许一个线程等待其他线程各自执行完毕后再执行。底层实现是通过 AQS 来完成的.创建 CountDownLatch 对象时指定一个初始值是线
程的数量。每当一个线程执行完毕后,AQS 内部的 state 就-1,当 state 的值为0 时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
CountDownLatch countDownLatch=new CountDownLatch(5); //new 了5个线程对象
每使用一个线程 state 就 -1,减到0所有线程就执行完毕。
八.池的概念
1.字符串常量池
String a1="123";
String a2="123";
System.out.println(a1==a2); //true
由于字符串常量池的存在,避免了大量创建同一字符串,节约了资源,但是如果是使用的是new关键字创建字符串时,就不会存放到字符串常量池当中。
String a1=new String("123");
String a2=new String("123");
System.out.println(a1==a2); //false
2.Integer自动装箱
Java中自动封装(缓存)了(-128,127)之间的数据。
Integer a1=12;
Integer a2=12;
System.out.println(a1==a2); //true
3.数据库连接池
阿里巴巴Druid数据库连接池
帮我们缓存一定数量的链接对象,放在池子里,用完还回到池子中,
减少了对象的频繁创建和销毁的时间开销
4.线程池
为了降低频繁创建和销毁线程,提高cpu的利用率。
建议使用ThreadPoolExecutor类来创建线程池,这样可以提高效率。
Java.uitl.concurrent.ThreadPoolExecutor
ThreadPoolExecutor中的七个变量。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
七个参数
corePoolSize:核心线程池中的数量(初始化的数量)
maximumPoolSize:线程池中最大的数量
keepAliveTime:空闲线程存活时间 当核心线程中的线程足以应付任务时,非核心线程池中的线程在指定空闲时间到期后会销毁。
unit:时间单位
workQueue:等待队列,当核心线程池中的线程都在使用时,会先将等待的线程放到等待队列中,等到等待队列也放满之后才会去创建非核心线程池中的线程。
threadFactory:线程工厂,用来创建线程池中的线程
handler:拒绝处理任务时的策略 共有4种拒绝策略。
线程池的执行
当一个任务进来线程池准备执行任务时,
会先判断核心线程池中是否有空闲线程能够执行该任务,
如果能执行就直接交给和核心线程池中的线程进行执行任务,
如果没有空闲线程那么就将任务排到等待队列当中进行等待,
当核心线程中有空闲线程就会依次执行等待队列当中的线程,
如果等待队列也已经存满线程,那么就判断线程池中是否已满,
如果没满那么就创建非核心线程池中的线程进行执行任务,
在如果线程池中已经没有了空间,那么就使用构造方法中定义的拒绝策略处理。
线程中的等待队列
1.ArrayBlockingQueue:用数组实现,创建时必须设置长度,按FIFO排序量。
2.LinkedBlockingQueue:底层使用链表实现,按FIFO排序任务,容量可以选择进行设置,不设置的话就是一个最大长度为Integer.MAX_VALUE。
4中拒绝策略
1.AbortPolicy策略:抛出异常
2.CallerRunsPolicy:有提交任务的线程执行,例如在main线程提交,则有main线程执行拒绝的任务
3.DiscardOldestPolicy:丢弃等待时间最长的任务(也就是等待线程中的第一个线程)。
4.DiscardPolicy:丢弃最后执行的任务(最后一个进来的任务)。
execute 与 submit 的区别
这两个方法都是可以进行提交任务的。
但是excute()没有返回值,void
submit()有返回值
关闭线程池
关闭线程池可以使用shutdown()和shutdownNow()两个方法实现。
区别:
shutdown():执行shutdown()方法之后,会把线程池中还有等待队列中已有的任务执行完再停止。
shutdownNow():立即停止,队列当中的等待任务就不再执行了。
九.ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填的是当前线程,该变量对其他线程而言都是相互隔离的。
ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal底层分析
ThreadLocal的底层最重要的其实就是每个线程中独有的ThreadLocalMap对象,使用ThreadLocalMap来存储变量副本。
ThreadLocalMap是懒加载的模式,在第一次添加元素的时候才会创建出来。键----ThreadLocal对象,值------value在set() get()方法中,开始都是先通过 Thread t = Thread.currentThread(); 和 ThreadLocalMap map = getMap(t); 拿到当前线程和当前线程中的那个ThreadLocalMap对象,再对每个线程中的变量副本来进行存取,所以线程之间就是相互隔离的。
ThreadLocal内存泄漏
TreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,这样就会导致ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,只有 thead 线程退出以后,value 的强引用链条才会断掉。
4种对象引用
强引用
Object obj = new Object(); 强引用
obj.hashCode();
obj=null; 没有引用指向对象
对象如果有强引用关联,那么肯定是不能被回收的
软引用
被SoftReference类包裹的对象, 当内存充足时,不会被回收,当内存不足时,即使有引用指向,也会被回收。
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<Object>(o1);
弱引用
被WeakReference类包裹的对象,只要发送垃圾回收,该类对象都会被回收掉,不管内存是否充足。
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<Object>(o1);
ThreadLocal 被弱引用管理static class Entry extends WeakReference<ThreadLocal<?>> {}
当发生垃圾回收时,被回收掉,但是value还与外界保持引用关系,不能被回收. 造成内存泄漏
虚引用
被PhantomReference类包裹的对象,随时都可以被回收,
通过虚引用对象跟踪对象回收的状态
解决内存泄漏的方法
每次使用完 ThreadLocal 都调用它的 remove()方法清除数据。
threadLocal.remove();//不再使用时,调用remove方法,删除键值对,可以避免内存泄漏问题
案例分析
package org.example;
public class ThreadLocalDemo {
public static int test=1;
public static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1;
}
};
public static void main(String[] args) {
new Thread(()->{
threadLocal.set(10);
System.out.println("线程1:"+threadLocal.get());
}).start();
new Thread(()->{
threadLocal.set(20);
System.out.println("线程2:"+threadLocal.get());
}).start();
threadLocal.remove();
}
}