Java— JUC并发编程 (自我学习用)
1、Lock版的生产者/消费者问题
旧版本的生产者消费者模式的实现方式是:Synchronized关键字+线程通信(wait()和notify()/notifyAll())
juc版的实现方式,也就是Lock版的实现方式是:Lock+Condition(await()和signal()/signalAll())
public class SynchronizeDemo2 {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
//线程操作的资源类,判断等待业务、通知
class Data2 {
private int number = 0;
Lock lock = new ReentrentLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
//上锁
lock.lock();
//在try语句里执行业务代码
try{
while(number != 0) {//0
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程,我+1完毕了
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
//在finally语句中释放锁
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
//上锁
lock.lock();
//在try语句里执行业务代码
try{
while(number == 0) {//0
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程,我+1完毕了
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
//在finally语句中释放锁
lock.unlock();
}
}
}
补充:sleep和wait的区别
链接: link.
①、sleep()方法属于Thread类,wait()方法属于object类。
②、sleep()方法导致了程序暂停执行指定的时间,让出cpu,但调用sleep()方法的线程不会释放对象锁,当指定的时间到了又会自动恢复运行状态。
③、当调用wait()方法的时候,线程会放弃对象锁
④、sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。
2、Condition实现精准通知唤醒(精准版的生产者/消费者问题)
比如有三个线程A、B、C,要实现A结束了唤醒B,B结束了唤醒C,C结束了唤醒A。实现如下:
主要就是用三个Condition(condition1,condition2,condition3)分别监视三个线程(A、B、C)
package com.wl.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestJUC_Condition {
public static void main(String[] args) {
DataCondition data = new DataCondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
class DataCondition{
private int number = 1; //A-1 B-2 C-3
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA() throws InterruptedException{
//上锁
lock.lock();
//在try语句里处理业务代码
try {
while (number != 1) {
//等待
condition1.await();
}
number=2;
System.out.println(Thread.currentThread().getName() + "=>" + "AAAAAAAAAAA");
//唤醒B线程
condition2.signal();
} catch ( Exception e) {
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
public void printB() throws InterruptedException{
lock.lock();
try {
while (number != 2) {
condition2.await();
}
number=3;
System.out.println(Thread.currentThread().getName() + "=>" + "BBBBBBBBBBB");
//唤醒C线程
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() throws InterruptedException{
lock.lock();
try {
while (number != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "=>" + "CCCCCCCCCCCCCCCC");
number = 1;
//唤醒A线程
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
3、Synchronized锁的到底是什么
如果类中的方法用synchronized修饰, 那么锁的是 new 出来的对象
如果类中的方法用static synchronized修饰, 那么锁的是类的Class对象
4、CopyOnWriteArrayList 和 CopyOnWriteHashSet
多线程并发下,ArrayList和HashSet都不是安全的,juc提供了CopyOnWriteArrayList 和 CopyOnWriteHashSet实现了多线程安全的List和Set。
CopyOnWrite写入时复制的思想,保证了在读取时不用进行同步,而只需在写入时进行同步(synchronized)。具体做法是,在同步写入数据时先把原数据复制一份,在该副本上进行添加操作,最后再把完成添加操作的副本赋给原始数组,这样就可以避免在写入时对读取数据操作的影响 。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//这里对原始数据复制了一份,然后在副本上进行添加,不能直接在原始数据上进行添加,会影响读取操作
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//最后把完成添加操作的副本赋给原始数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
由于在修改时复制了一份数据, 因此所有读取操作都无需进行同步
public E get(int index) {
return get(getArray(), index);
}
对应的还有一个ConcurrentHaspMap实现了线程安全的HaspMap。
5、Callable的使用(代码里的注释有详细解释)
Callable的优势是可以有返回值,还可以抛出异常;结果有缓存,效率高
package com.wl.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
//FutureTask是一个适配类(中间类),可以传递一个Callable<>作为参数,构造一个FutureTask
FutureTask futureTask = new FutureTask(myThread);
//FutureTask类实现了RunnableFuture接口,RunnableFuture接口继承了Runnable接口,
//因此FutureTask本质上也是一个Runnable
new Thread(futureTask).start();
//不同于Runnable,Callable可以有返回值,还可以抛出异常
//获取Callable的返回结果,这里可能会阻塞(原因是call方法可能是一个耗时操作)
Object o = futureTask.get();
System.out.println(o);
}
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("call方法");
return 1024;
}
}
6、JUC辅助类 CountDownLatch、CyclicBarrrier、Semaphore
CountDownLatch(减法计数器)用法:
链接: link.
、、、
//CountDownLatch 相当于一个计数器,创建时需要传递一个参数,表示计数器的大小
CountDownLatch cdl = new CountDownLatch(6);
//countDown()方法进行计数器减一操作
cdl.countDown();
cdl.await(); //等待计数器归零,然后再继续向下执行
CyclicBarrrier(相当于一个加法计数器)的用法:
链接: link.
CyclicBarrier的字面意思就是可循环使用的屏障。它要做的事情就是让一组线程到达一个屏障之前被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
和CountDownLatch一样,CycliBarrier构造方法中接受一个int型的数值,代表屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
Semaphore(信号量)的使用:
链接: link.
Semaphore semaphoore = new Semaphore(3);
semaphoore.acquire(); //获得资源,如果已经满了,就等待,等待到有资源被释放为止
semaphoore.release(); //释放资源,会将当前信号量数+1,然后唤醒等待的线程
Semaphore作用:多个共享资源互斥的使用;并发限流,控制最大的线程数
7、ReadWriteLock(读写锁)
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占有
Sychronized和ReentrantLock的问题是锁的粒度较大,不能做到读写分离,即读的时候一般不需要进行同步,写的时候才需要进行多线程同步。ReadWriteLock的好处就是做到了读写分离,具体如下:
①、Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
②、ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
③、ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
④、ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
package com.wl.ReadWriteLock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占有
ReadWriteLock(读写锁)
*/
public class TestReadWriteLock {
public static void main(String[] args) {
// MyCache myCache = new MyCache();
MyCache2 myCache = new MyCache2();
//写入
for(int i=0;i<5;i++){
//lambda表达式内部无法获得外部变量,因此这里使用final修饰一个临时变量
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for(int i=0;i<5;i++){
//lambda表达式内部无法获得外部变量,因此这里使用final修饰一个临时变量
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
//自定义缓存,带锁的
class MyCache2{
private volatile Map<String,Object> map = new HashMap<>();
//定义一个读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
//存入的时候,用的是写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入了"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK");
} catch (Exception e) {
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
//读取的时候,用的是读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
} catch (Exception e) {
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
//自定义缓存,不带锁的
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"写入了"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK");
}
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
}
}
8、BlockingQueue(阻塞队列)
链接: link.
BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
常用的BlockingQueue有ArrayBlockingQueue有界的任务队列(使用时会设定队列长度)、LinkedBlockingQueue无界的任务队列(链表)、PriorityBlockingQueue优先任务队列、SynchronousQueue直接提交队列(无缓存)
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中队首的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。
四组不同的行为方式解释:
抛异常:如果试图的操作(向满队列插入或空队列取出)无法立即执行,抛一个异常(Queue full / NoSuchElementException)。
返回特定值:如果试图的操作无法立即执行,返回一个特定的值(满false / 空null)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞(一直等),直到能够执行。
超时等待:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
9、ThreadPoolExecutor 线程池类
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler ) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数解释:
corePoolSize:核心池大小,意思是当超过这个范围的时候,就需要将新的线程放到等待队列中了即workQueue;
maximumPoolSize:线程池最大线程数量,表明线程池能创建的最大线程数
keepAlivertime:当活跃线程数大于核心线程数,空闲的多余线程最大存活时间。
unit:存活时间的单位
workQueue:存放任务的队列---阻塞队列
ThreadFactory:线程创建工厂,一般使用默认值即可,即Executors.defaultThreadFactory
handler:超出线程范围(maximumPoolSize)和队列容量的任务的处理程序
workQueue是一个BlockingQueue(阻塞队列),常用的workQueue有ArrayBlockingQueue有界的任务队列(使用时会设定队列长度)、LinkedBlockingQueue无界的任务队列(链表)、PriorityBlockingQueue优先任务队列、SynchronousQueue直接提交队列(无缓存)
handler是RejectedExecutionHandler ,即拒绝任务策略,线程池的最大容量为maximumPoolSize的值加上workQueue的长度,当超过这个阈值还有线程需求,那就会触发拒绝任务策略。
拒绝策略有4种:
①、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;
②、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;比如是从main线程调用该任务,那么该任务就会在main线程中执行,而不是从线程池中获得一个线程执行该任务
③、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交(但是并不一定能丢弃成功);
④、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理,也不会抛出异常。当然使用此策略,业务场景中需允许任务的丢失;
maximumPoolSize的值该如何确定(大小为多少):
①、对于CPU密集型的任务,根据CPU的核数,几核就把maximumPoolSize设置为几,保持CPU的效率最高
//获取CPU的核数,把核数设置为maximumPoolSize 的值
maximumPoolSize = Runtime.getRuntime().availableProcessors();
②、对于IO密集型的任务,需判断程序中比较耗IO的线程有多少(比如10),把maximumPoolSize的值设为大于该值即可(>10)
10、四大函数式接口
链接: link.
函数型接口(Function):有一个输入值,有一个返回值
断言型接口(Predicate):有一个输入值,有一个boolean类型的返回值
消费型接口(Consumer):只有输入值,没有返回值
供给型接口(Supplier):没有输入,只有返回值
11、Stream流式计算和链式编程
新时代的程序员(java8新特性):lambda表达式、链式编程、函数式接口、Stream流式计算。
什么是Stream流式计算
大数据:存储+计算
存储:集合set/list MySQL
计算:都应该交给流操作
/**
* 题目要求:一行代码实现
* 现在有五个用户,筛选:
* 1:ID 必须是偶数
* 2:年龄必须大于23岁
* 3:用户名转为大写字母
* 4:用户名字母倒着排序
* 5:只输出一个用户
*/
public class Demo05 {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(6, "e", 25);
//集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
//计算交给Stream流
//lambda表达式、链式编程、函数式接口、Stream流式计算。
list.stream() //把list转化为流
.filter(u->{return u.getId()%2==0;})
.filter(u->{return u.getAge()>23;})
.map(u->{return u.getName().toUpperCase();})
.sorted((uu1,uu2)->{return uu1.compareTo(uu1);})
.limit(1)
.forEach(System.out::println);
}
}
12、ForkJoinPool的使用
链接: link.
ForkJoinPool可以将一个大的任务拆分成多个子任务进行并行(多核CPU)处理,最后将子任务结果合并成最后的计算结果,并进行输出。
Fork/Join框架中提供的fork方法和join方法,fork()方法用于将新创建的子任务放入当前线程的work queue队列中(拆分),join()方法则进行不断等待,获取任务执行结果(合并)。
使用方法:
首先需要创建一个ForkJoinPool ,然后调用ForkJoinPool.submit()方法,submit()方法会返回一个ForkJoinTask类型的task,可以通过这个task获取最终结果。
submit()方法需要传入一个类型为ForkJoinTask的参数,ForkJoinTask是一个抽象类,RecursiveTask是ForkJoinTask的一个抽象子类。可以通过自定义一个类继承RecursiveTask类,重写compute()方法,来创建一个ForkJoinTask。
//创建ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
//调用ForkJoinPool.submit()方法,提交ForkJoinTask
ForkJoinTask<Long> task = forkJoinPool.submit(new MyForkJoinTask());
//获取最终结果
Long result = task.get();
具体示例代码:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class ForkJoinDemo {
public static void main(String[] args) {
Long startTime = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = forkJoinPool.submit(new MyForkJoinTask(1L, 200_0000_0000L));
Long result = null;
try {
result = task.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Long endTime = System.currentTimeMillis();
System.out.println("执行时间:"+(endTime-startTime));
System.out.println("结果:"+result);
}
}
class MyForkJoinTask extends RecursiveTask<Long>{
private Long start;
private Long end;
private Long Max = 10000L;
public MyForkJoinTask(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if((end-start)< Max){
long sum = 0;
for(long i=start;i<end;i++){
sum+=i;
}
return sum;
}else {
MyForkJoinTask task1 = new MyForkJoinTask(start, (start + end) / 2);
// task1.fork();
MyForkJoinTask task2 = new MyForkJoinTask((start + end) / 2 + 1, end);
// task2.fork();
invokeAll(task1,task2);//也可以使用invokeAll,但实测效率上和fork并没有差太多
return task1.join()+task2.join();
}
}
}
有一点需要注意:Fork-Join框架只是在同时使用多核运行原来的单核程序,每个核心上的程序之间互不交叉。这不会改变算法原来的时间复杂度。比如查询一个超大数组中的最小值,复杂度为O(n),通过Fork-Join框架,将数组分为两部分,递归的查询每部分的最小值,这只是将原来的所有操作用两个核心运行,时间复杂度为O(n/2),其实依然是O(n)。所以,假如排序一亿个数,单核用时30s,双核的用时也不会少于15s,因此,想要通过Fork-Join指数级提升性能是不现实的。但是对于已经成熟的系统,每一个微小部分的提升都有助于提升系统整体性能,从这个角度讲,Fork-Join还是有极大的用处。(简单理解为充分利用多核CPU的多核性能)
13、通过CompletableFuture实现异步回调
链接: link.
链接: link.
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果
callable就可以实现异步调用,为什么要使用CompletableFuture呢,因为CompletableFuture更加强大, callable只有成功回调,CompletableFuture可以有成功回调,异常(失败)回调。
14、JMM(Java内存模型)
链接: link.
链接: link.
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的工作内存(local memory),线程的工作内存中保存了该线程使用到的共享变量的主内存的副本拷贝,线程对共享变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。本地内存是JMM的一个抽象概念,并不真实存在。
volatile关键字的作用:
①、保证可见性(共享变量在不同线程之间的可见性);即一个线程对共享变量进行了修改要立即让其他拥有该共享变量副本的线程知晓。
②、不保证原子性;原子性是指,一个操作不能被打断,要么全部执行完毕,要么不执行,有点类似于事务操作。(可以使用Atomic类(如AtomicInteger)保证操作的原子性)
③、禁止指令重排序
15、单例模式
链接: link.
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,不能通过new来创建该类的对象。(构造函数是私有的。)
常见的单利模式有:饿汉式、懒汉式、DCL双重检测锁、静态内部类以及枚举单例。但是以上单例方法除了枚举单例,其他的都可以通过反射进行破坏,因此都是不安全的。
注意:
①、单例类只能有一个实例。(构造函数是私有的。)
②、单例类必须自己创建自己的唯一实例。
③、单例类必须给所有其他对象提供访问这一实例的方法。
1、饿汉式 单例模式
问题:类加载时就创建对象,可能会造成内存空间的浪费
public class Hungry {
//构造器私有,防止外部用new来创建对象
private Hungry(){}
//饿汉式,在类加载的时候就创建对象
private static Hungry instance = new Hungry();
//对外提供访问对象的方法
public static Hungry getInstance(){
return instance;
}
}
2、非安全懒汉式 单例模式
需要时才进行创建
public class LazyMan {
private LazyMan(){}
private static LazyMan instance;
public static LazyMan getInstance(){
//需要时才进行创建,避免类加载就创建对象造成内存的浪费
if(instance==null){
instance = new LazyMan();
}
return instance;
}
}
3、安全版懒汉式 单例模式
问题:synchronized会影响效率
public class SafeLazyMan {
private SafeLazyMan(){}
private static SafeLazyMan instance;
//getInstance方法用synchronized修饰,保证安全
public static synchronized SafeLazyMan getInstance(){
if(instance == null){
instance = new SafeLazyMan();
}
return instance;
}
}
4、DCL双重检测锁+volatile 单例模式
注意为什么要给单例对象加volatile
public class DCL {
private DCL(){}
private volatile static DCL instance;
public static DCL getInstance(){
//双重检测锁
if(instance == null){
synchronized (DCL.class){
if (instance == null){
instance = new DCL(); //这里可能会发生指令重排,因此要使用volatile修饰 instance
/*
new一个对象不是原子性操作,分为三步:
1、内配内存空间
2、执行构造方法
3、把这个对象指向这个空间
例:如果线程A按照 1 3 2 的顺序进行了指令重排,
此时并发线程B发现instance不为null,就会直接执行return instance
但是此时的instance其实还没有完成构造,会造成问题。所以要禁止指令重排
*/
}
}
}
return instance;
}
}
5、使用静态内部类实现 单例模式
public class Outer {
private Outer(){}
//内部类在用到时才会加载,因此外部类在加载的时候并不会立即就加载内部类
private static class InnerClass{
private static final Outer instance = new Outer();
}
public static Outer getInstance(){
return InnerClass.instance;
}
}
6、枚举单例
不能通过反射破坏枚举单例
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
以上单例方法除了枚举单例,其他的都可以通过反射进行破坏
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class De_Singleton2 {
private static boolean flag = false;
//private De_Singleton2(){} //构造函数私有
private De_Singleton2(){
//防止反射破坏单例
synchronized (De_Singleton2.class){
if(flag == false){ //通过外部变量防止单例被破坏
flag = true;
}else{
throw new RuntimeException("不要试图通过反射破坏单例");
}
}
}
private volatile static De_Singleton2 instance;
public static De_Singleton2 getInstance(){
//双重检测锁
if(instance == null){
synchronized (De_Singleton2.class){
if (instance == null){
instance = new De_Singleton2();
}
}
}
return instance;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过反射获得构造器
Constructor<De_Singleton2> constructor = De_Singleton2.class.getDeclaredConstructor(null);
//破坏构造器私有
constructor.setAccessible(true);
//两个对象都是通过构造器创建的
De_Singleton2 instance1 = constructor.newInstance();
De_Singleton2 instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
16、CAS(compareAndSwap)比较并交换
链接: link.
链接: link.
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。
CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
比较当前工作内存中的值和主内存中的值,如果相同就执行操作,如果不是就一直循环(自旋锁)。
CAS存在的问题:
①、ABA问题。
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
②、CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
③、只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
17、各种锁的理解
链接: link.
①、公平锁和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
例如:ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。
②、乐观锁和悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
③、可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
示例:
④、自旋锁VS 适应性自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。自旋锁的实现原理同样也是CAS。
适应性自旋锁:自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
⑤、无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
这四种锁是指锁的状态,专门针对synchronized的。(具体见链接)
⑥、独享锁 VS 共享锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有,如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
例如ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,读锁时共享锁,写锁是独占锁。
18、死锁排查
jps(Java Virtual Machine Process Status Tool)命令:
链接: link.
例如:jps -l 输出完全的包名,应用主类名,jar的完全路径名 (包含进程号信息)
jstack 加上 进程号
链接: link.
jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 java 应用程序中线程堆栈信息。
jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。