一)对于synchronized的锁策略:
synchronzed是一个自适应的锁,应该根据具体情况来决定选取那种锁策略;
1)synchronized既是一个乐观锁又是一个悲观锁,一开始是一个乐观锁,但是如果发现锁冲突的概率比较高,就会自动转化成悲观锁;
2)synchronized不是一个读写锁,他是一个普通互斥锁3)synchronized既是一个重量级锁,又是一个轻量级锁,它一开始是一个轻量级锁,如果发现锁持有的时间比较长,锁冲突的概率比较高,就会升级成重量级锁,根据锁竞争的激烈程度来进行自适应;
3)synchronized是一个非公平锁,是一个可重入锁;
4)synchronized是一个轻量级锁的时候,大概率是一个自旋锁,当他是重量级锁的时候,大概率是一个挂起等待锁5)偏向锁就是再赌这个锁不会发生竞争,本质上是一个延时加锁
锁膨胀/锁升级:这就体现了synchronized自适应的能力
无锁-->偏向锁--->自旋锁---->重量级锁
1.for(....){ synchronized(lock){ n++ } }//锁的粒度比较细 2.synchronized(lock){ for(....){ n++ } }//锁的粒度比较粗
1)如果说锁的粒度比较细,那么多个线程的之间的隔离性越低,并发性就会更高,多次加锁解锁开销就很大;
2)如果说锁的粒度比较粗,那么加锁解锁的开销就会更小,因为某一个线程持有锁的时间比较长,PCB的调度不是太频繁;
3)因为上面的锁粒度比较细的代码加锁解锁很多次,每一次开销就特别大,会把多次加锁解锁操作合并成一次;
function() { synchronized(this){ 任务1 } synchronized(this){ 任务2 } synchronized(this){ 任务3 } } 这样写还不如把三个任务直接加到一个锁中呢,这样效率比较高 function() { synchronized(this) { 任务一(); 任务二(); 任务三(): } } 1)因为我们的编译器会有一个优化,就会自动判定说,如果说某一个地方的代码锁的粒度太细了 那么就会进行粗化,如果说我们的两次加锁之间的代码中间间隔比较大,就不会出现这种优化 2)如果说我们的加锁之间的间隔比较小,中间隔的代码比较少,就很有可能会触发这个优化 就会把多次加锁解锁操作合并成一次,这样就会减少加锁解锁的次数 3)虽然并发性降低了,但是编译器认为这样的代码优化速度会提升很多
二)CAS机制和ABA问题:
2.1)CAS机制:Compare and swap,叫做比较和替换,但是这里面的swap可以理解成赋值
2.2)比较:拿着寄存器或者某一个内存的值和另一个内存的值进行比较,如果值相同了,就把另一个寄存器的值或内存的值,和当前的这个内存进行交换
2.3)交换:交换比较的这俩东西
2.4)关键来说,比较和交换的这个过程是原子的,CPU提供了一组有关于CAS相关的指令,通过使用这样的一条指令,就可以完成上面的比较与交换过程2.5)假设内存中的原数据是V,旧的预期值是A,需要修改的新值是B
1)判断A与V是否相同(比较)
2)如果比较相同,就把B写入V(交换)
3)返回操作是否成功
1)使用一个期望值来和当前变量的值进行比较,如果当前的变量值与期望的值相等,就用一个新的值来更新当前变量的值
2)CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,这是一个条件,将内存值V修改为B并返回true,否则条件不符合返回false;3)内存的值和旧的预期值不相同,说明该变量已经被其它线程更新了,多个线程访问相同的数据时,如果使用锁来进行并发控制,当某一个线程(T1)抢占到锁之后,那么其他线程再尝试去抢占锁时就会被阻塞,当T1释放锁之后,下一个线程(T2)再抢占到锁后并且重新恢复到原来的状态线程从阻塞到重新准备运行有很长的时间开销,而假设业务代码本身并不具备很复杂的操作,并发量也不高,那么当我们使用CAS机制来代替加锁操作;
4)当多个线程操作同一变量时每个线程会首先会读取到地址值对应的原值作为自己的期望值,然后进行操作,操作完以后在更新的时候他会判断现在地址值对应的值是否与自己的期望值相同,如果相同,就认为自己操作的过程中别人没有进行过操作,则将自己操作后的值更新到地址值对应的位置,如果不同则说明自己操作的过程中一定有其他的线程更新过数据,然后会把当前地址值对应的值返回给用户,用户拿到新值重新上次的操作,不断循环直到更新成功;
5)当多个线程同时对某个资源进行CAS操作时,只有一个线程可以修改成功,但是并不会阻塞其他线程,其他线程只会受到操作失败的信号,可以看待成乐观锁的一种实现方式,不会涉及到操作系统的锁;
boolean CAS(address,expectvalue,swapvalue){ if(&address==exceptedvalue){ &adress==swapvalue; return true; } return false; }
1)在上面的这个代码中,address是待比较内存地址,excpectvalue是预期内存里面的值(旧值),swap是希望把内存中的值改成的新值;
2)&address是取出内存里面的值,看看内存中取出的值是否与旧的值是否相等,如果相等,就将内存中的值就改成新的值,如果不是旧值,我们就什么也不做,此处我们说的比较和交换,比较我们也可以认为是赋值操作;
3)返回true表示交换成功(咱们内存中的值和旧值是相等的),返回false表示交换失败(咱们内存中的值和旧值是不相同的,说明在中间这个过程中,有人把这个内存中的值进行了修改,这一次CAS就失败了);
4)我们此处指的CAS是指,CPU提供了一个单独的CAS指令,通过这一条指令,就可以完成上述代码描述的一个过程,上面的伪代码,显然是线程不安全的,既有读,又有写,况且写和读还不是原子的,如果上述的过程都是通过一条指令来进行完成的,这就是一定原子的,因为我们的指令就是不可分割的最小单元;
5)CPU上面执行的指令就是一条一条执行的,指令已经是不可分割的最小单位,这就可以保证线程安全;
6)咱们的CAS最大的意义,就是说让写这种多线程安全的代码,提供了一个新的思路和方向,像咱们的刚才这一段比较和交换逻辑,这就相当于是用硬件直接进行实现出来了,通过这一条指令,封装好直接进行使用了;其实我们在这里面所说的CAS,就是通过硬件,通过CPU把这些一串逻辑,一串指令,封装成一个原子的指令;
总结:取出当前内存里面的值和我们的另一个旧值进行相互比较,如果说相等,那么就把当前内存里面的值换成新的值,并且返回true,就认为操作成功,否则就操作失败
但是如果说多增加一些线程,是不是效率就会更高呢?
一般来说是会,但是也不一定,因为如果说线程多了,那么这些线程可能会竞争同一份资源,那么此时整体的速度就收到了限制,因为咱们的整体的硬件资源是有限制的,已经达到瓶颈了,CPU,磁盘,网络带宽,内存
保证线程安全:通过CAS实现原子类
1)Java标准库里面提供了一些原子类,针对常用的int,long,int array进行了封装,可以基于CAS来进行封装,并且线程安全;
2)例如用违代码(自己想象的代码)实现多线程同时修改一个值,不用加锁操作保证原子性;
例如我想把count的值从零加到2,运用两个线程,两个线程分别进行+1操作,如何解决并发执行线程安全问题呢(在不加锁的情况下);
3)CAS此时既可以高效地完成自增操作,又可以不用加锁,保证线程安全;
4)像CAS这样的操作,并不会触发阻塞等待,但是在循环里面会狂吃CPU资源,就像之前咱们说的定时器的那一个循环一样,也是在狂吃CPU资源
通过CAS就可以实现一些原子类,这些原子类也相当于是一个整数,也可以进行加加或减减操作,都是可以通过不需要加锁就可以实现线程安全的; class AtomicInteger { private int value;//存储原始的数据 public int getAndIncrement() { int oldvalue=value; //A过程,把数据从内存读到CPU寄存器里面 //B过程,接下来我们再来进行判断一下当前内存里面的值是否和刚才寄存器里面的值是否一致,如果判定成功,我们就把value设置成value+1,此时返回true //如果说判定失败,那么就直接返回false while(CAS(value,oldvalue,oldvalue+1)!=true) { //重新把我们的内存里面的值读到我们的CPU寄存器上面 oldvalue=value; } return oldvalue; } boolean CAS(address,expectvalue,swapvalue){ if(&address==exceptedvalue){ &adress==swapvalue; return true; } return false; } }
上述代码执行过程:
1)value就是存储原始的数据,表示的是实实刻刻表示的是内存里面的值
2)在执行getAndIncrement操作的时候,我们让oldvalue=value
3)此时为代码中看到的oldvalue变量是用int来进行表示的,但是在实际实现的过程中可能是用一个寄存器来进行存储的,因为伪代码中不好用来表示寄存器,这个赋值操作就相当于是把数据从内存中读取到我们的CPU寄存器里面;
3)然后再进入while循环,我们来进行判断一下,当前内存中的值是否和刚才我们CPU寄存器里面的值是否是一致的,如果我们判定成功,就把内存中的值设置成oldvalue+1,返回true,循环结束;
4)如果我们判定失败,咱们的CAS操作就返回false,继续下一次循环
(再次把我们的内存中的值读到CPU寄存器里面,然后再来一次CAS操作)
一旦发现这里的内存中的值被修改了,那么就可以重新把内存中的值读到CPU寄存器里面
6)所以我们在写多线程代码的时候,任意两个代码之间,都有可能被其他线程插入一些其他的逻辑
7)即使其他线程修改导致CPU寄存器的值和内存中的值不相等了,也没事,在CAS里面就可以发现这个值是否被改了,我们的CPU就会重新的把内存的值读到寄存器里面,尝试在CAS(原子操作,即完成了等待,又完成了赋值)里面进行交换操作
Java标准库中的原子类:基于CAS实现,不会触发阻塞等待,不存在线程安全问题
1)这是基于CAS实现的++操作,这里面就可以保证了即可以实现线程安全,又可以比synchronized高效
2)CAS不会涉及到线程阻塞等待
咱们的线程1进行++操作之后,因为我们的线程2已经完成了load操作,接下来就应该执行CAS操作了,但是我们此时发现此时线程2CPU寄存器里面的值和我们的内存中的值不相等,所以此次CAS操作失败,直接返回false
因为此次CAS操作失败,那么我们的线程2就又会重新进入到循环,就又会执行load操作,再进行比较我们此时的内存中的值和我们CPU寄存器的值相等,那么就会把内存中的值变成2,这是因为整个CAS就是一个原子性操作的指令
1)在上面的这个代码实现中,整个代码的目的是实现++操作,但是CAS是进行了比较+1和交换这三个动作,如果说比较失败,没有必要进行++操作
2)如果想一下,咱们的CAS不是一个原子性的操作,如果说两个线程同时执行load操作和CAS操作,那么就会使数据出现覆盖,但是此时恰好CAS是一个原子性操作的指令,只有一个线程先进行执行CAS指令之后,后续的那一个线程才可以执行CAS这样的指令
AtomicInteger atomic=new AtomicInteger(0); Thread t1=new Thread(()->{ for(int i=0;i<50000;i++) { atomic.getAndDecrement(); } }); Thread t2=new Thread(()->{ for(int i=0;i<50000;i++) { atomic.getAndDecrement(); } }); t1.start(); t2.start(); t1.join(); t2.join(); //通过get方法得到原子类中的内部的值 System.out.println(atomic.get()); }
AtomicInteger num=new AtomicInteger(0); num.getAndIncrement();//相当于num++ num.getAndIncrement();//相当于++num num.getAndAdd();//+=10 System.out.println(num.get()); 内部通过CAS实现的
1)在上面的这个代码里面其实不存在线程安全问题,这是基于CAS实现的++操作,因为这里面的代码既可以保证线程安全,有是比synchronized要高效很多,因为这里面的synchronized会涉及到锁的竞争,两个线程之间要进行相互等待;
2)但是咱们此时的CAS操作不会涉及到线程阻塞等待,相当于是两个线程全功率向下跑不会涉及到线程等待和调度;
基于CAS实现自旋锁:先通过CAS来进行判断当前锁是否是被当前哪一个线程所持有
public class Locker{//这是一个类表示自旋锁 public Thread owner=null;//表示占用这把锁的线程是哪一个,也就是说记录一下当前的这一把锁被哪一个线程所持有,为空就表示当前这把锁没有被任何线程所占有 public void lock() { while(!CAS(this.owner,null,Thread.currentThread()) { } } public void unlock() { this.owner=null; } 只要有其他线程占用锁,this.owner就是空 }
如果说这个锁是被其他的线程所持有,那么就会一直进行自旋阻塞等待
如果说这个锁没有被其他线程所持有,那么就会把当前的owner设置成当前尝试加锁的线程
如果说这里面的owner不是null,那么这里面的CAS操作就会一直失败,只有说当这把锁没有被占用,那么就会立即设置成当前线程
1)总结:和刚才的原子类类似,咱们基于CAS实现自旋锁,也是通过一个循环来进行实现的,会在循环里面不断地进行调用CAS操作,CAS会进行比较当前的owner值是否为空,如果是空那么就会把owner改成当前线程,意思就是当前线程拿到了锁,如果说不是null,那么就会返回false,进入到下次循环,下次循环仍然是CAS操作
2)如果说当前这把锁一直被其他线程所持有,那么就会导致当前尝试进行加锁的线程就会快速在while的这个地方进行反复循环,就是在心里面想着,只要这把锁被别的线程释放,就可以立即获取到锁,这就是忙等;
3)咱们当前的这个自旋锁就是一个乐观锁,是一个轻量级锁,就是假设当前的这个场景下,锁冲突并不会很激烈,乐观锁就是说当前这把锁我们虽然没有立即拿到,但是我预期就很快会拿到,乐观锁认为当前锁冲突并不是很激烈,我可以经过短暂的自旋几次浪费点CPU,问题都不大,因为我就可以很快地拿到这把锁;好处就是说这里面的锁一进行释放,我们就可以立即拿到锁;
1)如果说咱们的滑稽老铁比较乐观,已经洞察到了这个女生的感情即将出现危机(马上这个锁就要被其他线程释放了),和我一起竞争这个女生的哥们很少(所冲突的概率比较低),短时间付出这些成本也是值得的,频繁进行尝试也是无所谓的;
2)但是如果说,女神和他男朋友的关系很好(线程持有锁的时间比较长),或者说和同一批竞争这个女神的人有很多(所冲突的概率比较高),显然我们再去频繁的去自旋就不合适了,此时也会浪费我大量的时间和精力(狂吃CPU没效果),此时我们使用自旋锁就没有什么必要了
自旋锁适用于锁的冲突的概率不高,某一个线程持有锁的时间不会太长,不然无限的循环会狂吃CPU资源
如何理解CAS中的经典ABA问题:
定义:假设当前内存中的值就是旧值,我们就认为当前内存中的值没有发生过改变,也就是说没有别的线程来进行修改,于是我们就开始执行交换操作,但是这一种设定并不是准确的,可能出现一种极端情况,本来旧值是A,被其他线程改成了B,又被改成了A,不是这个值没有变,而是变了一圈又变回来
1)理解:咱们CAS的关键是先进行比较,再进行交换,比较本质上是比较当前值和旧值是否相同,如果这两个值相同,就视为是认为在中间过程中没有发生过改变,但是在我们的结论中存在漏洞,并不严谨,当前值和旧值相同可能中间确实没有改变过,也有可能也有可能变了,又变回来了,这样的漏洞,在大多数情况下没什么影响,但是极端情况下会引起BUG;
2)所以说我们就草率地认为这两个值相同就没有发生过改变,这是错误的;
3)就是说本来这个旧的值是A,当前值也是A,结果我们不知道当前的值的这个A,是一直都是A,还是从A变回了B,最终又变回了A;
4)咱们的ABA问题就是类似于说我当前拿到的这个手机,我们无法进行区分出他是一个新的手机,还是一个翻新机
新的手机:就是说从出厂到现在没有被使用过
翻新机:就是说出厂之后已经卖给别人了,但是已经被别人用了一段时间之后,旧了,又被奸商回收回来,换了个壳子,当作新的机器来进行卖了,所以说在大部分情况下,去不去分他当前变过,所以说意义是不大的,对代码没有影响,但是我们的ABA问题在转账过程中就会出现BUG
假设滑稽老铁的账户余额中有100块钱,想要在ATM机中取50块钱,当想要按下取款操作的时候,突然ATM机卡了一下,滑稽老铁多按了一下取款,于是此时就按下了两次取款操作,这就是说一次取钱操作,我们执行了两遍,两个线程并发的去执行这一个取钱操作
1)咱们的预期效果就是应该是只有一次我们可以取钱成功,虽然多按了一下
2)但是还是说期望拿到并取走50元钱,剩下的账户剩下50元钱
int oldvalue=value; if(CAS(&value,oldvalue,oldvalue-50)!=true){ oldvalue=value; }
如果基于CAS实现这里面的取款:现在有两个线程,线程一和线程二同时进行-50的操作;
第一步:线程一的寄存器和线程二的寄存器都读到了存款中的值是100(oldValue);
第二步:线程一的寄存器的值会与CPU中的值进行比较(发现在这个线程执行前没有其他线程来修改内存中的值),发现都是100,就将内存中的值减50;
第三步:这时线程二也想进行减50操作,但是发现当前线程二的值和当前CPU中的值不相等,就无法进行减50操作了;
第四步:但是如果这时候,再进行判断之前,突然有个人,给这个滑稽老铁转了50元钱,此时内存中的值就变成100了(就算变成101都没这事),此时我们的线程二又扣了50元钱,这就出现了BUG;
那如何解决这个问题呢,只有CPU寄存器里面的版本号比内存中的版本号高,才可以CPU执行的操作更新到内存里面,才可以进行更新操作;
1)此时就要引入版本号,为了解决ABA问题,无论此时如何对账户余额进行操作都会对版本号加一,如果当前的版本号和读到的版本号相同,就修改数据,就让版本号加1,如果当前的版本号高于读到的版本号,就认为操作失败;
2)那么此时:线程一的寄存器和线程二的寄存器都读到了100元钱,和版本号是1,线程1扣款成功内存中的钱变成50,版本号改成2,此时线程二还在阻塞等待中,这时突然有人给滑稽老铁转账50,虽然此时的钱数是100,但是版本号也进行加1操作,变成了3,此时线程二进行扣款操作之后修改版本号是2,但是此时的内存的版本号已经是3了,小于读到的内存中的版本号,就操作失败;
1)两个线程在同时执行load操作,将内存的值读到线程1的CPU的寄存器里面和线程2的CPU寄存器里面;
2)此时线程1会把CPU寄存器的值和内存中的值进行比较,发现都是100,那么进行CAS操作,把内存中的值减少50;
3)咱们线程2在线程1执行完成CAS操作之后,也会开始执行CAS操作,但是此时发现线程2的CPU寄存器里面的值和我们的内存中的值不一样,那么此时CAS操作就会直接返回false,线程2的CAS操作永远无法执行,因我们此时的代码没有使用循环,判定一次失败就直接结束了;
3)所以说按照上面的情况来进行分析,虽然我们一不小心按了两下提款机,有两个线程想同时执行取款操作,但是却执行一次CAS操作,也就是说钱只被扣了50元;
4)上述的操作的成功,是因为我们没有进行引入ABA问题
但是假设有t3的转账50的操作,就可能让线程t2的CAS操作出现误判,T2的寄存器里面已经读到了100这个旧值,然后t1扣款成功了,内存中的值从100变成50,然后我们的t3转账成功了,就从50变成了100,t2此时来进行判定,就是因为ABA问题产生误判了,导致t2也被扣了一笔钱
5)如果说我们多了一次朋友的转账操作,那么此时就会在我们执行CAS的时候,此时我们的线程2的CPU寄存器中得值就和我们内存中的值就是完全一样的,那么此时我们的CAS操作就是成功的,于是我们的线程2就认为好像内存中的值就好象从来没有改变过一样,于是就又开始执行转账操作了,再扣50元;6)其实我们这里面的100,是先从100-->50,又从50-->100,就被CAS草率地认为好像是从来没有进行改变过一样,此时最终的结果就是从提款机中连续取出了两次50元钱
在上面的过程中,我们出现了两次巧合:
1)一次取钱操作,不小心按了两下子取款机;
2)假设在取款的一瞬间,滑稽的朋友给他转了50元钱;
正是这两次巧合才最终导致了存在BUG我们的ABA问题,这是极端场景的问题;
我们的一个互联网产品每天进行接收的请求,我们要进行处理访问的用户量是特别大的,像之前搞出来的一个神马搜索+UC头条,用户每日的访问量可以达到3亿之多
解决方案就是说我们引入一个版本号,这个版本号只能变大,不能变小,修改变量的时候,比较的就不是变量本身了,而是我们比较的是版本号
1)在进行读操作的时候,把余额和初始情况下的版本号都给读到CPU的寄存器里面
2)此处我们就要求,每一次我们进行余额进行修改的时候,都让版本号进行+1操作
3)每一次进行修改的时候,也要进行对比版本看看当前的旧值和当前的值是否一致
4)当引入版本号之后,t2线程再去尝试进行这里面的比较版本操作,发现版本的旧值和当前的值并不会匹配,因此就进行修改,也就是说我们如果拿变量本身来进行修改,那么因为变量的值有加有减,此时就是比较容易的出现ABA问题,如果说是直接拿版本号来进行判定,这个时候要求版本号只能增加,这个时候就不可能出现变了又变回来,就不会出现ABA问题
5)这里面不一定非要用版本号,也可以用时间戳,只要是朝着一个方向来进行变化就可以,只要发现只被改过就可以了
三)进程和锁的总结:
让一个进程进行正常工作,就需要给这个进程分配一定的系统资源
1)内存:加在exe文件到内存里面,花费一些内存资源来进行完成加载的过程
2)磁盘:打开并且操作一些文件
3)CPU:执行我们进程的一些指令
让一个进程进行正常的工作,那么操作系统必须在硬件上面给予足够的硬件资源方面的支持,才可以保障进程的基本工作,才能使进程完成一些重要的工作任务
进程的状态,进程的优先级,进程的记账信息,进程的上下文
2.1)咱们现在的操作系统,一般都是多任务操作系统,就是一个系统,同一时间内只能执行一个任务,它的前身就是单任务操作系统,同一时间只能运行一个进程,单任务操作系统比如说想要运行QQ,就不能运行别的进程;
2.2)单任务操作系统,不需要考虑进程调度
笔记本电脑就是一个多任务的操作系统,同一时刻有很多进程在运行,在任务管理器里面,同时运行着CSDN,Idea,这些任务在同时执行
线程调度:本质上是说有限的核心执行很多很多的任务,其实就是操作系统在想着如何把CPU资源给各个进程来进行分配
想要通过多进程,完全是可以实现并发编程的,但是也会有一定的问题:
1)如果需要进行频繁的创建进程,销毁进程,这个事情成本还是挺高的
2)如果想要频繁的进行线程调度,这个事情成本也是比较高的
创建进程就需要分配资源,CPU资源,内存资源,文件资源,销毁进程就需要释放资源,就必须要释放内存,释放文件,对于资源的申请和释放操作本身就是一个比较低效的操作,所以说创建进程,销毁进程是一个比较低效的操作
申请资源:假设我们此时要进行配置一台电脑,就去了电脑城,然后老板给我写了一个配置单,那么老板就得去这些仓库里面去找,就需要去仓库里面
释放资源:假设把这个电脑来进行退货,那么就需要把这个电脑拆了,然后放到对应的货架子位置上面,CPU放在CPU的货架子上面,各种零件也要放在对应的位置
进程之间的独立性:
进程的调度,本质上来说就是操作系统在进行考虑CPU资源如何给各个进程来进行分配,但是除了CPU资源,还有内存资源,那么内存资源又是如何来进行管理的呢?
1)这就不得不谈到虚拟地址空间,由于在操作系统上面,同时运行着很多的进程,如果某一个进程出现BUG,进程崩溃了,那么是否会影响到其他进程呢?现代的操作系统是不会的windows,mac,能够做到这一点,就是依据了虚拟地址空间
2)在早期的操作系统里面,所有进程都是访问同一个内存里面的虚拟地址空间,如果说某一个进程出现了BUG,那么会把某一个内存的数据写错了,那么就可能会引起其他进程的崩溃
运行态:进程占用CPU,并在CPU上运行;
就绪态:进程已经具备运行条件,但是CPU还没有分配过来;
阻塞态:进程因等待某件事发生而暂时不能运行;在这里面的解决方案就是说把这个院子划分出很多的道路,这些道路之间彼此分割开来,每一个人走自己的道路,这个时候就没事了,也就是说每一个人都有着自己的路,一个人阳了,每一个人都不会影响其他人,虽然楼里面有很多人,但是同一时刻只有6个人出门,所以当一个人走完之后,我们就进行消杀一遍;
1)把进程按照虚拟地址空间的方式划分成了很多份,这个时候每一份不久只剩下一点了吗?虽然我们的操作系统里面有百八十个进程,但是从微观上来看,同一时刻,同时执行的进程就只有六个,这是我们把内存分成六份就可以了,每一个进程能够被分配的内存还是挺多的,也不是所有的进程都使用那么多的内存,比如说一个王者荣耀的游戏,要分给他的内存还是很多的,但是大部分的进程也就是说只占几M就可以了,如果这六个进程中有一个进程A退出CPU了,那么就把他对应的那份内存释放掉,再让这份内存给其他进程提供服务,各个进程之间不会相互影响,就算一个进程把自己的那一分内存写坏了,那一分内存是自己的,也不会影响到别人
2)进程和进程之间为了保证系统的稳定性,是彼此隔离开的,每一个进程有各自不同的虚拟地址空间,每一个进程用各自的内存,互不干扰,互不影响,就可以保证基本的稳定性,但是在实际工作当中,进程之间还是需要进行交互的
进程之间如何进行通信?
一方面,确实要保证进程之间的独立性,保证进程的隔离,保证系统的稳定,保证一个进程出BUG之后不会影响到其他进程,但是从另一个角度来看,完成隔离也不行呀,我们还是需要进行间接交互的,我们既要完成隔离,又要进行交互
类似的,两个进程之间,进程A可以把数据放到公共空间里面,进程B再取走这个数据,这样的话就完成了两个进程之间的交互,同时我们也是可以保证原来的隔离性不会受到影响,稳定性并不会破坏,这就叫做进程间通信;
如果说想创建进程就要分配资源,内存和文件,销毁进程就要释放资源,内存和文件;
调度进程成本也是很高的,咱们的进程重量重量在资源申请释放,线程是包含在进程里面的,一个进程中的多个线程,是共用同一份资源(同一份内存+文件)
我们只是在创建第一个线程的时候(由于要分配资源),成本是比较高的,后续这个进程再去创建其他线程,成本会更低;
1)进程池:使用完这个进程之后,并不会销毁进程,而是把这个进程放到池子里面,下次还想使用进程,直接从池子里面拿就可以了,跳过了申请内存,释放内存的过程,整体的开销就大大降低了,但是消耗资源太多了,池子里面的闲置进程还是要消耗系统资源,也是很多的,这些进程都在内存里面堆着呢,会消耗大量资源,其他程序运用的资源就变少了
2)使用线程来实现并发编程,因为线程比进程更清量,每一个进程可以执行一段任务,每一个线程可以执行一段任务,也就是一段代码,也能够并发编程,创建线程比创建进程的成本要低很多,销毁线程的成本也比销毁进程的成本要低很多,调度线程比调度进程的成本要低很多,在linux中,也把线程称之为轻量级进程
一)进程的虚拟地址,一个进程要想运行,就要给他分配一些系统资源,其中内存就是最核心的一个资源,并不是分配了真实的物理地址,而是虚拟地址;
传统的进行分配,直接依据真实的内存地址划分成空间,分配给每个进程;物理地址:真实的内存的地址
在我们的原始的操作系统中,我们直接依据真实的内存地址划分出空间,分配给每个进程 比如说0x100-0x800分配给当前的进程1 0x900-0xc00分配给进程2
进程一:0x100-0x400
进程二0x500-0x700
下面是另外一种情况:
1)进程一:0x100-0x800
2)进程二:0x100-0x800
3)进程三:0x100-0x800
这三个地址,都是操作系统抽象出来的虚拟地址,通过中间的一个类似于哈希表的映射,映射到一块真实的内存,系统就会把这个虚拟地址转换成真实的物理地址;
这就相当于是给java100班进行编号,1-50号,再给java101班进行编号1-30号,都是1号,但属于不同的班级,属于不同的人;
为啥要搞一个虚拟地址空间呢?
为了一定程度上减少内存访问越界带来的后果,进程1的访问范围是0x100-0x400,如果尝试访问0x401,就访问错误,如果此时你想要访问0x401,系统就会查询这个页表,找到这个虚拟地址对应的物理地址,由于此时的0x401已经是非法地址了,页表就无法查询到,系统就感知你这是越界访问,会给进程发送一个信号,让当前这个线程进行崩溃,防止影响到其他的进程,就让进程与进程之间相互影响的可能性减少了,隔离性增加了,进程就会更加稳定了虽然比较低效,假设访问的是真实的物理地址,访问0x401,但是有可能会真的访问成功,但是这一块内存空间并不是当前进程的分配地址呀
PCB---->PCB---->PCB------>PCB--------->PCB PID=1 PID=2 PID=3 PID=4 PID=1
1)进程一创建出了一个PCB但是在这个进程中创建一个线程,也是在加了一个PCB;
2)图中的第一个PCB和第五个PCB是属于同一个进程,这俩PCB就可以认为是当前的进程中包含了两个线程,这两个线程就共用了同一份系统资源,尤其是内存资源;
3)其实在操作系统的角度来说,是不会进行区分线程和进程的,只区分PCB,只认PCB创建一个进程的时候,就是创建出一个一个PCB过来,PCB也可以被认为是当前进程中同时这个已经包含了一个线程了;也就是说,一个进程中至少包含一个线程,后期再创建线程,创建PCB,它们的PID是相同的;
4)从上面的图中可以看到,属于同一个进程的线程之间,是可以共用一份内存空间的,同时对于其他的进程PCB是使用独立的内存空间,一个线程就是代码中的一个执行流(按照顺序,来执行一定的指令)
四)复习synchronized:
1)这就是类似于说ATM机上面有一把锁,同一时刻,如果说人们之间不相互认识,那么通过这把锁就进行限制了说就限制了说一次只能有一个人来进来取钱,我们通过这样的锁,就可以来进行避免上述这种乱序排序执行的情况
2)特点:互斥的,同一时刻只有一个线程可以获取到锁,如果其他的线程也尝试获取到锁,就会发生阻塞等待,一直阻塞到刚才的线程释放锁,剩下的线程再尝试竞争锁
1)当一个线程加锁成功的时候,其他线程尝试加锁,就会进入到阻塞等待状态,此时对应的线程,就处于BLOCK状态,阻塞会一直持续到占用锁的线程把锁释放为止
2)咱们的加锁操作,就是把若干个不是原子的操作封装成一个原子的操作
3)同步:在多线程中,线程安全中,同步,本质上是指定的是互斥,只有一个人可以获取成功,其他竞争同类资源的只能失败
4)但是在IO或者是网络编程里面,同步相对的词叫做异步,此时同步和异步没有任何关系了,和线程也没有关系了,它表示的是消息的发送方是怎么获取到结果的
5)synchronized本质上来说是修改了对应对象的对象头里面的一个标记
class Counter{ public int count; } Counter counter=new Counter();
1)对应的线程进入到increase之前会尝试加锁,直到我们对应的线程执行完成increase方法执行完后才会执行解锁
2)但是在编程中如果出现了一个线程获取到锁了,出问题了,自己不干活,还影响到其他线程获取到锁,那么该怎么办呢?后续会说的
3)使用synchronized关键字,其实本质上就是在针对具体的某一个对象在进行加锁,正常情况下在创建的对象中会包含一个具体的字段,表示该对象的加锁状态,可以想象到是一个BOOLEAN类型,针对这个对象未加锁就显示为false,针对这个对象进行加锁就显示为true
4)在数据结构中,加锁的有 Stringbuffer,vector,Hashtable
一)加入到普通方法前,相当于是针对this来进行加锁;
1)synchronized加到普通方法前,表示锁this(当前实例对象),就是设置this也就是普通对象的对象头的标记位,如果两个线程同时的并发去调用这个synchronized修饰的方法,尝试针对同一个对象来进行加锁的时候,本质上是修改了Object对象中的“对象头的”里面的一个标记,假设一开始默认情况下是false,线程1已经把这个对象的标志位设置成了true,此时线程2也尝试来进行获取到这把锁,但是此时因为标志为不是false,所以说我们就不能进行修改标志位,必须要等到标志位变成false的时候,或者说等到线程1出了synchronized修饰的方法了也就是将我们对象对应的标志位改成false之后,我们的线程2才可以进行获取到锁
2)进入到synchronized修饰的方法,就相当于是加锁;出了synchronized修饰的方法,就相当于是解锁;它的功能就相当于把并发变成串行,两个线程针对不同的对象来进行加锁,就不会产生竞争
3)适当的牺牲速度,换来的是结果的准确性;
4)如果此时是加锁的状态,其他的线程就会无法执行这里面的逻辑,就只能进行阻塞等待;
这就相当于是ATM机取钱,同一时刻只能有一个人进入到ATM机取钱,别的人想要进来,并且获取到锁,是会阻塞等待的;
//我们是针对当前this来进行加锁 public synchronized void run() { System.out.println("我叫做run方法"); }
1)进入到Synchronized修饰的方法或者同步代码块==将对象头的标志位从false改成true
2)退出synchronized修饰的方法或者同步代码快==将对象头的标志位从true修改成false
3)当我们的两个线程尝试针对同一个对象加锁的时候才会产生竞争,如果是两个线程针对不同的对象进行加锁,那么就不会产生竞争
4)这就类似于说两个人抢1个ATM机,会产生竞争,但是如果说两个线程同时抢两个不同的ATM机,那么就不会产生竞争
2)加到某个静态方法前表示锁当前类的类对象,对类对象进行加锁
就算我们把synchronized加到静态方法上面,静态方法上面是没有this的,因为和实例不相关,也就是说谈不上this,所谓的静态方法更应该叫做类方法,咱们的普通的方法,更严谨的叫法,就应该叫做实例方法
2.1)反射是面向对象的一个基本特性,和封装,继承,多态是并列关系;反射也叫做自省,是指一个对象可以认清自己,这个对象里面包含哪些属性,每个属性叫啥名字,是什么类型,包含哪些方法,每个方法叫啥名字,参数列表是什么,所以说反射机制就是.class文件赋予的力量
2.2)这些信息来自.class文件,它咱们在进行运行程序的时候,是.java文件编译生成的字节码文件,会在JVM运行的过程中加载到内存里面,就会通过类对象来描述这个.class文件的内容和一切信息,类名.class就得到了这个类对象,每个类的类对象都是单例的
2.3)由于类对象是单例的,多个线程并发调用被synchronized修饰的同步代码快况且是给类对象进行加锁或者是使用synchronized修饰时的静态方法,一定会触发锁竞争
package com; class Counter{ public static int count=0; public synchronized void increase(){ //这是针对当前Counter对象来进行加锁 count++; } } public class Solution { private static Counter counter=new Counter(); public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(()->{ for(int i=0;i<10;i++){ counter.increase(); } }); Thread t2=new Thread(()->{ for(int i=0;i<10;i++){ counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
3)如果说针对某一个代码块进行加锁,就需要手动进行指定,锁对象是什么,synchronized后面要加一个括号,表示进行加锁的对象,针对哪一个对象来进行加锁,咱们JAVA中的任何对象,都可以作为锁对象
3.1)进入到synchronized修饰的同步代码快,相当于是加锁==给对象头的标志位设置从false设置成true;
3.2)出了到synchronized修饰的同步代码快,相当于是解锁==给对象头的标志位设置从true设置成了false;
public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(()->{ for(int i=0;i<10;i++){ synchronized(Object.class){ count++; } } }); Thread t2=new Thread(()->{ for(int i=0;i<10;i++){ synchronized(Object.class){ count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }
1)是如果括号里面的内容类型不同,也就是说对应到两个类对象(这时有两把锁
就不会出现竞争)
2)但是如果两个对象是相同的类,就是一把锁,就会发生竞争,他们必须竞争同一把锁
1)如果两个线程,尝试针对同一个锁对象进行加锁,此时一个线程会先获取到锁,另一个线程就会阻塞等待;
2)如果两个线程,尝试对不同的锁对象进行加锁,此时两个线程都可以获取到锁,互不冲突;3)synchronized会进行记录是当前哪一个线程所持有的锁,当线程一获取到这个锁的时候,在进入到代码块,此时就知道线程一已经获取到了这个锁,就不会进行阻塞了;