ThreadLcoal
ThreadLcoal是什么?
threadLocal实际上是线程中threadlocals的key(threadlocals是线程内部的一个ThreadMap),它依赖于Thread类中的ThreadLocalMap,当调用set(T value)时,ThreadLocal将自身作为Key,值作为Value存储到Thread类中的ThreadLocalMap中,这就相当于所有线程读写的都是自身的一个私有副本,线程之间的数据是隔离的,因此保证了线程并发安全问题,另外threadLocal还能简化参数传递的繁杂性;
注意:一个ThreadLocal只能标记标记一种类型,比如标记ThreadLocal dateFormatThreadLocal
= new ThreadLocal()
ThreadLocal为什么引起内存泄漏?
ThreadLocal的出现内存泄露问题一般是在与长时间存在的线程搭配使用时出现的,线程中的ThreadMAp的key是一种弱引用,当遇到GC时会被剔除内存,而ThreadLocal的Value普遍存在的强引用,即使遇到GC也不会被剔除
因此,再一次GC时会造成一种key为null,value缺具体存在的一种情况,也就是内存泄漏。如果线程存在的时间极其的短,也就是说在gc发生之前,此线程就被销毁,一般不会发生内存泄漏。
如何解决内存泄漏问题?
有两种方法:
- 如果threadLocal所指定的线程独享对象是一种类似SimpleDateFormat的对象,可以采用static final等方式对ThreadLocal进行声明,避免ThreadLocal频繁的创建,
- 如果threadLocal所指定的线程独享对象是类似用户信息的对象,则采用remove()的当时。
锁
synchronized
synchronized是一把同步锁,具有排他性,这相当于线程由并行执行变成串行执行,保证了线程的安全性,但是损失了性能。在jdk1.6版本之前,synchronized是一把重量级锁,但在1.6之后引入了锁升级的机制,提高了synchronized的性能。
- 同步锁的核心特性是排他,要达到这个目的,多个线程必须去抢占同一个资源。
- 在同一时刻只能有一个线程执行加了同步锁的代码,意味着同一时刻只允许一个线程抢占到这个共享资源(锁),其余没抢占到的线程只能等待。
- 处于等待状态的线程不能一直占用CPU资源,如果没抢占到锁就要被阻塞等待,并且释放CPU资源。
- 如果非常多的线程都被阻塞了,那么我们要通过一个容器来存储线程,当获得锁的线程执行完任务并释放锁后,要从这个容器中唤醒一个线程,被唤醒的线程会再次尝试抢占锁。
synchronized作用范围
- 类锁
- 修饰静态方法
- 修饰代码块,锁的对象是类
详见代码
- 对象锁
- 修饰普通方法
- 修饰代码块
详见代码
要正确使用synchronized,最重要的是判断syn锁的谁,要么锁类,要么锁对象。类只有一个,实例对象有多个。
public class SynchronizedClass {
public static synchronized void m1() {
while (true) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
public static synchronized void m2() {
while (true) {
synchronized (SynchronizedClass.class){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public synchronized void m3(){
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedClass synchronizedClass0 = new SynchronizedClass();
SynchronizedClass synchronizedClass1 = new SynchronizedClass();
Thread[] threads = new Thread[3];
threads[0] = new Thread(() -> {
synchronizedClass0.m1();
}, "线程" + 0);
threads[0].start();
Thread.sleep(1000);
threads[1] = new Thread(() -> {
synchronizedClass1.m2();
}, "线程" + 1);
threads[1].start();
//由此可见 synchronized (SynchronizedClass.class){}与public static synchronized void m1()是锁的类,只允许一个线程执行
// 类中静态方法.
threads[2] = new Thread(() -> {
synchronizedClass0.m3();
}, "线程" + 2);
threads[2].start();
Thread.sleep(1000);
threads[0].join();
threads[1].join();
threads[2].join();
//非静态方法不受类锁的影响,因为是在实例对象上假的锁
}
}
锁升级过程
当只有一个线程访问,不存在锁的竞争时,synchronized是一把偏向锁,当存在两个及两个以上的献线程竞争锁时,synchronized省纪委轻量级锁,获得锁的线程独占共享资源,其他线程则通过CAS不断去尝试获取锁,当CAS达到一定数量任然没有拿到锁,synchronized则升级为重量级锁,将没有拿到锁的线程挂起,知道等待获取锁的线程释放锁将将挂起的线程唤醒。
volicate
volicate? 可见性?,有序性?
AQS
什么是AQS
AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ReentrantReadWriteLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。
首先AQS提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次AQS中维护了一个双向链表,用于存储线程,有head,有tail,并且每个节点都是Node对象
synchronized
重量级锁是使用ObjectMonitor来实现的,在该对象中包含几个关键字段:_cxq、_EntryList、_owner。当多个线程竞争重量级锁时,如果竞争到锁,则通过_owner保存获得锁的线程;如果竞争失败,则会先添加到_cxq单向链表的头部,然后调用park()方法挂起当前线程。接着获得锁的线程释放锁,并从_cxq队列中唤醒一个线程,唤醒的过程会根据QMode的不同,从_cxq队列中获取线程并移动到_EntryList队列头部或者尾部。默认策略是,如果_EntryList为空,则将_cxq中的线程移动到_EntryList,并唤醒第一个线程,也就是说当_EntryList为空时,后面进来的线程会先获得锁。synchronized通过_cxq维护最新参加锁竞争的线程,通过_EntryList维护当前参与竞争的线程
wite() notify()
Condition类大家应该用得非常少,它的作用和wait()/notify()方法相同,都是基于某个条件去等待和唤醒,所以可以认为它是J.U.C包中的wait()/notify()方法。之所以在J.U.C包中重复造轮子,其实还是和同步锁有关系,在J.U.C中使用了ReentrantLock来实现同步锁,如果使用wait()/notify()方法,显然无法工作,因为wait()/notify()方法依赖的是synchronized,所以为了提供配套的功能,针对ReentrantLock同步锁提供了Condition条件控制类。
await()方法,让线程等待,并释放锁。
signal()/signalAll()方法,唤醒被await()方法阻塞的线程。
AQS唤醒节点时为何从后向前唤醒
最后一个节点时只能由尾指针找到,并且尾结点记录了前驱节点;
ReentrantLock和synchronized的区别
核心区别:
synchronized是关键字,他是通过给对象的对象头添加标记进行上锁,而ReentrantLock本身就是一个对象,基于本身内部的state属性实现加锁;
底层实现上:
实现原理不一样,ReentrantLock是基于AQS实现的,synchronized是基于ObjectMonitor’
效率上:
如果竞争比较激烈,推荐使用ReentrantLock,不存在锁升级概念,而synchronized是存在锁升级概念的,如果升级到了重量级锁,是不存在所降级的。
功能上:
ReentrantLock基于类实现,功能比synchronized更加全面,比如说可以对采用公平锁还是非公平锁进行选择。
ReentranWLock
多线程
在实际应用过程中使用多线程,能够极大提高程序的性能,与此同时,如果多线程使用不当会导致线程安全问题,要想保证想爱你成安全问题,首先要明白什么情况下会导致线程安全问题。
导致线程不安全的原因
- 原子性
数据库事务的ACID原则中就有原子性,他的原子性的含义是在当前操作中包含多个数据库事务,要么全部执行,要么全部不执行。而在多线程的原子性定义与数据库事务相同,一个指令或多个指令在CPU执行过程中不允许别中断。
public class AtomicExample {
volatile int i = 0;
public void incr(){
i++;
}
public static void main(String[] args) throws InterruptedException {
AtomicExample atomicExample = new AtomicExample();
Thread[] threads = new Thread[2];
for (int i=0;i<2;i++){
threads [i] = new Thread(()->{
for (int k = 0;k<1000;k++){
atomicExample.incr();
}
});
threads[i].start();
}
threads[0].join();
threads[1].join();
System.out.println(atomicExample.i);
}
}
在上述代码中启动了两个线程,每个线程对成员变量i累加10000次,然后打印出累加后的结果。我们从结果中发现,原本期望的值是20000,但是打印出来的i值都是一个小于20000的数,和预期的结果不一致,导致这个现象产生的原因就是原子性问题
-
有序性
-
可见性
原子性问题的本质原因
- CPU的时间切片+多核处理器并行
- 执行指令的原子性,也就是线程运行的指令是否具备原子性
我们无法改变CPU时间切片以及CPU并行的机制,因此要在指令的原子性上做出改变,引入同步锁的机制,也就是对于多线程同时操作一个共享资源,保证一个线程对共享资源的独占性,保证在同一时刻,只允许一个线程执行某个方法或代码块。
如何保证线程安全问题?
java中线程实现的方式?
-
继承Thread类,重写run方法
-
实现Runable接口,重写run方法
-
如果想要拿到线程执行的返回结果,实现callable接口,重写call方法,配合FutureTask
4.基于线程池
底层都是实现Runnable实现的
java中线程的状态
- 新建
- 就绪
- 运行
- 阻塞 没有拿到锁,被放到等待队列
- 等待 持有锁之后因wait(),sleep(),join等方法导致等待
- 结束
java中如何停止线程
- stop 任务没结束,强制线程执行结束,已过时
- 使用共享变量,通过判断共享变量的状态,结束线程
- interrupt方式,interrupt是线程内部的表示线程是否中断的状态,通过执行interrupt方法改变线程状态,并提供了isinterrupt()方法。(获取线程状态,从而达到控制线程的中断的目的,是使用共享变量的改进)
.
sleep()与wait()的区别
- sleep()是Thread类的static方法,wait()属于Object类的方法
- sleep()属于TIMED_WAITING的等待,到达设置的时间会自动唤醒,继续执行任务;wait属于WAITING,需要手动唤醒;
- sleep()在持有锁时执行不会释放锁,wait在执行后,会释放锁资源
- sleep()可以在持有锁或者不持有锁时执行,wait()方法必须在只有持有锁时才能执行。?
CAS
什么是CAS
compare and swap 比较和交换,是一条CPU的并发原语。具体来说就是替换内存中某个位置时,首先查看内存中的值与预期值是否一致,如果一致执行替换操作,这个操作是原子性的。CAS也不是完美的,既有优点也有缺点。
优点:
- 通过不断尝试对共享对象进行CAS,避免了获取不到锁就将线程挂起和唤醒的情况。
缺点: - 自旋时间过长,影响性能
- 只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性
- 无法解决ABA问题
Java四种引用类型
- 强引用:只要这个对象有引用关系,就不会被清除,即使会导致OOM
- 软引用:内存泄漏时被清理
- 弱引用:只要执行一次GC就会被清理
- 虚引用:不常用,基本创建后就会被清理
java中锁的分类
第一种分类:可重入锁,不可重入锁
线程池
线程池的状态
线程池的执行流程
核心线程数 阻塞队列 最大线程数 拒绝策略
为何线程池使用完后要执行shutdown方法
有待验证,有人说要关闭,有人说尽量不关闭,我感觉要分情况,如果创建件的线程数少,可以不shutdown,
待研究
核心线程数如何设计
一般来说具体分为IO密集型和CPU密集型,具体的确定个数需要根据压测情况来决定;
集合
ArrayList
最常用的List实现类,内部通过数组实现,这就决定了适合随机查找和遍历,不适合插入和删除,线程不安全
Vector
也是基于数组实现,但是支持线程同步,线程安全
LinkedList
基于链表实现,适合节点的插入和删除,但查询慢,只能遍历
ConcurrentHashMap
###优化
- 存储结构的优化
1.7之前是采用数组+链表,1.8及以后采用数组+链表+红黑树
当某个链表的长度大于8时,并且数组长度大于64时,(如果数组长度小于64,不先转变为链表,而是先采取增加数组长度的方式)将链表变为红黑树,使用了红黑树后,查询效率由logn变为log_2n; - 写数据的优化,在1.7之前采用分段锁的方式,虽然比hashtable的synchronied来说要快,但是锁的粒度还是不够细,1.8之后采用cas+synchronized的方式,在数组上采用cas方式,作用到链表上使用synchronied.,也就是说操作那一块的链表就只锁哪一块,锁粒度要小很多
- 扩容操作,比如说进行写数据的时候,发现正在扩容,此时这个写操作帮助其扩容,而不是等待阻塞
- 计数器,采用类似LongAdder的方式,根据cpu的核数将资源分成很多块,最后进行汇总,由此可见concurrentHashMap你不是强一致性,而是弱一致性。但最终后得到正确的结果,这就体现了线程安全性
concurrentHashMap的散列算法
根据key的hashcode算出来一个hash值
Redis
缓存使用
@Cacheable
cacheName:对应redis的key,
key 对应redis的key,可以配合EL表达式
keyGenerator 对于以上表达式如果不满足你的使用情况,可以自定义表达式
cacheManger 缓存管理器 定义使用使用是么类型的缓存器(Redis 或者本地缓存),序列化,缓存失效时间
condition:符合条件的情况下才能进行缓存
unless ?
sync 是否异步
@CachePut
@CacheEvict
为什么使用redis?
高性能:Redis的速度是微秒级别,速度至少比mysql开10倍到100倍
高并发:mysql 1000/s的并发,Redis100000/s的并发
Redis和memcache对比
不重要