Java并发编程问题与执行实现

并发编程的应用场景

1、某些需要提高性能的地方,显然在大多数情况可以多线程执行的地方采用多线程执行可以增加程序的执行效率;提高CPU利用率;
2、同步访问地段,除了在主动应用多线程提高性能外,必须在支持多线程访问的接口上进行多线程的处理,否则很容易出现问题;
3、有时需要保证多个用户对于系统顺序访问的公平性,让其通过多线程的方式比单线程串行较好一些。
4、通过将一个大的任务分成几个单独的小任务分线程来执行,不一定是为了效率,这样的编程方式使得各个线程任务隔离执行,提高可读性。

并发编程的问题

1、线程安全问题

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行其他操作,调用对象都可以获得正确的结果,那么它是一个线程安全的。
否则,它是线程不安全的,即在多线程执行情况下可能出现结果和期望值不相同的情况。

Java中采用物种情况来确定线程安全:
1)不可变的常量,通过final定义
2)绝对线程安全,同步代码块修饰,即使是Java中的安全集合类也并非绝对安全。因为调用者可能并非线程安全。
3)相对线程安全,对于对象的单独操作是线程安全的,如大部分的线程安全集合。
4)线程兼容安全,需要使用者加同步手段,对象本身不安全。
5)线程对立,无法在多线程中使用的。容易死锁的一些方法。

2、线程执行活跃度问题

线程执行过程中出现了错误的返回结果,或者线程执行了无法返回进入死锁。

死锁
死锁指使用锁或者是说一些临界资源时,多线程出现一个环路等待的情况的情况,例如A拿着锁1要锁2,B持有锁2要锁1;从而形成 A->B->A->B…的循环请求过程。

死锁的四个条件:
1、互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,
其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

避免死锁:
·避免一个线程同时获取多个锁。
·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

3、性能问题

多线线程执行结束,但是效率不够。。;
3.1、上下文切换
CPU通过时间片轮转算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

单CPU执行过程中,CPU通过快速短暂的CPU时间片轮转实现多线程,使的看似多个线程同时执行。问题存在于需要记录上下文降低了多线程的一个执行效率。

即在多线程的普通执行情况下,相对于单线程多了创建和上下文切换的开销。

减少上下文切换的方法:无锁并发编程、CAS、使用最少线程和使用协程;
3.2、资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。如服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限
制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接
数和socket连接数等。

因此在有固定资源限制时,多线程未必能提高效率,反而可能由于上下文切换而降低效率。因此需要在同的场景调整不同的程序并发度;

资源限制解决方案:
对于硬件资源限制,可以考虑使用集群并行执行程序。对于软件资源限制,可以考虑使用资源池将资源复用。

Java并发实现原理分析

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。

volatile简介

Java编程语言允许线程访问共享变量,为了
确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言
提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存
模型确保所有线程看到这个变量的值是一致的。

volatile保证线程执行的可见性;禁止指令重拍保证有序性;
原理基本介绍:对volatile关键字修饰的读写指令会分别加上读写屏障;
内存屏障:CPU的一个指令,用来禁止指令重排的,常见于Volatile关键字的读读、读写、写写、写读四种屏障,
屏障指令通过将其和其后面的指令不放入序列缓存区,而放在一个FIFO的队列中执行。当这一批指令执行完成后在进入乱序执行。

写屏障保证了对该屏障之前对于变量的改动都同步到主存中;
读屏障保证在改屏障之后的读取加载的都是主存中的最新指令;
此外屏障还对读写指令的前后代码重排序加了控制,保证写屏障之前的代码不会排在后面,
而读屏障之后的代码不会排在读指令之前。

Synchronized简介

Synchronized关键字用于给线程加锁,它可以修饰方法代码块或是类;常用来保证线程执行的原子操作安全性;本质是一种加锁的过程,如果修饰非静态方法即同步整个方法,修饰静态方法作用类的对象,Synchronized加的锁是可重入的,出现异常会自动释放锁,每个java对象都会关联一个Monitor监视器,使用Synchronized加上锁后,对象头中的markWord就被设置一个指向Monitor对象的指针;

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,
这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。
更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

原子操作简介

原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。
在Java中可以通过锁和循环CAS的方式来实现原子操作
1、CAS
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本
思路就是循环进行CAS操作直到成功为止,以下代码实现了一个基于CAS线程安全的计数器
方法safeCount和一个非线程安全的计数器count。
CAS操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。
如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。
如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。
无论哪种情况,它都会在 CAS 指令之前返回该位置的值。
当然使用CAS也有问题,如ABA、循环开销、不保证多个共享变量的原子操作。
2、使用锁
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。通过对共享变量进行加锁,从而保证原子性;

参考:Java并发编程艺术

Guess you like

Origin blog.csdn.net/qq_44830792/article/details/121295576