JUC
1.八锁问题
synchronized锁的对象是方法的调用者,当两个方法使用同一个锁,谁先拿到谁先执行
当有两个方法,一个方法上锁,另一个方法没上锁时,同时执行多线程的的操作,没上锁的方法总是先执行
如果是静态方法上加锁(synchronized),锁的其实是Class(每个类都有一个唯一的Class对象)
想要知道哪个方法先执行就需要去观察锁的同步方法的对象是不是同一个对象,如果是同一个对象,那么先拿到锁的先执行,如果不一样那么就没法确定顺序(可以通过线程休息的方式来人为操控线程的先后执行顺序)
2.ArrayList在并发条件下的不安全问题(有关集合类的笔记)
ArrayList在单线程的情况下是安全的,但是在多个线程同时操作的时候就会不安全,下面是运行实例
package demo1;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class UnsafeArrayList {//并发修改异常
public static void main(String[] args) {
List<String> list=new ArrayList<>();//创建多个线程来对List进行操作
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},String.valueOf(i)).start();
}
}
}
1.使用Vector可以避免这个错误
2.使用Collections.synchronizedList(new ArrayList<>());
3.new CopyOnWriteArrayList<>();
CopyOnWrite 简称COW 计算机程序设计领域的一种优化策略
写入时复制 写入的时候避免覆盖,造成数据问题
读写分离
CopyOnWrite相较于Vector效率更高 CopyOnWrite 底层使用的是lock锁而不是synchronized,vector使用的是synchronized,效率更低
底层的CopyOnWrite 只在需要加锁的地方加锁,因此效率更高
HashSet的底层
public HashSet() {
map = new HashMap<>();
}
HashSet的底层其实是HashMap
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
可以看到,add方法使用的是HashMap的put方法,因为map内部的key值是唯一的,不会存在相同的值,这符合set集合的特性。其中,PRESENT是一个常量
private static final Object PRESENT = new Object();
3.Callable的实现
package demo1;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
public class CallAbleTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable=new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("执行call");
return "执行成功";
}
};
FutureTask futureTask=new FutureTask(callable);
new Thread(futureTask,"打搅望").start();
String o = (String) futureTask.get();//get方法可能会产生阻塞,通常放到最后或者使用异步通信的方式执行
System.out.println("返回值"+o);
}
}
原理
Runnable->RunnableFuture->FutureTask,FutureTask是Runnable的实现类,new Threadd的时候需要一个实现了Runnable接口的类来传参,FutureTask需要传递一个Callable实现类因此Callable和Thread正式挂钩可以通过start方法实现多线程操作
4.常见的辅助类
一、CountDownLatch
package demo1;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch countDownLatch=new CountDownLatch(10);//设定计数的次数
for (int i = 1; i <= 10; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"吃了粑粑");
countDownLatch.countDown();//倒数计数
},"学生"+i).start();
}
try {
countDownLatch.await();//等待new CountDownLatch(10);的计数结束再执行await后面的操作
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("粑粑吃完了");
}
}
把await注释掉,就不会最后执行想要执行的操作了
很明显粑粑应该是在吃完后才能输出粑粑吃完了,但是如果注释掉await的话程序直接输出粑粑吃完了,这与预期不符
二、CyclicBarrier
package demo1;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{//约定7条线程都到达指定位置后(使用await方法后)开始执行lambda表达式里面的线程
System.out.println("拉完了");
});
for (int i = 0; i < 7; i++) {
final int s=i+1;
new Thread(()->{
System.out.println("拉出第"+s+"坨粑粑");
try {
cyclicBarrier.await();//await表示自己已经到达约定地点,开始等待,直到等待的数量达到之前的设定值
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}
关于lambda表达式无法获取for循环中的参数(i)的问题
for (int i = 0; i < 7; i++) {
final int s=i+1;
new Thread(()->{
System.out.println("拉出第"+s+"坨粑粑");
try {
cyclicBarrier.await();//await表示自己已经到达约定地点,开始等待,直到等待的数量达到之前的设定值
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
理由如下
被final
修饰的变量在Java中表示常量,一旦被赋值后就不能再修改。在你提供的代码中,final int s=i+1;
语句在每个线程内部执行,为每个线程创建了一个独立的常量s
,并且该常量的作用域仅限于线程内部。
尽管在每个线程内部都有一个名为s
的常量,但它们是相互独立的,互不影响。因此,每个线程都可以修改自己的s
常量的值,而不会影响其他线程的s
常量。
需要注意的是,虽然s
的值可以在每个线程内部修改,但它仍然是一个常量,不能在其他地方再次赋值。这是因为final
修饰的变量只能被赋值一次,之后就不能再修改。
总结起来,被final
修饰的s
常量在每个线程内部是可以被修改的,但每个线程内部的s
常量是相互独立的,互不影响。
三、Semaphore
当存在这样一个资源,在多个线程中间该资源是互斥的(即只允许一个或者几个线程占用该资源),可以使用Semaphore来操作线程对资源的访问
package demo1;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(4);
for (int i = 1; i <= 15; i++) {
new Thread(()-> {
try {
semaphore.acquire();//检查拿到资源的线程数是否满,如果满了则让后面的线程等待被释放为止,没有满则获取资源
System.out.println(Thread.currentThread().getName() + "拿到车位");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
semaphore.release();//将当前的资源释放
}
},String.valueOf(i)).start();
}
}
}
5.读写锁(ReadWriteLock )
package demo1;
import java.util.HashMap;
import java.util.Map;
//读写锁
public class WriteLock {
public static void main(String[] args) {
WriteLockTest writeLockTest=new WriteLockTest();
for (int i = 1; i <= 5; i++) {
final int s=i;
new Thread(()->{
writeLockTest.push(s+"",s+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int s=i;
new Thread(()->{
writeLockTest.get(s+"");
},String.valueOf(i)).start();
}
}
}
class WriteLockTest{
Map<String,String> map=new HashMap<>();
public void push(String key,String val){
System.out.println(Thread.currentThread().getName()+"开始写入");
map.put(key,val);
System.out.println(Thread.currentThread().getName()+"写入成功");
}
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取开始");
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
}
}
这是没有加锁的线程,对这样的读写操作十分的混乱!运行结果如下
这是只对写加锁的代码
package demo1;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//读写锁
public class WriteLock {
public static void main(String[] args) {
WriteLockTest2 writeLockTest=new WriteLockTest2();
for (int i = 1; i <= 5; i++) {
final int s=i;
new Thread(()->{
writeLockTest.push(s+"",s+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int s=i;
new Thread(()->{
writeLockTest.get(s+"");
},String.valueOf(i)).start();
}
}
}
class WriteLockTest2{
Map<String,String> map=new HashMap<>();
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
public void push(String key,String val){
try {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"开始写入");
map.put(key,val);
System.out.println(Thread.currentThread().getName()+"写入成功");
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取开始");
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
}
}
可以看到如果不对读取加锁仍然会导致输出逻辑混乱,我们想要一个先写入,在写入完成过后再进行读取操作
接下来对读取操作加锁
package demo1;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//读写锁
public class WriteLock {
public static void main(String[] args) {
WriteLockTest2 writeLockTest=new WriteLockTest2();
for (int i = 1; i <= 5; i++) {
final int s=i;
new Thread(()->{
writeLockTest.push(s+"",s+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int s=i;
new Thread(()->{
writeLockTest.get(s+"");
},String.valueOf(i)).start();
}
}
}
class WriteLockTest2{
Map<String,String> map=new HashMap<>();
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
public void push(String key,String val){
try {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"开始写入");
map.put(key,val);
System.out.println(Thread.currentThread().getName()+"写入成功");
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
try {
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"读取开始");
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
} finally {
readWriteLock.readLock().unlock();
}
}
}
可以看到程序完全按照预想的步骤运行
6.阻塞队列(ArrayBlockingQueue)
方式 | 抛出异常 | 不抛出异常,但是有返回值 | 阻塞,等待 |
---|---|---|---|
添加 | add(返回值为Boolean) | offer(返回值为Boolean) | put(无返回值) |
删除 | remove(返回值为Boolean) | poll(返回删除元素的值,失败返回null) | take(返回删除元素的值) |
队首元素 | element(返回值为元素本身) | peek(返回值为元素本身或者null) | 不存在该类型的方法 |
public static void test01(){
ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue<>(3);//设定队列的大小为3
System.out.println(blockingQueue.add(0));//这是抛出异常的方法,但是返回值为布尔类型
System.out.println(blockingQueue.add(1));
System.out.println(blockingQueue.add(2));
blockingQueue.add(12);
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
blockingQueue.remove();
}
public static void test02(){
ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer(0));//不抛出异常,如果添加元素失败直接返回false
System.out.println(blockingQueue.offer(1));
System.out.println(blockingQueue.offer(2));
System.out.println(blockingQueue.offer(3));
System.out.println("=========================================");
System.out.println(blockingQueue.poll());//移除元素失败直接返回null
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
public static void test03() throws InterruptedException {//这是阻塞方法
ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue<>(3);
blockingQueue.put(0);//当加入的元素超过队列所能容纳的极限的时候,该方法就会使程序阻塞,
// 如果不使用其他方法让队列元素减少或者手动停止程序,程序就永远阻塞在该位置
blockingQueue.put(1);
blockingQueue.put(2);
//blockingQueue.put(3);
System.out.println(blockingQueue.take());//当队列没有取出的元素的时候,该方法就会使程序阻塞,
// 如果不使用其他方法让队列添加元素或者手动停止程序,程序就永远阻塞在该位置
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
}
其中offer和poll方法有重载
public static void test04() throws InterruptedException {
ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer(0));//不抛出异常,如果添加元素失败直接返回false
System.out.println(blockingQueue.offer(1));
System.out.println(blockingQueue.offer(2));
System.out.println(blockingQueue.offer(3,1, TimeUnit.SECONDS));//添加了时间处理,参数是等待的时间,如果时间结束前无法实现添加方法则直接结束方法
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));//超过一秒钟就退出
}
关于以上表格其实在源码官方注释中有详细说明:
7.线程池
一、三大方法
ExecutorService executorService = Executors.newSingleThreadExecutor();//单个线程
ExecutorService executorService = Executors.newFixedThreadPool(5);//多个线程,此处设定为五个
ExecutorService executorService = Executors.newCachedThreadPool();//可伸缩式线程个数
-
Executors.newSingleThreadExecutor
该方式创建单个线程去执行将要执行的任务
-
Executors.newFixedThreadPool(5)
创建多个线程去执行将要执行的任务,括号内传递希望用多少条线程去执行
-
Executors.newCachedThreadPool()
可伸缩的线程数,理论上最大线程数等于将要执行的任务数量,实际上不一定能够分配这么多线程来执行任务
二、七大参数以及自定义线程池
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;
}
下面是官方解释
(1).corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非设置了 allowCoreThreadTimeOut。
抽象成银行办理业务可以这样理解:有这样一个银行,他有若干个窗口,银行肯定不能休息,所以必须要有几个窗口一直开着,corePoolSize就是设定这个一直开着的窗口的数量,这就能解释——要保留在池中的线程数,即使它们处于空闲状态
corePoolSize可以理解为在银行取钱时已经开放的窗口,也就是线程池活跃的线程数。
(2).maximumPoolSize—池中允许的最大线程数
银行的客户有时多有时少,多的时候就需要把所有办理业务的窗口对外开放,该参数则是设定这样一个值,将该值作为可以开放的最大数量,假设最大数量为5,corePoolSize 为2,那么暂未开放的窗口数量就是3。
那么可以这样理解:池中允许的最大线程数,实际上就是允许最大活跃的线程数
(3).keepAliveTime – 当线程数大于核心时,这是(线程池中)多余的空闲线程在终止之前等待新任务的最长时间。
抽象成银行业务办理可以这样理解:银行在一段时间内处理了大量业务,处理完过后有几个窗口是空闲的,银行会再开启这些空闲的窗口一段时间,超过一定时间窗口就暂时关闭了,这就是对——(线程池中)多余的空闲线程在终止之前等待新任务的最长时间的理解
(4).unit – keepAliveTime 参数的时间单位
这个不必多说,时间单位是秒、时、分、天等等。
(5).workQueue – 用于在执行任务之前保留任务的队列。此队列将仅保存由 execute 方法提交的可运行任务。
可以这样理解:银行不只是有办理业务的窗口,还有个客户等待区,处于客户等待区的客户在窗口办理业务的客户办理完过后就会按照顺序去窗口办理业务了,workQueue存放的就是这样一群“客户”
(6).拒绝策略
如下图可见,拒绝策略有四个,他们都是共同实现了一个叫做rejectedExecution的接口,超出最大承载就会被拒绝策略拒绝
AbortPolicy
package demo2;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ParamTest {
public static void main(String[] args) {
//自定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(5),//可以理解成银行的待客区
Executors.defaultThreadFactory(), //线程工厂
new ThreadPoolExecutor.AbortPolicy()//这是默认的拒绝策略,直接不接受多出来的任务
);
try {
for (int i = 1; i <=15; i++) {
final int tem=i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了"+tem);
});
}
} finally {
threadPoolExecutor.shutdown();
}
}
}
该策略会抛出异常
CallerRunsPolicy
main执行了多出的线程,需要注意的是,由于处理速度的原因,main可能会处理多个线程,main处理的线程不一定是一个固定值
DiscardPolicy
队列满了不会抛出异常,但是也不会处理多出来的业务
DiscardOldestPolicy
队列满了会尝试和第一个线程竞争,如果第一个线程处理完成,那么就处理该任务,如果没有则不处理该任务,也不会抛出异常
8.函数型接口与断定型接口
一、函数式接口
既有参数也有返回值,传进去一个T返回一个R
二、断定型接口
可以看到返回值是布尔值
输入一个参数返回值是布尔值的接口就是断定型接口
三、消费者接口
四、供给型接口
没有参数,只有返回值
9.JMM
Java内存模型,是一个不存在的东西,是一种约定和概念
线程内部有一个自己的工作内存,有一个自己的执行引擎
关于JMM的约定
1.线程解锁前必须把共享变量刷回主存
2.线程加锁前,必须读取主存中的最新值放到工作内存中
3.加锁解锁必须是同一把锁
JMM有八种操作
read、load、use、assign、write、store、lock、unlock
大概模型如下
import java.util.concurrent.TimeUnit;
public class JmmTest {
public static volatile boolean flag=true;//使用volatile可以保证在修改了flag之后可以及时把值传递到线程中去
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(flag){
System.out.println(1);
try {
TimeUnit.MICROSECONDS.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
TimeUnit.SECONDS.sleep(1);//如果不在此处让主线程休息,其他线程根本没法启动就结束了,因为主线程直接就把值设定成false不让自定义线程跑了
flag=false;
System.out.println(flag);
}
}
10.Volatile
保障可见性
不加volatile的值可以保证可见性,如下所示,对于内部的线程flag变量本来是不可见的,意思就是线程无法根据后面的操作来更新flag的值,但是加上volatile关键字过后就可以保证实时更新了,当flag发生变化的时候,while内部循环就不输出了。
可以看到,没有加volatile关键字,程序一直卡在while循环内部了
import java.util.concurrent.TimeUnit;
public class JmmTest {
public static volatile boolean flag=true;//使用volatile可以保证在修改了flag之后可以及时把值传递到线程中去
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(flag){
System.out.println(1);
try {
TimeUnit.MICROSECONDS.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
TimeUnit.SECONDS.sleep(1);//如果不在此处让主线程休息,其他线程根本没法启动就结束了,因为主线程直接就把值设定成false不让自定义线程跑了
flag=false;
System.out.println(flag);
}
}
不保证原子性
有以下代码
package demo03;
public class AtomicTest {
public static int num=0;
public static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
System.out.println(num);
}
}
预期的输出结果是20000,因为add被执行了20000次,但是很容易知晓,这样多的线程都去操作add函数来使得num++,总会引发数据的异常,我们知道使用synchronized能规避这一问题
实际上如果对num值声明为volatile,并不能保证输出的一定是20000,这是因为add操作并不是原子操作,如下图,并没有输出20000
打开该类的字节码文件可以看到,即便是add内部只有一个num=num+1,但是字节码文件里面的操作不止一步,因此volatile并没有保证原子性
所以要使用原子包装类进行处理
可以看到使用原子类操作输出了我们预期的值
事实上原子类使用了很多声明为unsafe的方法,点进unsafe类去看,发现很多native方法,这是java通过native方法直接操作了cpu对程序进行了调度,因此效率极高
禁止指令重排
什么是指令重排:你写的程序并不是按照你写的那样去执行的
源代码–>编译器优化重排—>指令并行也有可能重排—>内存系统也会重排—>执行
处理器在进行指令重排时,会考虑数据之间的依赖性
11.单例模式
懒汉模式
package demo03;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"执行");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){//LazyMan是一个static变量,应该是只允许一条线程去创建它,因此下面的输出应该是一条执行的线程
lazyMan=new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
这样的输出显然不符合预期
于是将lazyman进行上锁操作
package demo03;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"执行");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();//不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
可以看到,已经符合预期了,但是这不是一个原子性操作
这里可以解释一下为什么要使用双重if,如果只用了一层if假设有t1,t2两个线程,t1判断为null准备创建实例,t2此时跟t1是同时判断的,t2也判定为null。这个时候假设t1拿到了锁创建对象,那么t2在等待t1过后因为t2之前判断过为null他也要获取锁来创建对象,这个时候单例模式就不安全了
程序在执行getInstance时有以下步骤
1.分配内存空间
2.执行构造方法,初始化对象
3.把新对象指向这个空间
我们期望按照123步骤执行,但是CPU可能会使用132的顺序执行
假设在A线程做了指令重排按照132执行,有一个B线程在执行到步骤2的时候访问了lazyMan变量直接把lazyMan返回了,但是这个lazyMan根本没有初始化,于是就产生了数据错误
添加volatile关键字防止指令重排
由于反射的存在,所有的类都是不安全的,可以通过反射直接访问类的内部变量然后直接生成一个新的对象,这个对象的hashcode会跟直接通过类的初始化的hashcode不一样,这样的单例模式显然是不安全或者说没有达到预期,于是我们采用枚举类
枚举类似乎会欺骗人
这里输出了一个NoSuchMethod,就是说没有这个方法,这就很奇怪了——.class文件里面明明有一个无参构造啊,你就算有保护也是给我抛出一个不要用反射破坏枚举啊
可以用某些特殊手段(jad)把源码反编译出来,发现存在一个有参构造,里面是使用了String和int类型,这下提示说不让你去反射枚举来创建对象了
下面是反编译的结果
饿汉模式
饿汉模式就是拿到一个对象就狠狠加载,这样的模式十分耗费计算机资源
12.CAS
什么是CAS机制(compare and swap)
CAS算法的作用:解决多线程条件下使用锁造成性能损耗问题的算法,保证了原子性,这个原子操作是由CPU来完成的
CAS的原理:CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后的新值。
(1)如果内存中的值和预期原始值相等, 就将修改后的新值保存到内存中。
(2)如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。
注意:
(1)预期原始值(A)是从偏移位置读取到三级缓存中让CPU处理的值,修改后的新值是预期原始值经CPU处理暂时存储在CPU的三级缓存中的值,而内存指定偏移位置中的原始值。
(2)比较从指定偏移位置读取到缓存的值与指定内存偏移位置的值是否相等,如果相等则修改指定内存偏移位置的值,这个操作是操作系统底层汇编的一个原子指令实现的,保证了原子性
JVM中CAS是通过UnSafe类来调用操作系统底层的CAS指令实现。
CAS基于乐观锁思想来设计的,其不会引发阻塞,synchronize会导致阻塞。
原子类
java.util.concurrent.atomic包下的原子类都使用了CAS算法。而java.util.concurrent中的大多数类的实现都直接或间接的使用了这些原子类。
Unsafe类使Java拥有了类似C语言指针操作内存空间的能力,同时也带来了指针的安全问题。
AtomicInteger原子类
AtomicInteger等原子类没有使用synchronized锁,而是通过volatile和CAS(Compare And Swap)解决资源的线程安全问题。
(1)volatile保证了可见性和有序性
(2)CAS保证了原子性,而且是无锁操作,提高了并发效率。
操作步骤:
(1)获取AtomicInteger对象首地址指定偏移量位置上的值,作为期望值。
(2)取出获取AtomicInteger对象偏移量上的值,判断与期望值是否相等,相等就修改AtomicInteger在内存偏移量上的值,不相等就返回false,重新执行第一步操作,重新获取内存指定偏移量位置的值。
(3) 如果相等,则修改值并返回true。
注意:从1、2步可以看CAS机制实现的锁是自旋锁,如果线程一直无法获取到锁,则一直自旋,不会阻塞
CAS和syncronized的比较
CAS线程不会阻塞,线程一致自旋
syncronized会阻塞线程,会进行线程的上下文切换,会由用户态切换到内核态,切换前需要保存用户态的上下文,而内核态恢复到用户态,又需要恢复保存的上下文,非常消耗资源。
CAS的缺点
(1)ABA问题
如果一个线程t1正修改共享变量的值A,但还没修改,此时另一个线程t2获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1检查发现共享变量的值没有发生变化,但是实际上却变化了。
解决办法: 使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JUC包里提供了一个类AtomicStampedReference来解决ABA问题。AtomicStampedReference类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
下面是加入版本号的代码示例
package demo03;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASTest {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicReference=new AtomicStampedReference<>(1,1);
new Thread(()->{
int st=atomicReference.getStamp();
System.out.println("a1--------->"+st);
System.out.println("a "+atomicReference.compareAndSet(1, 6, atomicReference.getStamp(), atomicReference.getStamp() + 1));//先将一改成6
System.out.println("a2--------->"+atomicReference.getStamp());
System.out.println("a "+atomicReference.compareAndSet(6, 1, atomicReference.getStamp(), atomicReference.getStamp() + 1));//再将6改成1
System.out.println("a3--------->"+atomicReference.getStamp());
}).start();
new Thread(()->{
int st=atomicReference.getStamp();
System.out.println("b1--------->"+st);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("b "+atomicReference.compareAndSet(1, 7, st, st + 1));
System.out.println("b2--------->"+atomicReference.getStamp());
}).start();
}
}
输出结果如下
B线程并没有对值做出修改,
(2)循环时间长开销会比较大:自旋重试时间,会给CPU带来非常大的执行开销
(3)只能保证一个共享变量的原子操作,不能保证同时对多个变量的原子性操作