当前位置: 首页 > 后端技术 > Java

并发编程的概念

时间:2023-04-02 10:02:06 Java

为什么做并发编程主要是为了压榨硬件(上图是我的cpu使用情况)。现在硬件已经是过剩的状态了,何乐而不为呢,总不能指望程序员天天拿头发去优化算法吧。并发编程带来的问题安全问题我们都知道并发编程可以提高效率,但这必然是要付出代价的,它会带来很多问题,主要分为三类。原子性问题原子性:一个或多个操作在CPU执行过程中是不可分割的。不可分的意思是中间状态对外界不可见,所以只要保证对外界不可见即可。为什么会出现这个问题?cpu会和上图差不多,hoop线程会定时执行,达到多个程序同时执行的感觉。换句话说,在你的程序执行到一半时,你可能会离开。Java是一门高级语言,一行往往对应多条CPU指令,再加上CPU指令是排序的,很可能结果出来了,但是流程还没走完,一旦被外部使用世界,就会出现问题。例如,Objecto=newObject();此操作对应于以下3个步骤。申请内存,赋默认值,初始化成员变量,给对象引用赋值。如果这个进程对外不可见,你怎么取回来都无所谓,但是如果2和3丢了,别人用的时候可能会发现没有初始化。问题。这只是一行命令,已经存在风险。多行语句出现问题的概率更大。如何解决?锁可见性问题可见性:一个线程可以修改一个值,另一个线程可以立即看到。为什么会出现这样的问题?内存速度比CPU慢很多,所以有CPU缓存,每个核心都有自己的CPU缓存。如果多个CPU核心处理同一个变量,处理完成后未能及时刷新主存并通知其他线程重新读取,会造成可见性问题。怎么解决?voliatelockordering问题排序:程序是按照代码的顺序执行的为什么会出现这个问题?前面说过,一行高级语言往往对应多条CPU指令。为了优化性能,编译器有时会改变程序中语句的顺序。指令集的并行重排序是为了优化CPU的性能。从指令执行的角度来看,一条指令可以分为多个步骤来完成,具体如下:取指IF译码取寄存器操作数ID执行或有效地址计算EX(ALU逻辑运算单元)内存访问MEM写回WB(寄存器)x代表这里暂停,应该是R2的数据没有准备好,所以在这里稍等片刻。接下来我们再来看另一种情况a=b+cd=e-f。会有很多停顿。这些指令的轻微重拍可以解决这些停顿并提高CPU利用率而不影响结果。只需重新排列说明即可,但效率更高。但是不能排序,为了减少停顿,减少随机排序。JMM通过happens-before保证可见性。程序顺序规则:线程中的每个操作都发生在该线程中的任何后续操作之前。监控锁规则:解锁一个锁,happens-before在后续锁的加锁中。易失性变量规则:对易失性字段的写入发生在对易失性字段的任何后续读取之前。传递性:如果Ahappens-beforeB,且Bhappens-beforeC,则Ahappens-beforeC。()operationofthreadAhappens-beforeanyoperationinthreadB.Join()规则:如果线程A执行了操作ThreadB.join()并成功返回,则线程B中的任何操作happens-before线程A从ThreadB.join()操作。程序中断规则:调用线程interrupted()方法先于被中断线程的代码检测中断时间的发生。对象finalize规则:对象的初始化完成(构造函数执行结束)在其finalize()方法开始之前。怎么解决?VoliatelockfinalactivityproblemDeadlock一组线程争夺资源相互等待,导致“永久”阻塞。并发程序一旦死锁,一般没有特别好的办法。很多情况下,我们只能重启应用。因此,解决死锁问题最好的方法就是避免死锁。互斥,共享资源X和Y只能被一个线程占用;占用等待,线程T1已获取共享资源X,等待共享资源Y时不释放共享资源X;非抢占,其他线程不能强行抢占该线程T1占用的资源;循环等待,线程T1等待线程T2占用的资源,线程T2等待线程T1占用的资源,这就是循环等待。Livelock有时虽然线程没有被阻塞,但仍然无法执行。这就是所谓的“活锁”饥饿。所谓“饥饿”,是指线程因无法访问所需资源而无法继续执行的情况。优先级低的线程被执行的机会很小,可能会出现线程“饥饿”;持有锁的线程,如果执行时间过长,也可能造成“饥饿”问题。性能问题不能很好的利用多线程的性能优势,也不能为了多线程而使用多线程,因为使用锁必然会带来一些性能问题,很可能在某些情况下执行时间可能不如多线程。我们之所以用多线程来搞并发程序,是为了提高性能。使用无锁优化,减少锁的持有时间,降低锁的粒度。使用读写锁分离锁来代替独占锁。写的很好,推荐看这篇synchronizedsynchronized。首先说一下如何使用它,它作用于方法,作用于同步代码块。写在静态方法中时,Class对象被锁定,与synchronized代码块中写(xxx.class)的效果一致。下面有测试代码。有兴趣的可以测试一下线程.睡眠(10000);System.out.println("测试1结束");测试3();}publicstaticsynchronizedvoidtest2()throwsInterruptedException{System.out.println("test2");线程.睡眠(10000);System.out.println("测试2结束");}publicvoidtest3()throwsInterruptedException{synchronized(this){System.out.println("test3");线程.睡眠(10000);System.out.println("测试3结束");}}publicvoidtest4()throwsInterruptedException{synchronized(SynchronizedDemo.class){System.out.println("test4");.睡眠(10000);System.out.println("测试4结束");测试2();}}publicstaticvoidmain(String[]args){SynchronizedDemosynchronizedDemo=newSynchronizedDemo();Runnabler=newRunnable(){@Overridepublicvoidrun(){try{//synchronizedDemo.test1();synchronizedDemo.test2();}catch(InterruptedExceptione){e.printStackTrace();}}};Runnabler1=newRunnable(){@Overridepublicvoidrun(){try{//synchronizedDemo.test3();synchronizedDemo.test4();}catch(InterruptedExceptione){e.printStackTrace();}}};线程t1=新线程(r);Threadt2=newThread(r1);//t1.start();t2.开始();}}synchronized锁其实存在于java对象头中。jvm用2个字来存放对象头(如果对象是数组,会分配3个字,多出来的字记录数组的长度)。其主要结构是MarkWord和ClassMetadataA地址的组成,其结构如下表描述:虚拟机位头对象结构描述32/64bitMarkWord存储对象的hashCode、锁信息、分代年龄或GC标志等信息32/64bitClassMetadataAddress类型指针指向到对象的类元数据,JVM通过这个指针来判断对象是哪个类的实例。MarkWord在不同的锁定状态下存储不同的内容。当只有一个线程获取锁时,偏向锁的标识改为1,线程id,时间戳。当其他线程尝试获取锁时,会判断当前线程id和markword中的线程id是否一致,扩展为轻量级锁,然后自旋比较。如果没有获取到锁,则会扩展为一个重量级锁,重量级锁的指针指向一个监控对象,结构如下ObjectMonitor(){_header=NULL;_count=0;//记录数_waiters=0,_recursions=0;_object=NULL;_owner=NULL;_WaitSet=NULL;//等待状态的线程会加入_WaitSet_WaitSetLock=0;_Responsible=NULL;_成功=空;_cxq=空;FreeNext=NULL;_EntryList=NULL;线程将被添加到列表中_SpinFreq=0;_自旋时钟=0;所有者是线程=0;}_owner:指向持有ObjectMonitor对象的线程_WaitSet:存放等待状态的线程队列_EntryList:存放等待锁块状态的线程队列_recursions:锁重入次数_count:用于记录线程获取的次数锁。当多个线程同时访问一段同步代码时,会先进入_EntryList队列。当线程获取对象后进入monitor后的_Owner区域,将monitor中的_owner变量设置为当前线程,monitor中的计数器_count加1,即获取到对象锁。如果持有monitor的线程调用wait()方法,当前持有的monitor会被释放,_owner变量恢复为null,_count减1,同时线程进入_WaitSet收集并等待被唤醒。如果当前线程执行完毕,也会释放管程(锁)并重置变量的值,以便其他线程进入并获取管程(锁)。如下图所示,volatile关键字确保对变量的更新对其他线程可见。当一个变量被声明为volatile时,线程写入时不会将该值缓存在寄存器或其他地方,线程读取时会从主存中获取最新的值,而不是使用当前线程的拷贝内存变量价值。尽管volatile提供了可见性保证,但它不能用于构建复合原子操作。也就是说,当一个变量依赖于其他变量或更新变量值时,新值依赖于当前旧值时,它不适用。如图,线程A修改了volatile变量b的值,然后线程B读取了变化的值,那么在写入变量b的值之前线程A可见的所有变量值都会被更改为线程B读取volatile变量b后的值。是可见的,图中线程B对A操作的变量a和b的值是可见的。volatile的内存语义和synchronized类似。具体来说,当一个线程写入volatile变量的值时,相当于线程退出了synchronized同步块(写入本地内存的变量值会被同步到主存)。Read取一个volatile变量的值相当于进入了同步块(会先清除本地内存变量值,再从主存中获取最新的值)。转自finalFinal的基本用法想必大家都已经了解了,它在多线程中比较重要的两点。写入构造函数中的final字段,然后将这个变量赋值给一个引用变量。这两个不能重排读取对象的引用和读取final字段,也不能在这两个操作之间重排(大部分处理器都是这样,但也有少数处理器抽风)参考书《实战Java高并发程序设计》