7、工具类
7.1CountDownLatch(减法计数器)
原理:
- CountDownLatch主要有两个方法,当一个线程或多个线程调用
await()
方法时,这些线程会阻塞, - 其他线程调用
countDown()
方法会将计数器减一(调用countDown方法的线程不会阻塞) - 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
public static void main(String[] args) throws InterruptedException {
//创建一个CountDownLatch的对象 设置初始的数量为10
CountDownLatch latch = new CountDownLatch(10);
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
//每执行一个线程数量减一
System.out.println(Thread.currentThread().getName()+"go !");
latch.countDown();
}, String.valueOf(i)).start();
}
//调用await方法对主线程进行了阻塞,只有当上面的所有线程全部执行结束(即数量变为0)主线程才会被唤醒继续向下执行
latch.await();
System.out.println("主线程 go !");
}
7.2CyclicBarrier(加法计数器)
阻塞分线程,当分线程阻塞的数量达到设定的某个值时,由最后一个阻塞的线程执行Runnable方法,然后所有阻塞的分线程被唤醒,继续向下执行
public static void main(String[] args) {
//这个Runnable任务是由打破栅栏的线程即最后一个线程执行的,执行完之后,所有被阻塞的分线程被唤醒,继续向下执行
CyclicBarrier barrier = new CyclicBarrier(7, () -> System.out.println(Thread.currentThread().getName()+"召唤神龙"));
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
try {
// xxxx....操作
barrier.await();
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(temp)).start();
}
System.out.println(Thread.currentThread().getName()+"sdasdas");
7.3Semaphore(信号量)
常用的三个操作
- 1、acquire()
(无返回值)
当一个线程调用acquire时,要么成功获取信号量(信号量减1)要么一直等待,直到有线程释放信号量然后被唤醒继续争抢信号量,或超时失败 - 2、tryAcquire() 获取一个信号量(可以一次获取多个信号量,但不能大于总的信号量),返回值是Boolean类型,有信号量值就成功获取,没有直接返回false,不会阻塞等待
- 3、release() 释放信号量,将信号量的值+1,然后唤醒等待的线程
信号量主要用于两个目的:
- 用户多个共享资源的互斥使用
- 用于并发线程数的控制(常用于秒杀,多个线程同时来了,都先去获取信号量(一定数量),拿到了信号量才能进行下一步的操作)
一般的acquire和release配合使用,使用于资源的重复使用
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
semaphore.acquire(); //获取信号量 会一直阻塞等待
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release(); //执行完毕必须释放信号量
}
},String.valueOf(1));
}
tryAcquire()方法单独使用,适用于秒杀,资源个数有限且不可重复使用,不会释放资源
//Semaphore semaphore = new Semaphore(int n); 指定信号量的数量
for (int i = 0; i < 100; i++) {
new Thread(()->{
boolean flag = semaphore.tryAcquire(1); //尝试获取一个信号量
try {
//获取到了 执行业务逻辑
if (flag==true){
xxx...成功的业务逻辑
}else{
xxx...失败的业务逻辑
}
} catch (Exception e) {
e.printStackTrace();
}//注意一般不释放资源
},String.valueOf(i)).start();
}
7.4ReadWirteLock(读写锁)
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果有一个线程想去写,就不应该在有其他线程可以对该资源进行读或写即
读读能共存 读写不能共存 写写不能共存
读共享,写加锁
读可以同时有多个,但是所有读没完成不允许写(读锁的意义),
写只能有一个写,且写的时候其他线程不能读也不能写
跟MySQL的共享锁和排它锁类似
- 如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁。
- 如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败
- 如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败
public class 读写锁测试 {
public static void main(String[] args) {
Resource resource = new Resource();
for (int i = 0; i < 100; i++) {
int num = i;
new Thread(() -> {
resource.put("" + num, num);
}).start();
}
for (int i = 0; i < 1000; i++) {
int num = i;
new Thread(() -> {
resource.get("" + num);
}).start();
}
}
}
class Resource {
private volatile Map<String, Object> map = new HashMap<>();
//创建一个读写锁对象
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
//获取一把写锁并加锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始写");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写完成");
}finally {
//一定要释放锁
readWriteLock.writeLock().unlock();
}
}
public void get(String key) {
//获取一把读锁并加锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始读");
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完成" + value);
}finally {
//一定要释放锁
readWriteLock.readLock().unlock();
}
}
}
7.5分之合并框架ForkJoin
7.6原子类
-
AtomicInteger
-
AtomicBoolean
-
AtomicLong
-
…
8、阻塞队列BlockingQueue
8.1阻塞队列概述
BlockingQueue是所有阻塞队列的父接口(常用于存放Thread对象),实现了Queue接口 ,Queue也是是Collection子接口(跟List Set接口并列)
public interface BlockingQueue<E> extends Queue<E>
阻塞队列的特点
- 当队列是空的,从队列中
获取元素
的操作将会被阻塞 - 对队列是满的,从队列中
添加元素
的操作将会被阻塞
阻塞队列的用处
在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒,在添加任务的时候,如果队列中任务满了的话,就先阻塞等待其他的任务被取走后自动的放在队列中。
为什么使用阻塞队列?
好处就是我们不需要关心什么时候需要阻塞线程,什么是需要唤醒线程,阻塞队列包办
8.2实现类
1、ArrayBlockQueue:由数组结构组成的有界阻塞队列
2、LinkedBlockinQueue:由链表组成的有界阻塞队列(默认值为Integer的最大值)
- 3、PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- 4、DelayQueue使用优先级队列实现的延迟无界阻塞队列
5、SynchronousQueue:只有一个元素的阻塞队列
- 6、LinkedTransgerQueue:由链表组成的无界阻塞队列
- 7、LinkedBlockingDueue:由链表组成的双向阻塞队列
常用方法
add_remove_element
//添加一个元素,如果队列已满 抛出异常 (添加成功返回true)
boolean add(E e);
//删除队头元素,如果队列是空的,抛出异常 (不为空返回删除的元素)
E remove();
//检查队列的第一个元素,如果队列是空的,抛出异常 (不为空返回第一个元素)
E element();
offer_poll_peek
//添加一个元素 如果队列已满则添加失败返回false 添加成功返回true
boolean offer(E e);
//从队列中取出元素,返回元素,队列为空返回null
E poll();
//检查队列中的第一个元素并返回,队列为空返回null
E peek();
-------
//添加一个元素 成功返回true 队列满了阻塞等待设置的时间 超过时间直接退出返回fasle
boolean offer(E e, long timeout, TimeUnit unit) throwsInterruptedException
//取出队列中元素 成功返回这个元素 队列为空时阻塞等待,超过等待时间退出返回null
E poll(long timeout, TimeUnit unit) throws InterruptedException;
put_take
//向队列中添加一个元素,如果队列已满,阻塞等待
void put(E e) throws InterruptedException;
//从队列中中取出一个元素 如果队列为空 阻塞等待
E take() throws InterruptedException;
9、CAS
1.概述
- 无锁优化 自旋
- -CPU原语支持 不会被打断
CompareAndSwap
cas(obj,期望的值,新值)
如果想要修改值,先判断原来的值是不是我期望的,是的话修改,不是的话自旋继续尝试修改
m = 0;
m++;
期望值 = read m;
cas(0,1){
for(;;) 如果当前值是0,就变为1 当前值不是0 则重新读取m,继续判断。。。
}
2.原子类
AtomicInteger
他的一系列操作都是由Unsafe类的CAS操作实现的。
incrementAndGet() 相当于 num++ 原子操作(CAS实现)
ABA问题
基本数据类型不会有问题,引用类型会有问题,加版本号解决(乐观锁)
10、AQS
11、ThreadLocal
作用:同一个线程之间共享数据
在开发中,我们的的请求经过 拦截器->controller->service->dao都是一个线程,同一个线程想要共享变量,就可以使用ThreadLocal,且ThreadLocal可以声明为静态的,所有线程可以拿到这个ThreadLocal对象,但是由于ThreadLocal的原理,不同的线程设置的值是不一样的(保存在自己的线程的Map中),就保证了线程之间的隔离性。
主要方法:
set
设置ThreadLocal内部的值 (设置当前线程内部的ThreadLocalMap中的value)
get
获取ThreadLocal内部存储的值 (获取当前线程内部的ThreadLocalMap中的value)
初识ThreadLocal
先看一段普通的多线程程序,线程A读取线程B修改后的person的name,也就是说,对于一个公共资源,其他线程对于公共资源的修改会影响到其他的线程
public class Test {
public static void main(String[] args) {
//公共资源Person
Person person = new Person();
//线程A 睡眠两秒后读取person的name值 去读到的是线程B修改过的
new Thread(()->{
System.out.println(person.name);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(person.name);
},"A").start();
//线程B 睡眠1秒修改name值为 lisi
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
person.name = "lisi";
}).start();
}
}
class Person{
String name = "zhangsan";
}
我们使用TheadLocal测试不同线程之间对于内部变量的隔离性,B线程对于共享资源的修改不会影响A线程
//最终线程A打印的对象是null,也就是说,不同线程对于ThreadLocal内部的值是不同的
//即B线程对于同一个对象的内部设置的值,A线程获取不到,
public class ThreadLocal1 {
//设置成将静态的,不然一个线程的不同方法无法使用
static ThreadLocal<Student> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tl.get());
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tl.set(new Student());
},"B").start();
}
}
class Student{
String name;
}
Set方法原理
存储的数据实际上就是在我们的每一个线程(Thread)的内部属性ThreadLocalMap<ThreadLocal,value>中,本质上就是一个Map,所以说不同的线程调set方法时虽然说是同一个ThreadLocal对象,但是Value值是存储在自己线程的Map中的
基本过程
1、 获取当前线程(Thread)的一个属性ThreadLocalMap<ThreadLocal,value>判断是不是null
2、 如果是null的话,就创建一个ThreadLocalMap对象,将ThreadLocal对象和我们的value值设置到ThreadLocalMap对象中,然后将这个ThreadLocalMap对象赋值给当前线程的ThreadLocalMap属性
3、 如果不是null的话,直接就修改value值即可
为什么一个共享资源,其他线程修改内部的值不会对其他线程造成影响?
我们先看一下set方法的源码
1、获取当前线程的ThreadLocalMap
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//getMap()将当前线程对象传入,返回一个ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
ThreadLocalMap的结构
就是我们ThreadLocal类的一个静态内部类,里面又套了一个静态内部类叫Entry继承了 WeakReference(弱引用)类,一个属性value,一个构造器(ThreadLocal,value)
本质上就是一个Map,key是ThreadLocal对象,value是我们每个线程设置的值
public class ThreadLocal{
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
再来看一下getMap()方法的源码
// 传入当前的线程对象,返回的是当前线程(Thread)对象的threadLocals属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocal
}
//我们发现,返回的就是我们当前线程中的ThreadLocalMap属性!!!!
ThreadLocal.ThreadLocalMap threadLocals = null;
2、判断当前线程的ThreadLocalMap是否是null
①如果不是null的话,就修改当前线程的ThreadLocalMap中的value值,
if (map != null)
map.set(this, value);
else
createMap(t, value);
②如果是null的话,说明当前线程的ThreadLocalMap还没有创建,就去创建一个ThreadLocalMap
创建过程
:直接将我们set方法中设置的value值和当前线程对象传来,调用ThreadLocalMap的构造器,key就是这个ThreadLocal对象,value就是我们调用set(value)传来的value,为我们当前线程的ThreadLocalMap赋值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
强软弱虚引用
当一个对象被回收时,会调用对象所在类的finalize()方法
强引用_永远不会被回收
我们正常的使用new关键字造出来的对象,就像下面这种,就是强引用,只要有引用指向这个对象,永远不会被垃圾回收,除非没有任何一个引用指向这个对象时,这个对象会被GC
Object obj = new Object();
代码演示
System.gc() 和Runtime.getRuntime().gc()有何区别?
没有任何区别,System.gc底层就是调用的Runtime.getRuntime().gc().(Full GC)
public static void main(String[] args) throws IOException {
//这时的person就是强引用 永不会被回收
Person person = new Person();
//当我们将person置为null时,发现Person这个对象被JVM回收了,即这个对象没有任何引用指向
// person = null;
System.gc();
//阻塞当前线程
System.in.read();
}
软引用SoftReference
当内存不够时发生GC被回收
一般作用于缓存
//首先设置JVM的参数 设置堆空间的最大为20m
public class 软引用 {
public static void main(String[] args) {
//创建一个弱引用对象,里面放一个10m的byte数组 即这个数组在堆空间中 由这个软引用指向数组
SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 10]);
//可以拿到数组 不为null
System.out.println(softReference.get());
//调用一下GC
System.gc();
//模拟让GC去回收
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里还是可以拿到值的 说明在内存空间够用的时候,GC不会回收软引用指向的对象
System.out.println(softReference.get());
//造一个15m的强引用byte数组,这时超出了堆空间的大小,JVM会回收软引用指向的对象
//也就是说讲上面的10m的byte数组回收
byte[] bytes = new byte[1024 * 1024 * 15];
//打印null说明在内存空间不够时软引用指向的对象才会被回收
System.out.println(softReference.get());
}
}
弱引用WeakReference
一旦遭遇GC,弱引用指向的对象就会被回收 一般用在容器里
代码演示
public class 虚引用 {
public static void main(String[] args) {
//创建一个弱引用指向Person对象
WeakReference<Person> wr = new WeakReference<>(new Person());
System.out.println(wr.get());
//调用GC FullGC
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//发现弱引用指向的对象已经变为了null
//即 只要发生GC 弱引用执行的对象就会被回收
System.out.println(wr.get());
}
}
ThreadLocal的内存泄露问题
1、我们在上面知道,ThreadLocal的set方法将我们线程共享的数据,设置到了当前线程(Thread对象)的threadLocals里,就是一个Map,继承了WeakReference,也即这个Map中的key弱引用,它指向的是ThreadLocal对象,value是我们的共享数据。
思考:为什么使用弱引用指向我们的ThreadLocal呢?
解决key的内存泄露问题,因为当我们将ThreadLocal的引用置为null时,理论上JVM会将ThreadLocal对象回收,但是我们的ThreadLocalMap中的key还引用着ThreadLocal对象,如果说key使用强引用着ThreadLocal对象,那么这个ThreadLocal对象就不会被回收,但是我们已经无法访问到ThreadLocal对象了,就会出现内存泄露,我们将key使用弱引用指向了这个ThreadLocal对象,那么这时ThreadLocal就会被回收了。
思考:还会发生内存泄露吗
会,会出现value的内存泄露
使用key的弱引用尽管当我们的ThreadLocal对象被回收时,ThreadLocal对象没了,但是我们的value
指向的数据会一直存在,因为我们的ThreadLocal对象没了,即我们的Map的key变为null了,那么我们就无法访问到这个map了(因为ThreadLocalMap只能通过ThreadLocal对象来访问,因为虽然ThreadLocalMap属性也声明在Thread类中,但是由于权限是默认的,我们无法通过当前线程直接访问ThreadLocalMap,
),但是value会指向我们的共享数据,这个共享数据就无法被回收,就造成了内存泄露
public class ThreadLocalWTest {
public static void main(String[] args) throws Throwable {
ThreadLocal<ThreadLocalWTest> tl = new ThreadLocal<>();
tl.set(new ThreadLocalWTest());
//将tl的引用断开 运行程序发现并没有调用finalize(),即我们的共享数据并没有回收
//但是我们已经无法访问共享数据了,因为我们的ThreadLocal已经不存在了,
//所以此时就出现了共享数据的内存泄露
tl=null;
//解决办法:调用remove方法,将我们的value的引用置为null,
tl.remove();
//请求GC
System.gc();
Thread.sleep(1000);
}
@Override
protected void finalize() throws Throwable {
System.out.println("value被回收了");
}
}
//最后 我们发现并没有调用finalize方法,即当我们的ThreadLocal不存在的时候,我们的共享数据还在,但是也无法被回收,就造成了内存泄露
解决办法:
调用ThreadLocal的remove()方法,就会将Map中的value的引用置为null,JVM就会回收我们的共享数据
所以说,我们一定如果不使用数据的时候,要调用ThreadLocal的remove()方法,将Map中的value置为null,那么value就没有引用指向,就会被回收了
虚引用PhantomReference
只要发生GC,就会被回收
用于堆外内存垃圾回收,get不到设置的值,
Unsafe类直接操作内存)
总结
1、强引用:永远不会被回收
2、软引用:当内存不够时发生GC被回收
3、弱引用:只要发生GC就会被回收
4、虚引用:只要发生GC就会被回收
11、volatile关键字DCL单例模式与JMM
见Java多线程笔记
12、各种锁的理解
可重入锁
一个线程调用同步方法A,A方法里调了同步方法B,且A和B是同一把锁,这时线程在获取到A锁时执行B方法时不必重新申请锁,直接可以调用B
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2
。
看下面这段代码就明白了:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized
和Lock
都具备可重入性,所以不会发生上述现象。
自旋锁
一、首先是一种锁,与互斥锁相似,基本作用是用于线程(进程)之间的同步。与普通锁不同的是,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起(阻塞);试想下,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程B可以不放弃CPU时间片,而是在“原地”忙等,直到锁的持有者释放了该锁,这就是自旋锁的原理,可见自旋锁是一种非阻塞锁。
二、自旋锁可能引起的问题:
1.过多占据CPU时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据cpu时间片,导致CPU资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片阻塞;
2.死锁问题:试想一下,有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
JAVA中一种自旋锁的实现: CAS是Compare And Set的缩写
import java.util.concurrent.atomic.AtomicReference;
class SpinLock {
//java中原子(CAS)操作
AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋锁的线程对象
private int count;
public void lock() {
Thread cur = Thread.currentThread();
//lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环
//一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。
while (!owner.compareAndSet(null, cur)){
}
}
public void unLock() {
Thread cur = Thread.currentThread();
owner.compareAndSet(cur, null);
}
}
}
public class Test implements Runnable {
static int sum;
private SpinLock lock;
public Test(SpinLock lock) {
this.lock = lock;
}
public static void main(String[] args) throws InterruptedException {
SpinLock lock = new SpinLock();
for (int i = 0; i < 100; i++) {
Test test = new Test(lock);
Thread t = new Thread(test);
t.start();
}
Thread.currentThread().sleep(1000);
System.out.println(sum);
}
@Override
public void run() {
this.lock.lock();
sum++;
this.lock.unLock();
}
}
死锁
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。