基础回顾
<1> JUC:
java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks三个和多线程相关的包
<2> Java可以开启进程吗?
不能,java不能直接操作硬件,调用的本地方法,c++底层,start0()方法
<3> 并发:多个线程操作统一资源
<4> 并行:多个线程可以同时进行
<5> 并发编程的本质:充分利用CPU的资源
<6> 线程的六个状态:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
<7> 如何正确使用多线程编程?(面向对象编程 OOP)
编写资源类,然后将资源类丢进线程,
//资源类 包括属性和方法
class A{
private int number=10;
public void sub(){
number--;
}
}
class B{
public static void main(String[] args){
//多个线程操作同一个资源类
//创建资源类
A a=new A();
//把资源类丢进线程,lambda表达式直接重写Runnable方法
new Thread(()->{for(int i=10;i>0;i++) a.sub;},"a").start();
new Thread(()->{for(int i=10;i>0;i++) a.sub;},"b").start();
new Thread(()->{for(int i=10;i>0;i++) a.sub;},"c").start();
}
}
<8> 如何实现线程交替进行 ?
方法编写的逻辑:判断等待–>执行业务–>唤醒其他进行
Lock锁
三个子类
- ReentrantLock 可重入锁 默认无参构造是非公平锁,加入参数true是公平锁
- ReentrantReadWriteLock.ReadLock 读锁
- ReentrantReadWriteLock.WriteLock 写锁
公平锁与非公平锁
公平锁:按照线程的就绪顺序进行调度
非公平锁:根据CPU指令进行调度,可以插队
Lock锁的使用
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
Synchronized和Lock的区别
- synchronized 是java内置的关键字,Lock是一个java类
- synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
- synchronized会自动释放锁,Lock需要手动释放,不然可能会造成死锁
- synchronized锁如果不释放,其他线程就会持续等待,Lock中有方法tryLock,尝试获得资源,不会持续等待
- synchronized是可重入锁,不可以中断的,非公平的;Lock是可重入锁,可以判断锁,公平与否可以设定的
- synchronized适合锁少量的代码块,Lock适合锁大量的代码块
Synchronized生产者与消费者
需要注意的问题 在判断是否wait的时候要习惯用while 因为if可能会造成伪唤醒,使用if,就判断依次,如果同时有两个if进行,就会出现线程不安全,使用while是持续判断,如果另外一个线程进行了操作,就会进入阻塞。
public class a {
public static void main(String[] args) {
b b1=new b();
new Thread(()->{
for(int i=0;i<10;i++) {
try {
b1.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
b1.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
static class b {
private int number=0;
//判断等待,业务,唤醒
public synchronized void increment() throws InterruptedException {
while(number!=0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"-->"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while(number==0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
this.notifyAll();
}
}
}
使用Lock替换Synchronized
- synchronized关键字对应Lock 而notify和wait分别对应condition对象中的singal和await方法
- condition对象可以通过多个来实现精准的唤醒,如下图,synchronized是无法实现的
- 在编写时依旧是按照判断等待->执行业务->唤醒其他线程来写
class b {
private Lock lock=new ReentrantLock();
private Condition condition1=lock.newCondition();
private Condition condition2=lock.newCondition();
private Condition condition3=lock.newCondition();
private int number =0;//0->A 1->B 2->C
//判断等待,业务,唤醒
public void printA() throws InterruptedException {
lock.lock();
try {
while(number!=0) {
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"-->"+"AAAAAA");
number=1;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB() throws InterruptedException {
lock.lock();
try {
while(number!=1) {
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"-->"+"BBBBB");
number=2;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() throws InterruptedException {
lock.lock();
try {
while(number!=2) {
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"-->"+"CCCCC");
number=0;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
八锁问题(延时只是为了效果更加明显)
synchronized关键字锁的对象,一个是new的实例,一个是class的模板(带static关键字),使用同一把锁的线程执行先后顺序一定是按照拿到锁的顺序执行,不适用同一把锁的线程执行先后顺序是按照CPU的调度(需要考虑人为延迟)。如果是synchronized,那么不同的实例就会有不同的锁,但是如果使用static synchronized ,锁的就是class这个类模板,就算有不同的实例对象,使用的同样是一把锁。
集合安全问题
ArrayList、HashSet、HashMap都是不安全的(vetor是安全的,可以替代ArrayList),
1、可以使用Collections中的Synchronized方法来使他们安全(如下,其中set需要的使sortSet 已知的ConcurrentSkipListSet和TreeSet)
Set<String> s=
Collections.synchronizedNavigableSet(new ConcurrentSkipListSet<>());
2、使用新的实现类,实现安全
List<String> list=new CopyOnWriteArrayList<>();
Set<String> set=new CopyOnWriteArraySet<>();
Map<> map=new ConcurrentHashMap();
一点补充
- HashMap有两个参数loadFactor,和threshold 可以去看HashMap的底层原理
- HashSet的底层其实就是HashMap 只不过他的值是一个常量
Callable
callable如何使用的
因为线程的创建需要new Thread().start(),其中传入的参数应该使Runnable的实现类,所以应该找一个中转类,可以实现Runnable接口,将线程作为参数传入
public static void main(String[] args) throws ExecutionException, InterruptedException {
//使用FutureTask来创建一个Runnable的实现类,可以作为参数丢进Thread里
//自己创建的资源类需要作为参数
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask).start();
String s = futureTask.get();//可以获得线程的返回值,会造成线程阻塞
System.out.println(s);
}
//写一个资源类,实现Callable接口
//Callable接口的泛型就是返回值类型
static class MyThread implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("123");
return "A";
}
}
callable和runnable的区别(不完整,在补充)
- callable有返回值,runnable没有返回值
- callable的异常可以向上抛,runnable的异常必须try-caych处理
常用的辅助类
ConutDownLatch 减法计数器
CountDownLatch countDownLatch=new CountDownLatch(10);//创建一个倒计时的计数器
countDownLatch.countDown();//使计数器减一,放在线程里面执行
countDownLatch.await();//等待计数器归零之后再执行后续,确保线程执行完毕
CyclicBarrier 加法计数器
CyclicBarrier有两种构造方法,一种是只有一个数字,表示到什么时候,一种是数字加runnable类,到什么时候走什么方法
c.await();表示计数器加一,当计数器的值为预设值时,就会执行后续的方法
CyclicBarrier c=new CyclicBarrier(7,()->{
System.out.println("我真的没有开挂");
});
for (int i = 0; i < 21; i++) {
final int number=i;
new Thread(()->{
try {
Thread.sleep(100*number);
//i是无法直接在线程中用的,需要使用final关键字修饰才能够拿到
System.out.println("上了一波分"+number);
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
Semaphore 信号量
预定一个信号量数值,当信号量没有的时候,其他线程就会等待,如果有就可以获得
作用:并发限流,限制最大线程数,
Semaphore s=new Semaphore(7);
for (int i = 0; i < 21; i++) {
final int number=i;
new Thread(()->{
try {
s.acquire();//获得信号量
Thread.sleep(100*number);
System.out.println("上了一波分"+number);
} catch (Exception e) {
e.printStackTrace();
} finally {
s.release();//释放信号量
}
}).start();
读写锁(独占锁和共享锁)
读锁(共享锁):可以多条线程同时操作
写锁(独占锁):只能有一条线程进行操作
class a{
private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
void read(){
lock.readLock().lock();//加锁,写锁使用同理
try{
System.out.println("读");
}catch(Exception e){
System.out.println(e);
}finally{
lock.readLock().unlock();//解锁,写锁使用同理
}
}
}
阻塞队列
队列的继承关系
BlockingQueue 双端的阻塞队列,写入时如果满了就会阻塞,写出如果是空的也会阻塞
常用的方法:
- add 和 remove 添加移除,element 查看队首元素,如果阻塞,就会排除异常
- offer 和 poll 添加移除,peek 查看队首,如果阻塞,不会抛出异常,返回false或者null
- put和take 如果阻塞,就一直等待
- offer(E,time_number,单位) 和poll 如果阻塞,等待设定时间后就不等待了
SynchronizedQueue 同步队列:放入元素之后必须取出来之后才能继续放入,即他的容量只有1。
池化技术:优化资源的使用
线程池的好处
- 降低资源的消耗—>线程复用
- 提高响应速度---->可以控制最大并发数
- 方便管理———>管理线程
三大方法:三个创建线程池的方法
一般不会使用 Executors 构造类里的方法构造线程池,因为默认的最大线程池数太大,可能会降低效率,甚至OOM
//单个线程的线程池
ExecutorService executorService1= Executors.newSingleThreadExecutor();
//固定线程数的线程池
ExecutorService executorService2= Executors.newFixedThreadPool(5);
//可以根据需求变化的线程池
ExecutorService executorService3= Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
//线程的创建需要使用executorService1.execute方法
executorService1.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
}finally {
executorService1.shutdown();
}
七大参数和四种拒绝策略
参数
- corePollSize : 核心线程数,一开始就开启的
- maximumPoolSzie : 最大线程数 ,阻塞队列也满了时,才会开启的线程数量
- keepAliveTime : 线程池存活的时间
- TimeUnit : 存活时间的单位 TimeUnit.SECONDS、 TimeUnit.Days等
- BlockingQueue: 阻塞队列,当核心线程都使用时,后加入的线程会在阻塞队列进行等待
- ThreadFactory: 线程工厂,使用默认的就可以,一般不变
- RejectedExecutionHandler :拒绝策略,当最大线程都开启,并且阻塞队列也满了的时候,对于再使用线程池时的策略 四种
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
2, //核心线程数
3, //最大线程数
5, //线程池的存活时间
TimeUnit.SECONDS, //时间单位
new LinkedBlockingDeque<>(5), //阻塞队列
Executors.defaultThreadFactory(),//线程工厂,使用默认的就可以,一般不变
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略 四种
拒绝策略
- new ThreadPoolExecutor.AbortPolicy(): 不处理,直接抛出异常
- new ThreadPoolExecutor.CallerRunsPolicy() :哪个线程向调用,就重新归还给他,让他去执行
- DiscardOldestPolicy() :尝试获取最早开启的、已使用完的线程,如果没有,就丢弃该线程任务
- DiscardPolicy():直接丢弃线程任务
最大线程数的设置
CPU密集型
根据电脑/服务器的CPU数量进行设置 CPU的个数就设置为最大线程数,可以保证CPU效率最高,代码获取 Runtime.getRuntime().availableProcessors() 动态获取,不同服务器不同
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
2, //核心线程数
Runtime.getRuntime().availableProcessors(), //最大线程数
5, //线程池的存活时间
TimeUnit.SECONDS, //时间单位
new LinkedBlockingDeque<>(5), //阻塞队列
Executors.defaultThreadFactory(),//线程工厂,使用默认的就可以,一般不变
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略 四种
IO密集型
根据程序中十分消耗IO的线程数目设置,最少要多于这个数值,一般设置成2倍,因为消耗IO的线程会大量占用线程,可能造成阻塞,2倍时可以保证IO线程正常运行时,其他线程也可以进行
四大函数式接口
题外话:传统程序员必备:枚举、泛型、反射
新时代程序员:Lambda、链式编程、函数式接口、Stream流式计算
函数型接口 Function<T,R>
用Lambda表达式简化,两个参数,前面是输入参数类型,后面是返回值类型(源码)
方法是apply
/*Function<String,String> f=new Function<String,String>() {
@Override
public String apply(String s) {
return null;
}
};*/
Function<String,String> function=(str)->{return str+str;};
System.out.println(function.apply("y"));
断定型接口 Predicate
一个参数,输入参数类型,返回值类型固定为boolean类型 方法是test
Predicate<Integer> p=(number)->{return number-1>0;};
System.out.println(p.test(1));
消费型接口 Consumer
只有输入,没有返回 方法是accept
Consumer<String> c=(str)->{System.out.println(str);};
c.accept("yh");
供给型接口 Supplier
只有返回,没有输入
Supplier<Integer> s=()->{ return 1024; };
System.out.println(s.get());
流式计算 : 负责计算
最重要的两个工作:存储和计算,队列等用于存储,流就是用来计算的
此处代码相当于整合了 Lambda表达式,函数式接口,链式编程、流式计算
User A=new User("A",96,3000);
User B=new User("B",29,4600);
User C=new User("C",37,3614);
User D=new User("D",18,2500);
List<User> users = Arrays.asList(A, B, C, D);
users.stream()
.filter((user)->{return user.getAge()>20;})
.map((user) -> {
user.setName(user.getName().toLowerCase());
return user;
})
.sorted((u1,u2)->{return u1.age-u2.age;})
.forEach(System.out::println);
双冒号表示 引用什么类下的什么方法
- filter 就是一个predicate函数,将原流中返回值为true的元素重新作为流 进行计算
- map 就是一个function函数,将原流中的数据作为参数,各个返回值重新作为流进行计算,此处可能会改变流的类型
- forEach 就是一个comsumer函数,对数据进行操作,不再返回,一般放在流式计算的最后
- reduce(identity,op) 前面是一开始的身份值,也就是初始值,后面是对流就行的操作,方法,使用双冒号啥的
ForkJoin
目的是通过将较大数据量的任务进行拆分,分由多个线程执行,从而提高执行效率。其中每个线程中的任务是通过双端队列进行维护的,当一个线程的任务结束,而其他线程的任务还没有结束,就会产生工作窃取,代替执行未完成的任务,提高效率。其原理就像分治算法,先分开,之后再合并
使用
- 1.创建一个实体类对象,让其继承RecursiveTask(有返回值的,泛型就是返回值类型),或者RecursiveAction (没有返回值),之后重写其中的compute方法,compute方法中间需要一个判断,来确定是否执行forkJoin,如下代码。如果需要,就创建新的对象,之后进行fork,最后返回的时候进行join
public class demo05forkJoin extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long temp=10000L;
public demo05forkJoin(Long start, long end) {
this.start=start;
this.end=end;
}
@Override
protected Long compute() {//返回值类型为泛型的类型
if(start-end>temp){
long mid=(start-end)/2;
demo05forkJoin fj1=new demo05forkJoin(start,mid);
fj1.fork();//将任务进行拆分
demo05forkJoin fj2=new demo05forkJoin(mid+1,end);
fj2.fork();
return fj1.join()+fj2.join();//将任务进行合并
}else{
long sum=0;
for(Long i=start;i<end;i++){
sum+=i;
}
return sum ;
}
}
}
并行流:也是为了提高效率 这个更提高
- 不同的对象就用不同的流 LongStream
- range 限定范围
- parallel 开启并行流
- reduce 进行操作
LongStream.range(1,1_0000_0000).parallel().reduce(0,Long::sum);
异步回调
在执行任务时,不希望阻塞,而且希望得到结果,所以让任务先去执行,等需要的时候在要结果
CompletableFuture.supplyAsync可以有返回值,也可以没有,有的话前面的泛型要写上
future.whenComplete表示执行成功的话 执行的操作 参数t就是返回值,u就是错误信息
exceptionally表示失败的话 参数就是错误信息
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int i=10/0;
return 1024;
});
future.whenComplete((t,u)->{
System.out.println(t);
}).exceptionally(throwable -> {
System.out.println(throwable.getMessage());
return -1;
});
JMM java内存模型
内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节。
Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。
一共有8个操作
- Lock与Unlock 读主内存前加锁,写回之后解锁
在加锁前必须读最新的,在解锁后必须立马回写,加锁解锁必须是用一把锁 - read和load 线程先从主存中读到数据read,之后加载到线程内存load
- use和assign 线程的执行引擎 使用数据use,然后复制给工作内存assign
- write和store 线程将数据写回主存write,并进行存储store
8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
Volatile
多线程读写问题 主内存、高速缓存、线程内存,一般是从高速缓存中读取,但是多线程环境下就会产生并发问题,synchronized可以解决,但是比较影响性能,volatile是更轻量级的选择,不会向synchronized那样阻塞线程。加volatile的变量被标记为共享变量,强制从主存中读取,修改后强制写回主存
保证可见性
保证线程中对公共变量的修改对其他线程是可见的,即某个线程一修改变量,其他线程如果在使用,其变量也会随之变化
private volatile int number=0;
不保证原子性
原子性:不可分割,要么都执行成功,要么执行失败。即在操作时,其他线程不能使用
synchronized和lock是可以保证原子性的。
i++是否安全?
不安全,无法保证其原子性,底层其实是三步:读到数据、执行+1操作、写回数据,所以可能会出错
如何不使用synchronized和lock保证原子性?
使用原子类:AtomicXxxxx AtomicInteger 其底层和操作系统挂钩,直接在内存中修改值
使用原子类中的方法 getAndIncreament i++ 和 increamentAndGet(++i) 完成+1
private AtomicInteger a=new AtomicInteger(1);//参数为赋值
禁止指令重排
计算机执行程序时,不一定是按照书写顺序执行的。
源代码–>编译器优化的重排–>指令并行也可能会重排–>内存系统也会重排–>执行
但是在处理器进行指令重排时,会考虑数据之间的依赖,所以单线程不会出现错误,但是多线程是存在错误的可能的
内存屏障
为了保证部分代码的执行顺序一定,即禁止指令重排,会有内存屏障产生,即在指令程序片段前后有CPU指令,(屏障)
- 保证代码的执行顺序
- 保证某些变量的内存可见性
volatile通过内存屏障实现了可见性和禁止指令重排,但不保证原子性
volatile的主要应用:单例模式
volatile与synchronized的区别
- 1.volatile 仅能使用在变量级别,synchronized 则可以使用在变量、方法、类级别、代码块上。
- 2.volatile 仅仅能实现变量修改可见性,并不能保证原子性,volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取,synchronized 可以实现变量的修改可见性和原子性。
- 3.volatile 不会造成线程阻塞,synchronized 可能会造成线程阻塞。
- 4.volatile 标记的变量不会被编译器优化,synchronized 标记的变量可以被编译器优化。
- 5.volatile 修饰的变量,jvm 每次都从主内存中读取,而不会从寄存器(工作内存)中读取。synchronized 表示只有一个线程可以获取作用 对象 的锁,执行代码,阻塞其他线程。
volatie 仅能实现变量的可见性,无法保证变量操作的原子性。而synchronized可以实现变量的可见性与原子性。
单例模式
基础
单例模式基础
目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
类加载顺序
类加载(classLoader)机制一般遵从下面的加载顺序
(1) 如果类还没有被加载:
- 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
- 执行子类的静态代码块和静态变量初始化。
- 执行父类的实例变量初始化
- 执行父类的构造函数
- 执行子类的实例变量初始化
- 执行子类的构造函数
同时,加载类的过程是线程私有的,别的线程无法进入。
(2)如果类已经被加载:
静态代码块和静态变量不在重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
static关键字
一个类中如果有成员变量或者方法被static关键字修饰,那么该成员变量或方法将独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享,只要这个类被加载,该成员变量或方法就可以通过类名去进行访问,它的作用用一句话来描述就是,不用创建对象就可以调用方法或者变量,这简直就是为单例模式的代码实现量身打造的。
饿汉式
在类加载的时候就完成了初始化,所以类加载较慢,但获取对象的速度较快。
缺点 可能会造成不必要的空间浪费
public class Hungry {
private Hungry(){ }
private final static Hungry INSTANCE =new Hungry();
public static Hungry getInstance(){
return INSTANCE;
}
}
Hungry instance1 = Hungry.getInstance();
懒汉式
在类加载时不初始化,等到第一次使用时才初始化
同步方法的懒汉式
单线程下是OK的,但是多线程下不行。
public class lazy {
private lazy(){}
private static lazy INSTANCE=null;
public static lazy getInstance(){
if(INSTANCE==null){
INSTANCE= new lazy();
}
return INSTANCE;
}
}
DCL懒汉式: 双重检测锁模式
这个是依然是不安全的,创建实例化类对象的过程
- 分配内存空间 1
- 执行构造方法,初始化对象 2
- 将对象指向内存空间 3
但是在多线程过程中,可能会产生指令重排,从而产生错误, 如132,在3执行后其他线程调用,此时认为对象已经创建,但实际上还没有,所以是不安全的。
public class lazy {
private lazy(){}
private static lazy INSTANCE;
public static lazy getInstance(){
if(INSTANCE==null){
synchronized (lazy.class){
if(INSTANCE==null){
INSTANCE=new lazy();
}
}
}
return INSTANCE;
}
}
DCL+volatile 防止指令重排
public class lazy {
private lazy(){}
private volatile static lazy INSTANCE;
public static lazy getInstance(){
if(INSTANCE==null){
synchronized (lazy.class){
if(INSTANCE==null){
INSTANCE=new lazy();
}
}
}
return INSTANCE;
}
}
CAS compare and swap
通过处理器的指令来保证操作的原子性
compare and swap 检查并更新 从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。其操作具有原子性,与synchronized、lock相比,效率更高
其方法底层都是调用的unsafe类里的方法,其操作都是直接操作内存的,使用的是c++中的方法
AtomicReference < T > 可以原子更新的对象引用
compareAndSet 比较对象现在的值,如果是预期值,就设置为想要的值
实际上是三个参数,还有一个是内存地址,根据内存地址可以获得自身的值
AtomicReference<String> reference=new AtomicReference<>("YH");
boolean b = reference.compareAndSet("YH", "yh");
System.out.println(b);
System.out.println(reference);
有了Synchronized和lock,为什么还要使用CAS
Synchronized原理
synchronized指令编译之后使用mointorenter和mointorexit两个字节码指令来使线程同步
mointor 管程/监视器 相当于一个同步队列,只有一个线程可以使用
但是mointor是依赖于操作系统的mytex lock来实现的
java线程实际使对操作系统线程的映射,所以每操作或挂起一个线程就需要切换操作系统的内核态,这个操作是重量级的,使用synchronized需要频繁地切换操作系统从用户态切换到内核态,消耗性能。从内存的角度来说,加锁会清除工作内存中的共享变量,然后从主内存中读取,释放锁是把工作内存中的共享变量写回主内存。
synchronized实际上还有两个队列,waitSet和entryList,当多个线程进入同步代码块时,首先进入entryList,一个线程得到monitor锁后,锁就赋值给这个线程,并且计数器+1,调用wait之后,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,notify之后,重新进入entryList进行竞争,如果线程执行完毕,就释放锁,计数器-1
lock实现原理
ReentrantLock 可重入锁是基于AQS AbstractQueuedSynchronizer 实现的,AQS 内部维护一个state状态位,需要竞争锁的线程会读取state状态,如果没有人占用,就会尝试获得,如果有人占用,线程就会进入CLH阻塞队列进行自旋等待,一段时间后进行阻塞挂起。尝试加锁的时候通过CAS去修改值,一旦修改成功,就把当前线程ID赋值,代表加锁成功,其他想要再获取的线程就会阻塞进入阻塞队列,通过自旋不断查询锁的状态,当锁被使用完后,state被置为0,重新竞争
使用CAS的原因
synchronized是重锁,对性能消耗太大,lock实际上就是CAS的上层应用,所以使用CAS是非常有必要的,降低性能消耗,提高效率
CAS存在的问题
- 存在ABA问题,即只关心是否为预期值,而不关心是否是一开始的预期值,可能其他线程已经对其进行操作,又重新设置为原来的值 ------ 通过AtomicStampedReference解决(原子引用)
//第一个参数是值,第二个参数是版本号 stamp
AtomicStampedReference<Integer> stamped=new AtomicStampedReference<>(10,1);
//四个参数 期望值、修改后的值、期望的stamp、修改后的stamp
boolean b1 =
stamped.compareAndSet(10, 13, stamped.getStamp(), stamped.getStamp() + 1);
System.out.println(b1);
System.out.println(stamped.getReference()+stamped.getStamp());
System.out.println( stamped.compareAndSet(10, 13, 1, 2));
System.out.println(stamped.getReference()+stamped.getStamp());
需要注意的是,compareAndSet使用的是= =来判断是否一致,如果是Integer类型是,在-128~127中,使用的是常量池中的值,之外的值需要重新new,此时相当于new的对象,使用= =判断的其实是地址值,会导致与期望值数值相等,但是无法完成操作
- 存在无限循环的问题,是通过自旋锁来实现的,所以会存在重复的空循环-----通过LongAdder,分段CAS和自动分段迁移
- 多变量原子问题 只能实现一个变量的原子操作-------通过AtomicReference自定义封装对象解决
AQS:AbstractQueuedSynchronizer
AQS是java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现。AQS是独占锁(例如ReentrantLock)和共享锁(例如Semaphore)的公共父类。
AQS是基于CLH阻塞队列进行实现的,如果线程在进行竞争的时候发现锁已经被占用,就会加入到阻塞队列中。
线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。,具体的可以看一下这个博客
锁的种类
锁的升级
无锁(CAS)—偏向锁—轻量级锁----重量级锁
锁的信息在对象头中,对象头又分为mark word和class point ,mark word存储的又锁的信息,如下
如果仅有一条线程来竞争,锁就是偏向锁,线程ID会放入mark word中,当下次有线程来获取时,就会判断是否位记录的线程ID,如果是,就直接使用,如果不是,说明有多个线程在竞争,锁就会升级位轻量级锁
轻量级锁主要解决的是竞争线程没有很多的时候的优化,会将mark word中线程ID那部分指向线程栈中的Lock Record,其中owner再指向mark word 实现绑定,获得锁的线程执行,其他线程自旋等待,当自旋个数超过CPU核数的一半 或者自旋次数超过10次才会升级成重量级锁
重量级锁,使用monitor监视器来实现,依赖于操作系统mutex中的lock,需要系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等,操作最严格,CPU消耗最大
锁只能升级,不能降级
公平锁和非公平锁
new ReentrantLock(true) 为公平锁,默认参数为false,非公平锁 ,非公平锁不允许插队
可重入锁
概念:可能多个锁嵌套,如果是可重入锁,那么拿到外层锁,就能够使用内层锁中的变量、方法。
所有的锁,都是可重入锁 lock synchronized
锁的优化
锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
自旋锁
使用do···while循环来持续判断状态
指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。产生重复的空循环可以设置自旋的次数,通过设置-XX:+UseSpining来开启,默认自旋次数是10次,可以使用-XX:PreBlockSpin设置
因为可能产生重复的空循环来降低CPU的性能,所以后有自适应锁,根据前一次在同一个锁上的自旋时间和锁的持有者状态来决定自旋的次数
static class MyLock{
private AtomicReference<Thread> reference=new AtomicReference();
public void lock(){
Thread thread=Thread.currentThread();
while(!reference.compareAndSet(null,thread)){
}
}
public void unlock(){
Thread thread=Thread.currentThread();
reference.compareAndSet(thread,null);
}
}
当一条线程拿到锁之后,会将AtomicReference< Thread >设置成当前线程,之后进行线程操作,此时如果有其他线程想要使用时,在lock时就会导致自旋,一值指向while循环,直到其他线程释放锁之后,才能执行后续操作
乐观锁与悲观锁
- 悲观锁阻塞事务,乐观锁回滚重试。
- 乐观锁适用于写比较少的情况下,即冲突很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。
- 如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
- 乐观锁并未真正加锁,所以效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
- 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
乐观锁的例子:CAS
死锁的排查
死锁产生的四个必要条件
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
死锁的排查
- 看日志信息 —一般看不出来
- 看堆栈信息
- 在Terminal窗口 键入jps -l 可以产看线程信息
- 使用jstack 线程号 找到线程信息
- 在Terminal窗口 键入jps -l 可以产看线程信息