JUC中的休息、暂停、sleep是TimeUnit.SECONDS.sleep()
java只能去调用本地方法开启线程,自身是开不了的
java无法操作硬件,他是运行在jvm虚拟机之上的
并行与并发
线程有几个状态
java中的sleep方法是thread.sleep(),而Python是time.sleep
但是在企业中一般不使用thread.sleep
而用
两者最主要的区别在于:sleep()方法没有释放锁,而wait()方法释放了锁。
两者都可以暂停线程的执行。
wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(longtimeout)超时后线程会自动苏醒。
现在wait也需要捕获异常;
企业中开发多线程
模板:
这里reentrantlock又分为
如果一个线程要3小时一个,只要3分钟,那么用公平锁就要排队3小时
synchronize和lock锁区别
synchronized关键字修饰的代码相当于数据库上的互斥锁。确保多个线程在同一时刻只能由一个线程处于方法或同步块中,确保线程对变量访问的可见和排它,获得锁的对象在代码结束后,会对锁进行释放。
synchronzied使用方法有两个:①加在方法上面锁定方法,②定义synchronized块。
synchronized是Java中的关键字,是一种同步锁能够保证代码片段的原子性(也就是当同步代码块出现异常的时候,所有的操作都会回滚)和可见性但是不保证有序性。它修饰的对象有以下几种:
【注意】,synchronized是不能直接修饰变量的,会报错Modifier'synchronized'notallowedhere
但是可以通过同步代码块来修饰,举例:
这里很明显resource1的锁和resource2的锁不是同一把锁(因为得到是指定对象的锁,不管这个对象是不是static的所以这里的resource1的锁和resource2的锁是两个对象各自的锁),否则会出现可重入
privatestaticStringresource1="abc"privatestaticStringresource2="abc"//那么你下面获取resource1和source2就是同一个对象的锁,因为“abc”会被放到常量池中publicstaticvoidmain(String[]args){newThread(()->{synchronized(resource1){System.out.println(Thread.currentThread()+"getresource1");try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(Thread.currentThread()+"waitinggetresource2");synchronized(resource2){System.out.println(Thread.currentThread()+"getresource2");}}},"线程1").start();
publicvoidsynchronizedmethod(){}
synchronized(mythread.class){},,例如:
在javaguide->并发编程->进阶篇->1.3
synchronized同步语句块的实现使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器monitor的获取。
首先synchronized获得锁的对象是根据修饰的不同而不同,一般分为对象和.class模板
而Lock的锁是来自其新建的newReentrantLock对象
1.synchronize是内置关键字,lock是java类
2.synchronize无法判断获取锁的状态,lock可以判断是否获得了锁
3.synchronize会自动释放锁(正常执行完程序或者抛异常都会自动释放),lock必须手动释放锁,不释放锁就会死锁
4.synchronize线程1(获得锁,阻塞)线程2(等待,傻傻的等)因为非公平锁;lock锁就不一定会等待下去,lock.trylock可以尝试获取锁
5.synchronize可重入锁,不可以中断,只能是非公平锁;lock。可重入锁,可以判断锁,非公平锁或者公平锁(可自己设置)
6.synchronize适合锁少量的同步代码,lock适合大量的同步代码。lock更灵活
7.synchronized关键字与wait()和notify()/notifyAll()方法(this.wait()和this.notify())相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
锁是什么,如何判断锁是谁!
wait和notifyAll都是通过对象的this调用的
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。
1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
2)当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,
(这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;(线程去竞争)
(notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的概率是随机的,取决于cpu调度
面试:单例模式、排序、生产者消费者、死锁问题
notifyall就是唤醒等待的线程
如果有ABCD4个线程:用以上方法可能存在虚假唤醒
把if判断改为while判断,因为多个线程都被唤醒了,很可能其中一个唤醒的线程,先一步改变的condition.此时另一个线程的condition已经不满足,因此需要加Where再次判断,参考下列代码和结果:
if唤醒之后不会再次判断,while唤醒之后会再次判断
新版:
Locklock=newReentrantLock();Conditioncondition=lock.newCondition();condition.await();//等待condition.signalALL());//唤醒全部condition优势
目前线程是随机状态执行
如何有序执行呢
8锁就是关于锁的8个问题
synchronize锁的对象是方法的调用者
1
.由于多线程是共享主线程的堆和方法区的,所以能够使用主线程的对象方法
2.hello方法没有锁
增加一个普通方法后,由于发短信延时4s,所以先执行hello再执行发短信
4.staticsynchronize静态方法,锁的对象是class模板,与对象无关。还是发短信优先。两个都是静态锁
5.普通同步和静态同步,一个锁对象,一个锁class模板,是两把锁
小结
普通同步锁,锁的是对象
静态同步锁,锁的是class模板
出现并发修改异常:ConcurrentModificationException
importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.UUID;importjava.util.Vector;publicclassArrayListNotSafeDemo{publicstaticvoidmain(String[]args){List
List不安全
解决方案:1.是用Vector,这是安全的2.使用collection把集合变安全Collections.synchronizedList(newArrayList<>());
3.使用JUC下面的CopyOnWriteArrayList(写入时复制),这个比vector牛逼,因为vector使用synchronize而后者用的lock
Copyonwrite写入时复制COw计算机程序设计领域的一种优化策略;//多个线程调用的时候,List,读取的时候,固定的,写入(覆盖)在写入的时候避免覆盖,造成数据问题,是读写分离的
方案二:Collections.synchronized()采用Collections集合工具类,在ArrayList外面包装一层同步机制
上一节程序导致抛java.util.ConcurrentModificationException的原因解析
综上所述,假设线程A将通过迭代器next()获取下一元素时,从而将其打印出来。但之前,其他某线程添加新元素至list,结构发生了改变,modCount自增。当线程A运行到checkForComodification(),expectedModCount是modCount之前自增的值,判定modCount!=expectedModCount为真,继而抛出ConcurrentModificationException。方案三:
CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[]newElements,然后新的容器Object[]newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
publicbooleanadd(Ee){finalReentrantLocklock=this.lock;lock.lock();try{Object[]elements=getArray();intlen=elements.length;Object[]newElements=Arrays.copyOf(elements,len+1);newElements[len]=e;setArray(newElements);//讲原引用指向新的returntrue;}finally{lock.unlock();}
Set不安全
解决方法:①Collections.synchronizedSet(newHashSet<>())②CopyOnWriteArraySet<>()(推荐,这是JUC提供的)
hashset的底层就是hashmap,就是用了hashmap的key,因为key是不重复的
publicHashSet(){map=newHashMap<>();}//addset本质就是mapkey是无法重复的,这也就是为什么set是无序的,因为没法对key进行排序存储publicbooleanadd(Ee){returnmap.put(e,PRESENT)==null;//这里的PRESENT是一个常量newObject}privatestaticfinalobjectPRESENT=newobject();//不变得值!与list同理
HashMap不安全
解决方法:
重要:加载因子0.75和初始化容量16
原因:
我们知道hashmap的扩容因子是0.75,如果hashmap的数组长度已经使用了75%就会引起扩容,会新申请一个长度为原来两倍的桶数组,
然后将原数组的元素重新映射到新的数组中,原有数据的引用会逐个被置为null。就是在resize()扩容的时候会造成线程不安全。
另外当一个新节点想要插入hashmap的链表时,在jdk1.8之前的版本是插在头部,在1.8后是插在尾部。
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,
那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,
所以如果我们已经预知hashmap中元素的个数,那么预设数组的大小能够有效的提高hashmap的性能。
(1)在put的时候,因为该方法不是同步的,假如有两个线程A,B它们的put的key的hash值相同,不论是从头插入还是从尾插入,假如A获取了插入位置为x,
但是还未插入,此时B也计算出待插入位置为x,则不论AB插入的先后顺序肯定有一个会丢失;
(2)在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,
主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。
1、可以有返回值2、可以抛出异常3、方法不同,run(/call)
线程启动的方式其实只有一个newThread().start();
代码测试
因为FutureTask里面的构造器能够接收callable,并且FutureTask是Runnable的实现类
使用FutureTask.get()获得callable的返回结果,get()方法可能会产生阻塞,等待结果的产生,或者使用异步通信,结果产生了才去拿,没产生就先执行后面的代码
细节:
1.有缓存
2.结果可能需要等待,会阻塞
这里其实就是只会有线程A执行,线程B不会执行,因为AB执行的是同一个对象,检测到futuretask已经执行过,就不会再执行了
下面的类都是AQS的组件
CountDownLatch(倒计时器):CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
如果不使用.await则会出现没有归0就closedoor的情况,使用之后必须归0才能关门,但是里面线程执行的顺序是不确定的
CyclicBarrier(循环栅栏):CyclicBarrier和CountDownLatch非常类似,它也可以实现线程间的技术等待,但是它的功能比CountDownLatch更加复杂和强大。主要应用场景和CountDownLatch类似。CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
很明显线程是在await会执行+1操作,之后线程沉睡,然后,当到达7就会被唤醒,结束线程
也就是所有之前的线程都会等待知道最后一个线程出现才会同时进行
例子:抢车位6辆车抢3个车位
Semaphore.acquire():获得,假设已经满了,就会等待被释放
Semaphore.release():释放,会将当前信号量+1,然后唤醒等待的线程
作用:多个共享资源互斥的使用!并发限流,控制最大的线程数!
读可以被多个线程读,写只能被一个线程,读锁和写锁是互斥的马也就是读的时候不能写
读的时候也需要加锁这样能保证读的时候不能写入,就没有脏数据进来
ReadwriteLock
读-读可以共存!读-写不能共存!写-写不能共存!
这里会出现一个问题,某一个写完之后其实是可以允许读的,但是写的过程中不允许读
在java.util中有
blockingqueue是继承collection接口(又继承了iterable接口)的子接口,与list和set并列
Queue的实现类LinkedList
什么情况下我们会使用阻塞队列:多线程并发处理,线程池!
队列四组API
阻塞等待,是如果队列满了会等待,并且不会抛异常,没有返回值,会卡住
不存储元素,放入一个元素之后必须要先取出来,才能再放入,相当于等待的阻塞大小为1的队列
线程池:三大方法、7大参数,4种拒绝策略
池化技术
程序的运行,本质:占用系统的资源!优化资源的使用!=>池化技术线程池、jdbc连接池、内存池、对象池//因为创建和销毁十分浪费资源,所以只创建一次,然后将其保存起来,要用的时候就去拿,用完就放回来
池化技术︰事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程复用、可以控制最大并发数、管理线程
Executors是ThreadPoolExecutor的工具类(一般带有s的都是工具类),三大方法
//执行的时候threadPool.execute(()->{})
使用线程池之后使用线程池进行创建线程
1.newSingleThreadPool:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
2.newFixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
3.newCacheThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。量的请求,从而导致OOM。
CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。
同理
源码分析
三个类底层都是调用的ThreadpoolExecutor
注意这里可伸缩线程最大是integer的值,是21亿,但是服务器不可能跑这么多,可能出现资源耗尽OOM,阿里规则建议使用ThreadpoolExecutor来创建
其中阻塞队列就是候客区,
正常创建:
privatestaticfinalintCORE_POOL_SIZE=5;privatestaticfinalintMAX_POOL_SIZE=10;privatestaticfinalintQUEUE_CAPACITY=100;privatestaticfinalLongKEEP_ALIVE_TIME=1L;publicstaticvoidmain(String[]args){//使用阿里巴巴推荐的创建线程池的方式//通过ThreadPoolExecutor构造函数自定义参数创建ThreadPoolExecutorexecutor=newThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS,newArrayBlockingQueue<>(QUEUE_CAPACITY),newThreadPoolExecutor.CallerRunsPolicy());for(inti=0;i<10;i++){executor.execute(()->{try{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("CurrentThreadname:"+Thread.currentThread().getName()+"date:"+Instant.now());});}//终止线程池executor.shutdown();try{executor.awaitTermination(5,TimeUnit.SECONDS);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Finishedallthreads");}
分析:核心线程池是永远会开的,其余的最大线程池减去核心线程池是当候客区已经满了,忙不过来的时候会开
所以12是核心线程池,12345是最大线程池,候客区就是阻塞队列,当候客区满了之后才回去开启最大线程里面的
人太多了,红色的也营业,
线程池最大承载是最大线程数+阻塞区
abortPolicy:不处理新进入的,并抛出异常
callerrunsPolicy:哪里来的去哪里,队列满了交给main线程处理,不会抛出异常
DiscartPolicy:任务满了就丢掉新的,不抛出异常
DiscartOldPolicy:把最前面的线程丢掉,执行最新的,不抛出异常
总结:拒绝新线程(1.抛出异常2.不抛出异常)交给main线程执行新的,丢掉最前面的
IO密集型(I/Obound)IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O(硬盘/内存)的读/写操作,此时CPULoading并不高。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
cpu密集型(需要CPu运算的):这个就直接吧线程数设置为cpu核数,用这个获取Runtime.getRuntime().availableProcessors()
io密集型:io是十分耗时的,比如15个大型任务,设置30个线程
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
Java中超级多FunctionInterface函数式接口
只要是函数式接口都可以用lambda表达式简化
函数式接口:只有一个方法的接口
例如:runnable接口
例如:foreach的中consumer参数
publicvoidforEach(Consumer
Function函数型接口,有一个输入,有一个输出
注意,这里的T和R是指在这个泛型中会用到的两个东西,并不是特指后面的就是返回值
用lambda表达式简化
一个参数str,可以省略()
输入值,返回布尔值
只有输入,没有返回值
@FunctionalInterfacepublicinterfaceConsumer
只有返回值,没有输入
一定会用到Arrays.asList
大数据:存储+计算
集合、Mysql本质就是存储东西
计算都交给流来做;
理解:list里面的数据转化为流,然后流依次通过filter,只有返回为true的才能通过,并保存起来
这里面的lambda表达式相当于匿名类重写接口的方法
注意这里map应该改为
.map((u)->{u.setname(u.getname().touppercase);returnu})这样才能转化为user类
map是映射方法去对流进行操作,但如果流中包含流,map不会将流整合,去将流数据里面的流数据作为基本对象操作
经过map处理后,得到的东西会放在一起,例如这里就是返回的一推大写U的name
这里的map就是得到了一堆字符串
JDK1.7之后出现的,并行执行任务,提高大数据效率
大数据:MapReduce(把大任务拆分为小任务)
这就是类似分治算法
forkjoin特点:工作窃取
两个线程AB,B的任务做完了之后,可以拿A的任务过来帮忙跑,
这一行小狂神讲的很撇,没听懂
这里是用0+到10亿来,区分普通方法和forkjoin以及并行流
这里使用并行流更快,stream
异步回调不用等待,我们最初学的异步调用就是ajax,就是我们请求后不需要立即拿到结果,当他拿到之后返回给我们就行了
首先看一下Future接口,是对未来的get方法返回的结果类型
异步回调相当于是多线程,在main线程执行到异步的时候会再开一个线程,main会继续往下执行,然后那个线程之后会返回来值
但是异步与多线程区别是,多线程没有返回值,异步回调是有返回值的
请你谈谈对volatile的理解
volatile是java虚拟机提供的轻量级同步机制
1.保证可见性
2.不保证原子性
3.禁止指令重排(保证有序性)
什么是JMM
JMM:java内存模型,是不存在的,是一种概念,约定
关于JMM的一些同步的约定∶1、线程解锁前,必须把共享变量立刻刷回主存。(把虚拟栈里面的变量搞到主线程里面的虚拟栈去)2、线程加锁前,必须读取主存中的最新值复制一份到线程的工作内存中!
3、加锁和解锁是同一把锁
可见性:线程A修改了值之后了,线程B要及时的能够读取到修改后的值
javaGuide->并发编程-》进阶篇-》2
volatile关键字并不是保证多线程安全的,因为他并没有锁,一个线程获取了之后在处理,另一个线程也可以获取,虽然有可见性,但是仅仅一个volatile并不能保证多线程安全,synchronized+volatile可以保证(看下面volatile没有原子性有例子)
synchronized关键字和volatile关键字是两个互补的存在(一般一起使用),而不是对立的存在!
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是JMM抽象出来的一个概念,存储了主内存中的共享变量副本。
原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized可以保证代码片段的原子性。
可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile关键字可以保证共享变量的可见性。注意当一个线程更新了值之后会刷进主内存,并且其余使用到这个变量的线程也会更新这个变量
有序性:代码在执行的过程中的先后顺序,Java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile关键字可以禁止指令进行重排序优化
可见性也就是,当这个值发生改变之后,会通知别的线程,将别的线程里面的值也变为改变后的值
下面这个程序是不会停下来,因为A线程只复制了第一次的值,第二次主线程更改后已经刷新进了主内存,但是A线程并没有复制变化后的值,所以我们需要让线程A知道主内存这个值变化了。
这个volitile就能解决,保证了可见性,加了volitale就可以结束,保证值的可见性
注意看字节码,原子性能保证这3步操作一口气执行完,不可分割,
但是由于add方法没有保证原子性,因此++操作是不能直接执行完的,这里你可以使用CAS原理(其实现类的AtomicInteger)即来实现++操作,通过调用atomicInteger.getAndIncrement()方法实现原子性,具体的看下面CAS里讲的
线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。
这个结果不是20000
这个原因是因为可见性
如果加了sychronized之后就是一个线程技术之后还是20000,因为他不仅有可见性,还保证了原子性
如果加了volatile之后还不是20000,因为他不保证原子性因为++操作的时候,有三步,取值,+1,赋值给原变量。所以在+1的操作的时候,可能别的线程已经+1了,最终两个add方法写回的时候只加了1次,这样就会错误
如果不加lock和sychronize怎么保证原子性:
1.首先打开测试代码
2.通过javap-cxxx.class,可以将.class文件展示处理,.class文件就是字节码文件,下面这个就是字节码文件,跟汇编操作一样.
3.使用java.concurrent.atomic原子包装类,这就能实现原子性
这个原子类的底层是采用了CAS(直接与操作系统挂钩,在内存中修改值,使用Unsafe类),这个比synchronize和lock还要高效
什么是指令重排:你写的程序,计算机并不是按照你写的那样执行的,计算机会自己优化,是汇编码的重排
源代码-->编译器优化重排--》指令并行也可能重排--》内存系统也可能重排--》执行
处理器在进行指令重排的时候,考虑∶数据之间的依赖性!
单线程没有影响,指令重排主要影响多线程
加了volatile会在底层加内存屏障,不允许上面的程序与下面的程序顺序颠倒
CAS是CPU的并发原语(也就是cpu的指令),compareAndSwap:比较并交换
CAS(compareandswap),比较并交换。可以解决多线程并行情况下使用锁造成性能损耗的一种机制.CAS操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。一个线程从主内存中得到num值,并对num进行操作,写入值的时候,线程会把第一次取到的num值和主内存中num值进行比较,如果相等,就会将改变后的num写入主内存,如果不相等,则一直循环对比,知道成功为止。
CAS∶比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就—直循环!
上面这个图中的getAndAddInt是AtomicInteger的方法,里面调用了compareAndSwapInt()这是一个原语方法,底层调用c++
publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);//参数含义:如果var1对象的内存地址偏移里的值var2,等于var4,那么就让这里面的值等于var5也就是说如果拿到我们的期望值var4,就把内存地址里的值改为var5//有点像我们的乐观锁,会去对比目前的值和之前取到的值
Unsafe类:java一般情况下想要操作内存只能调用本地方法c++写的,也就是这个unsafe类也可以封装了本地方法操作内存
compareAndSwap(a,b):如果是传入的是期望值,则交换a、b
缺点∶1、循环会耗时2、一次性只能保证一个共享变量的原子性(这里因为原来的普通方法num++是没有原子性的,因此++方法在执行的时候会被拆分为三步)
比如
-------没有原子性--------------publicvoidadd(intnum){num++;}-------有原子性--------------publicsynchronizedvoidadd(intnum){num++;}-------有原子性--------------publicAtomicIntegeratomicInteger=newAtomicInteger(0);//相当于我们定了一个初值为0的num//---或者intnum=0;publicAtomicIntegeratomicInteger=newAtomicInteger(num);//相当于我们定了一个初值为0的numpublicvoidadd(){atomicInteger.getAndIncrement();//这里又是有原子性的,不会出现volatile没有原子性出现的问题}
3、ABA问题
为什么要使用原子类(CAS)
AtomicInteger类主要利用CAS(compareandswap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。(不会像锁一样多线程效率比较低)
classTest{privatevolatileintcount=0;//若要线程安全执行执行count++,需要加锁publicsynchronizedvoidincrement(){count++;}publicintgetCount(){returncount;}}②多线程环境使用原子类保证线程安全(基本数据类型)
classTest2{privateAtomicIntegercount=newAtomicInteger(0);publicvoidincrement(){count.incrementAndGet();}//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。publicintgetCount(){returncount.get();}}
基本类型(用于对基本类型包装为原子性)
使用原子的方式更新基本类型
数组类型(用于对数组包装为原子性)
使用原子的方式更新数组里的某个元素
引用类型(用于对引用类型包装为原子性)
importjava.util.concurrent.atomic.AtomicReference;publicclassAtomicReferenceTest{publicstaticvoidmain(String[]args){AtomicReference
对象的属性修改类型
即是狸猫换太子的问题,是因为没有使用AtomicStampedReference带有版本号的
这是CAS在多线程中遇到的,12是两个线程,本身来说我认为也可以用synchronize解决
A不知道B已经对a进行变为3再变为1的操作,A线程只看到了现在a的值还为1.就以为没变
可以用原子引用解决ABA问题(原子引用采用了乐观锁机制,也有版本号)
用于解决ABA问题,对应思想是:乐观锁
原子引用是代版本号的原子操作,这个跟乐观锁就是一样的了,也就是每次对使用了原子引用的这个变量进行操作的时候版本号都会+1,每次比较的时候是比较版本号了,不只是变量值
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
这样别人拿到你第三次修改的数字之后,就算是CSA比较了期望值和现在的值是一样的,但是版本号不一样,也不会执行
在javaguide-》并发编程-》进阶篇
AQS是各种lock锁实现的原理
所谓的公平锁就是先等待的线程先获得锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
我举个例子给他家通俗
NonfairSync非公平的意思就是管你三七二十一,我先尝试给共享资源加锁,如果加锁成功就阻塞其他线程(因为其他线程都在队列中排队,这个时候就特别的霸道而显得不公平),如果是共享资源上已经被加锁了,这个时候还要再判断下能不能加锁,两次尝试加锁都失败再霸道也没用了,就只能老老实实去队列尾部排队!还是去超市购物后买单,只有一个收银台,这个收银台也只能服务一个顾客。当买单的人特别多,大家都排着队等着。这个时候来了个壮汉,仗着自己高大枉顾排队的游戏规则,直接跑到收银台看有没有人正在买单,如果没有人正在买单就直接插队买单。如果看了两眼还是有人正在买单,那就规规矩矩到队尾排队。但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。
“可重入锁”指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁
拿到外面门的锁,就可以拿到里面门的锁,这是自动获得的
【注意】这里由于在对象内创建了一个lock锁,所以两个方法使用的lock都是同一把,所以在第一个lock中调用第二个lock是可以获取到的,因为两个lock是同一把,所以是可重入的
锁必须配对,加锁几次就要解锁几次
这个是AtomicInteger类里面的函数,这里面就用到了自旋锁
这个函数的意义,只要对比var2的值和var5的值不一样,就说明有人改了,我们不能用这个值(跟乐观锁差不多),因此我们需要一直循环到没有人改他,知道我们拿到的值和他本身里面的值一样了,那么就可以进行操作
不断尝试,直到成功为止
这是自己写的自旋锁
首先T1获得锁,并吧atomicreference设置为thread
这时候T2也进入lock,但是由于此时不满足条件会一直while循环
T1解锁之后,条件就发生变化,所以T2就拿到锁,之后就会解锁
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
如何预防死锁?破坏死锁的产生的必要条件即可:
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态指的是系统能够按照某种进程推进顺序(P1、P2、P3.....Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称
也就是所有线程在分配时都采用同一个资源分配顺序
如:线程1是resource1、resource2那么线程2也应该resource1、resource2
什么是死锁
1.可以使用javabin文件下的jps命令来查看哪些进程在运行,由于我们在环境变量添加了JAVA_HOME/bin,所以是可以再系统中直接使用jps.exe
jps-l#查看当前运行进程号2.jstack查看某个进程是否死锁
jstack进程号#查看某个进程的堆栈信息,并给出是否死锁
3.处理jstack查看堆栈信息,也可以看日志
1.在lambda表达式中,是没办法拿到for循环的值的,但是可以使用final关键字,中间变量,来拿到,因为lambda表达式中只能拿到final型的,因为本身lambda表达式就是一个new对象
2.volatile保证可见性和指令不可重排
3.java虚拟机中有两个线程是默认执行的,main和gc
字符串常量池存放在永久代。JDK1.7字符串常量池和静态变量从永久代移动了Java堆中。
也就是说主内存就是堆和方法区,我之前看到的线程更改值就是因为那个num变量是static的,被放在方法区了,所以可以被别的线程读取到,读取到本地内存可能就是本地方法栈里面
volatile关键字除了防止JVM的指令重排,还有一个重要的作用就是保证变量的可见性。:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile关键字可以保证共享变量的可见性。(就算另一个线程在使用这个变量,也能够立马看到这个变量被别的线程修改后的值)
没有volatile需要等线程执行完之后,才会把值给主内存,有了它之后改了就能给主内存
每一个线程里面都有一个ThreadLocalMap
//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMapthreadLocals=null;当你使用ThreadLocal对象的set()方法。那么你的ThreadLocalMap里面就会被添加
publicvoidset(Tvalue){//注意这里new的时候是传入的副本变量,这里set也是直接更改副本的,而不是原始的Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);//threadLocals最初线程创建的对象,和当前线程设置的值elsecreateMap(t,value);}ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}publicTget(){//直接获取我们最开始创建的默认值Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}returnsetInitialValue();//也就是说你只使用的get方法,那么就会给你复制一个副本}你可以在你的线程ThreadLocalMap中有很多threadLocals对象,每一个threadLocals对象都代表从主内存复制的一个变量
例如下面这个东西,你就可以在线程中set3个不同的对象了
publicclassThreadLocalExampleimplementsRunnable{//SimpleDateFormat不是线程安全的,所以每个线程都要有自己独立的副本privatestaticfinalThreadLocal
publicclassDog{privatestaticvolatileDogsingle;//privateDog(){//这里的构造器必须写成private,}publicstaticDoggetSingle(){if(simgle==null){Synchronized(Dog.class){if(simgle==null){single=newDog();}}}returnsingle;}}