ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

并发编程(二)—synchronized和volatile

并发编程(二)—synchronized和volatile synchronized保证读和写的安全volatile保证读的安全但不保证写的安全在多处理器开发中保证了共享变量的“可见性”。可见性当一个线程 修改一个共享变量时另外一个线程能读到这个修改的值。一、高速缓存1.1 高速缓存如ALU在对数据进行for循环操作时如果在计算完成后立刻返回更新数据会出现一个运算核心在进行读写操作时一直占据总线的情况此时CPU多核处理器就失去意义所以在CPU内部设置高速缓存用于存储数据。ALU将数据存储在高速缓存中此时就释放了总线其他ALU就可以通过总线对其他数据进行操作。高速缓存的存储空间越大CPU与总线的交互就越少计算机的性能就越好。为了避免电压信号的干扰不同的指令不能同时传输即指令在总线必须排队传输内存同一时刻只能被一个指令指挥1.2 多级缓存在计算机的任务管理器中我们可以看到计算机的CPU中设置了三级缓存这是为什么多级缓存的目的也是为了加快传输速率提高每个核心的利用率。CPU从内存中读取数据一个导线同一时刻只能传输一个电压信号必须等该信号传输完成后才能继续传输下一个传输的过程对于CPU是浪费的。设置多级缓存后相当于设置了数据中转站数据到达第一个中转站后下一个数据便可发出以此进行传输提高了内存的数据发送频率与CPU的数据接收频率但是由于数据转发也需要消耗时间目前平衡性能最好的是三级缓存。二、synchronized和volatile2.1 高速缓存带来的问题通过对高速缓存的讲解我们可以看到高速缓存支持多核CPU且提高CPU的利用率但是它也带来了并发问题—相互覆盖。如上图若核心1对a进行10次的1操作计算完成后时间片到期此时核心2对a进行计算同样计算10次随后返回更新此时a10核心2任务完成后释放CPU核心1继续执行返回更新会覆盖核心2返回的数据即对a进行了20次操作但其计算得到结果为10。注高速缓存中的数据在返回更新时会将高速缓存中的该数据清空下次进行计算时需要重新从内存中读取数据。高速缓存存储容量满了之后会返回更新即更新是随机的不一定是任务执行完毕后才返回更新。2.2 volatile轻量级锁2.2.1 相关术语定义术语英文描述内存屏障memory barriers在内存中有一个变量标记该标记为某一确定值时不要继续访问。缓冲行cache line高速缓存的基本存储单元。原子操作atomic operations一系列操作全部成功或者全部失败如果一个操作失败其他操作还原。缓存命中读取数据时会先查看高速缓存中是否存在该数据有则直接读取缓存命中没有则到内存中读取。缓冲行计算机中具有存储能力的区域内存页、硬盘栈区、CPU内部—高速缓存 。在计算机中最小的物理单元是1字节一个存储单元操作系统给内存和硬盘划分的逻辑存储是4kB一个存储单元高速缓存中的存储单元是缓冲行64字节每一个缓冲行和外界相连的导线进行数据交互。每个缓冲行都有唯一的线路一个缓冲行可以存储多个任务的数据当一个任务对其进行操作时不同芯片有所不同可能加载全部数据也可能只加载一部分数据占据该缓冲行的线路相当于占据整个缓冲行其他任务不能进行操作。缓冲行填充操作系统指挥驱动驱动调动硬件为了指挥驱动操作系统内核也是C语言。计算机底层为C语言各种复杂的类型都会转成C语言的基本类型short、int、long、float、double、char所以缓存行中存储的都是1字节到8字节的数据若存储的数据多读取数据时就需要排队找到想读取得数据影响性能。若数据量较少可以将缓存行全部用一个任务的数据填充会大幅提升性能但是数据量较多时高速缓存中存储的数据过少就会影响性能。因此设计一个缓存行的大小为64字节存储的数据量不会过大数据拥堵排队也不会等待过长时间。非学术术语写命中CPU完成操作将数据返回到高速缓存中时若数据存在则写命中若返回前高速缓存已满数据更新回内存则没有写命中。写缺失数据返回到内存中时若数据被其他线程删除无法找到该数据则写缺失2.2.2 volatile2.2.2.1 volatile解释Java代码如下instance new Singleton(); // instance是volatile变量转变成汇编代码如下。0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp)Lock前缀的指令在多核处理器下会引发了两件事情。将当前处理器缓存行的数据写回到系统内存。这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。如a20返回更新后总线会记录其地址让CPU缓存中记录该地址的a10会失效但是对b无影响。2.2.2.2 volatile一定能保证准确性吗如a20返回到达返回队列中后时间片到期a10被选中返回更新此时处于返回队列中的a20并不会失效最终会造成数据覆盖因此volatile不能保证写的准确性。所谓的返回队列表实质上是顺序指令读到某个指令时对某块数据进行更新。即使数据无效页并不会影响到顺序指令它会继续执行返回无效数据。2.2.2.3volatile的两条实现原则1Lock前缀指令会引起处理器缓存回写到内存。2一个处理器的缓存回写到内存会导致其他处理器的缓存无效。volatile返回数据时会锁住缓存中其他地址相同的数据不让其进入顺序指令但是该步骤不是原子操作若刚进入顺序指令恰好时间片到期其他地址相同的数据执行进入顺序指令则无法阻止注一般是锁缓存如果锁住总线计算机所有任务都会停止执行代价太大。2.2.2.4 代码展示public class Test { public volatile int flag 0; }public class Test1 { public static void main(String[] args) throws Exception { Test x1 new Test(); Thread t1 new Thread() { public void run() { for(int i 0;i100000;i) { x1.flag; } } }; Thread t2 new Thread() { public void run() { for(int i 0;i100000;i) { x1.flag; } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(x1.flag); } }注意 跨线程操作时会认为变量前加了final,因此需要通过引用类型的对象来对flag进行操作不改变x1的指向。即flag1 final flag1final关键字final修饰的基本类型数据不能修改值final修饰的引用类型不能改变指向final修饰的方法不能被重写final修饰的类不能被继承final有一定的防止指令重排序的功能final通常和static一起用运行结果volatile只保证读到的是最新数据但是修改时可能背的线程会对该数据进行修改造成数据覆盖。2.2.2.5 保证volatile准确数据记录其读取时候的原值返回更新时若记录的原值与内存中对应的值不一致则认为该操作失效即实现了写后读思想保证了volatile的准确性。volatile使用优化追加字节追加数据使某一个任务所使用的数据占据一个缓存行64字节此时没有其他任务来读取不需要排队读取提高性能。注意并不是所有CPU的架构都支持该方式在两种情况下不应使用该方法1.缓存行非64字节宽的处理器。如P6系列和奔腾处理器它们的L1和L2高速缓存行是32个 字节宽。2.共享变量不会被频繁地写因为使用追加字节的方式需要处理器读取更多的字节到高速 缓冲区这本身就会带来一定的性能消耗如果共享变量不被频繁写的话锁的几率也非常 小就没必要通过追加字节的方式来避免相互锁定2.3 synchronized重量级锁synchronized不能直接修饰方法外的变量能修饰方法及方法内的变量volatile能修是属性但是不能修饰方法。要实现读的准确性要实现写后读写后读一个线程写对内存的更新完之后其他线程才可以读读取内存数据。2.3.1 synchronized代码讲解synchronized实现了写后读思想public class Test { public volatile int flag 0; public synchronized void add() { flag; } }public class Test1 { public static void main(String[] args) throws Exception { Test x1 new Test(); Thread t1 new Thread() { public void run() { for(int i 0;i100000;i) { x1.add(); } } }; Thread t2 new Thread() { public void run() { for(int i 0;i100000;i) { x1.add(); } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(x1.flag); } }运行结果若线程A对资源加synchronized锁成功即使线程A时间片到期线程B也无法读取synchronized锁住的资源所以保证了写后读最终结果准确。——synchronized禁止资源同时被两个线程拷贝。2.3.2 那么加synchronized锁后结果一定准确吗public class Test1 { public static void main(String[] args) throws Exception { Test x1 new Test(); Thread t1 new Thread() { public void run() { for(int i 0;i100000;i) { int a x1.get(); x1.set(a1); } } }; Thread t2 new Thread() { public void run() { for(int i 0;i100000;i) { int a x1.get(); x1.set(a1); } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(x1.flag); } }运行结果运行结果出错的原因读和写操作分开数据读取完成后释放锁若此时时间片恰好到期其他线程可以进行读取违背了读后写。所以一定要写数据更新完成后再释放锁否则加synchronized锁无效仍会出错。多线程并发、多进程并发、多服务器、分布式集群对某个变量进行操作只要实现了写后读那么结果一定是准确的同时写后读思想无视语言不管是C、C、Java等只要实现了写后读那么结果一定是准确的注意add()、get()、set()都属于加锁的非静态方法若其中一个线程调用 get()会锁住整个对象其他线程不允许调用add()、get()或set()其他想调用该对象内方法的线程直接进入阻塞队列。public class Test { public volatile int flag 0; public synchronized void add() { flag; } public synchronized int get() { System.out.println(get开始); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(get结束); return flag; } public synchronized void set(int a) { System.out.println(set开始); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(set结束); flag a; } }public class Test1 { public static void main(String[] args) throws Exception { Test x1 new Test(); Thread t1 new Thread() { public void run() { x1.get(); } }; Thread t2 new Thread() { public void run() { x1.set(20); } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(x1.flag); } }2.3.3 synchronized锁的形式对于普通同步方法锁是当前实例对象。对于静态同步方法锁是当前类的Class对象。对于同步方法块锁是Synchonized括号里配置的对象。public synchronized void add() { String flag1 null; synchronized(flag1){ } }如上述代码锁住的是flag1。注Synchonized只能锁引用类型数据。public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); synchronized(x1){ } }注意此时Synchonized锁住的是对象对象中的加锁及不加锁的方法都不能在代码执行完毕前被访问。public class Shop { public synchronized void m1() { //对象锁 System.out.println(m1开始); try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(m1结束); } public synchronized void m2() { //对象锁 System.out.println(m2开始); try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(m2结束); } public static synchronized void m3() { //类锁 System.out.println(m3开始); try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(m3结束); } public synchronized static void m4() { //类锁 static和synchronized都是修饰词先后顺序没有要求 System.out.println(m4开始); try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(m4结束); } public void m5() { System.out.println(m5开始); try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(m5结束); } public static void m6() { System.out.println(m6开始); try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(m6结束); } }public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { } }; Thread t2 new Thread() { public void run() { } }; t1.start(); t2.start(); } }内存图2.3.3.1 对象锁public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { x1.m1(); } }; Thread t2 new Thread() { public void run() { x1.m1(); } }; t1.start(); t2.start(); } }两个线程中同一个对象的m1()不能同时运行。public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { x1.m1(); } }; Thread t2 new Thread() { public void run() { x1.m2(); } }; t1.start(); t2.start(); } }两个线程中同一个对象的m1()和m2()不能同时运行。public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { x1.m1(); } }; Thread t2 new Thread() { public void run() { x2.m1(); } }; t1.start(); t2.start(); } }两个线程中不同对象的m1()能同时运行。public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { x1.m1(); } }; Thread t2 new Thread() { public void run() { x1.m3(); } }; t1.start(); t2.start(); } }m3()方法位于静态常量池锁住对象对其没有影响。对象锁一个线程调用方法时会锁住整个对象不允许另外一个线程想调用该对象中的其他方法但是不同对象中的同一方法可以调用。》非静态方法在每个对象中都有一份。2.3.3.2 类锁public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { x1.m4(); } }; Thread t2 new Thread() { public void run() { x1.m3(); } }; t1.start(); t2.start(); } }m3()和m4()是静态方法会锁住类常量池中的类不能同时进行调用。public class Test1 { public static void main(String[] aaa) throws Exception { Shop x1 new Shop(); Shop x2 new Shop(); Thread t1 new Thread() { public void run() { x1.m1(); } }; Thread t2 new Thread() { public void run() { x1.m5(); } }; t1.start(); t2.start(); } }对象锁及类锁的规则仅针对加锁的方法不加锁的方法如m5()不受影响 同一类锁只影响同一类锁对象锁和类锁不会互相影响不是同一个对象互不影响。
返回列表