技术分享java并发编程

Java并发编程

1.什么是java并发编程

​ Java是一种多线程编程语言,我们可以使用Java来开发多线程程序。 多线程程序包含两个或多个可同时运行的部分,每个部分可以同时处理不同的任务,从而能更好地利用可用资源,特别是当您的计算机有多个CPU时。多线程使您能够写入多个活动,可以在同一程序中同时进行操作处理。

根据定义,多任务是当多个进程共享,如CPU处理公共资源。 多线程将多任务的概念扩展到可以将单个应用程序中的特定操作细分为单个线程的应用程序。每个线程可以并行运行。 操作系统不仅在不同的应用程序之间划分处理时间,而且在应用程序中的每个线程之间划分处理时间。

多线程能够在同一程序同中,进行多个活动的方式进行写入。(一句话概括就是,可以让程序同时处理多个任务)

2.为什么需要多线程

​ 一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升

顶级计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。

另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分

摩尔定律:

摩尔定律(英语:Moore’s law)是由英特尔(Intel)创始人之一[戈登·摩尔]提出的。其内容为:集成电路上可容纳的晶体管数目,约每隔两年便会增加一倍;经常被引用的“18个月”,是由英特尔首席执行官大卫·豪斯(David House)提出:预计18个月会将芯片的性能提高一倍(即更多的晶体管使其更快),是一种以倍数增长的观测。

半导体行业大致按照摩尔定律发展了半个多世纪,对二十世纪后半叶的世界经济增长做出了贡献,并驱动了一系列科技创新、社会改革、生产效率的提高和经济增长。个人电脑,智能手机等技术改善和创新都离不开摩尔定律的延续。

3.线程不安全示例

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static java.util.concurrent.Executors.*;

public class JUCUnSafe {
    public static void main(String[] args) throws InterruptedException {

        final int threadSize = 1000;
        ThreadUnsafeExample example = new ThreadUnsafeExample();
        final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        ExecutorService executorService = newCachedThreadPool();
        for (int i = 0; i < threadSize; i++) {
            executorService.execute(() -> {
                example.add();
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println(example.get());

    } 
    public static class ThreadUnsafeExample {

        private int cnt = 0;

        public void add() {
            cnt++;
        }
        public int get() {
            return cnt;
        }
    }
}




​ 这个问题产生的原因就是由于多线程之间没有做到下面三点,只要满足了以下三点就可以说线程是安全的了

​ 1.可见性 2.原子性 3.有序性

4.什么是可见性,原子性和有序性

可见性: CPU缓存引起

可见性定义:一个线程对共享变量的修改,另外一个线程能够立刻看到。

就是线程不会主动地将修改的值立即提交给内存,而是将其存入高速缓存之中,而缓存是不能共享的。JMM中使用volatile关键字保证变量的可见性

原子性: 分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

经典的转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

有序性:

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,在java内存模型中,java内置机制帮我们做好了。Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字

  • Happens-Before 规则

    JMM涉及的原理过深一句话概括,他的作用就是*于屏蔽各种硬件和操作系统带来的内存访问的差异,定义了程序中各个***变量*不包括局部变量和方法参数,因为这些是线程私有的)的访问规则),感兴趣可以查看以下资料:

    https://zhuanlan.zhihu.com/p/29881777

    http://ifeve.com/java-%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b/

5.JAVA并发编程中的锁机制与原子类

java并发编程中原子的实现主要是依靠CAS,了解了CAS就可以说已经理解了原子类的核心原理

什么是CAS

CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。 简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。Java中的自旋锁也是基于CAS实现

java并发编程中的锁机制的实现主要是基于AQS实现

什么是AQS

AbstractQueuedSynchronizer简介

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLock(再短时间内可以获取同一把锁,而不是让线程重新获取锁),Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器

AQS 核心思想

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配

AQS 对资源的共享方式

AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch

6.java并发编程中的乐观锁与悲观锁

悲观锁:

悲观锁,总是假设每次每次被读取的数据会被修改,所以要给读取的数据加锁,具有强烈的资源独占和排他特性,在整个数据处理过程中,将数据处于锁定状态,例如synchronized关键字的实现就是悲观机制。

悲观锁.png

悲观锁的实现,往往依靠数据库提供的锁机制,只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据,悲观锁主要分为共享读锁和排他写锁。

排他锁基本机制:又称写锁,允许获取排他锁的事务更新数据,阻止其他事务取得相同的资源的共享读锁和排他锁。若事务T对数据对象A加上写锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的写锁

乐观锁:

乐观锁相对悲观锁而言,采用更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务的开销非常的占资源,乐观锁机制在一定程度上解决了这个问题。

乐观锁.png

乐观锁大多是基于数据版本记录机制实现,为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个version字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。乐观锁机制在高并发场景下,可能会导致大量更新失败的操作。

乐观锁的实现是策略层面的实现:CAS(Compare-And-Swap)。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能成功更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

6.异步线程futureTask与CompletableFuture

FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。

futerTask常用方法:cancel 用于尝试取消任务。

get ========用于等待并获取任务执行结果。带时间参数的 get 方法只会等待指定时间长度。

isCancelled===== 返回任务在完成前是否已经被取消。

isDone========= 返回任务是否完成。

java8中的CompletableFuture:

在Java中CompletableFuture用于异步编程,异步编程是编写非阻塞的代码,运行的任务在一个单独的线程,与主线程隔离,并且会通知主线程它的进度,成功或者失败。

在这种方式中,主线程不会被阻塞,不需要一直等到子线程完成主线程可以并行的执行其他任务

使用这种并行方式,可以极大的提高程序的性能。

Future vs CompletableFuture

CompletableFuture 是 Future API的扩展。

Future 被用于作为一个异步计算结果的引用。提供一个 isDone() 方法来检查计算任务是否完成。当任务完成时,get() 方法用来接收计算任务的结果。Future API 是非常好的 Java 异步编程进阶,但是它缺乏一些非常重要和有用的特性。

Future 的局限性

  1. 不能手动完成(依赖于future内部机制,也就是程序执行之前设置的参数,不能手动结束),假设我们有一个函数,用于通过一个远程API获取一个电子商务产品最新价格。假设这个 API 太耗时,它在一个独立的线程中,我们现在需要从函数中返回一个 Future。但现在假设这个API服务宕机了,这时想通过该产品的最新缓存价格手工完成这个Future几乎是不可能的。
  2. Future 的结果在非阻塞的情况下,不能执行更进一步的操作。 Future 不会通知你它已经完成了(被动的等待调用),它提供了一个阻塞的 get() 方法通知你结果。无法给 Future 植入一个回调函数,当 Future 结果可用的时候,用该回调函数自动的调用 Future 的结果。
  3. 多个 Future 不能串联在一起组成链式调用,(无法用于复杂计算) 有时候我们需要执行一个长时间运行的计算任务,并且当计算任务完成的时候,还需要把它的计算结果发送给另外一个长时间运行的计算任务,会发现,future也是不能实现的。

CompletableFuture 实现了 FutureCompletionStage接口,并且提供了许多关于创建,链式调用和组合多个 Future 的便利方法集,而且有广泛的异常处理支持。使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

7.如何利用java并发编程提高软件的性能(能用在实际生产之中)

R3实例代码:

//利用多綫程獲取相似的數據  -----組合
List<SignedAmountVo> signedAmountVoList = null;
        BigDecimal rate = amsRateUtils.getRate();
        try {
            //計數器
            CountDownLatch latch = new CountDownLatch(2);
            FutureTask<List<SignedAmountVo>> future = new FutureTask<List<SignedAmountVo>>(
                    () -> {
                        List<SignedAmountVo> model = baseMapper.getBySignedAmount(accountId);
                        latch.countDown();
                        return model;
                    }
            );
            FutureTask<List<SignedAmountVo>> future1 = new FutureTask<List<SignedAmountVo>>(
                    () -> {
                        List<SignedAmountVo> model = baseMapper.getBySignedAmountTwoLeve(accountId);
                        latch.countDown();
                        return model;
                    }
            );
            executorService.execute(future);
            executorService.execute(future1);
            latch.await();
            signedAmountVoList = future.get();
            List<SignedAmountVo> signedAmountVoList1 = future1.get();
/*利用多綫程提高初始化數據的速度*/ 
public void initDB()  {

        Future<Integer> submit = executorService.submit(() -> {
            // 初始化厅会
            initCompany();
            // 初始化account表
            initAccount();
            // 初始化account_setting表
            initAccountSetting();
            return 1;
        });

        Future<Integer> submit1 = executorService.submit(() -> {
            // 初始化account_name表
            initAccountName();
            // 初始化account_authorize表
            initAccountAuthorize();
            return 1;
        });

        Future<Integer> submit2 = executorService.submit(() -> {
            // 初始化account_authorize_sms表
            initAccountAuthorizeSms();
            // 初始化account_sms表
            initAccountSms();
            return 1;
        });

        Future<Integer> submit3 = executorService.submit(() -> {
            submit.get();
            submit1.get();
            submit2.get();
            // 初始化删除资源
            initDeleteDB();
            return 1;
        });

        try {
            submit3.get();
        } catch (Exception e) {
            AccountAssertUtils.defaultAssert("失敗");
        }
        log.debug("結束-----------------------------------------------------------------------");
    }
public ProgrammeOrderVO getDetail(Long id) {
		// 查询方案订单详情
		CompletableFuture<ProgrammeOrder> programmeOrderFuture = ThreadUtil.supplyAsync(() -> baseMapper.selectDetail(id));
		// 查询订单流程
		CompletableFuture<OrderChangeLog> orderChangeLogFuture = ThreadUtil.supplyAsync(() -> orderChangeLogService.getNewestByOrderId(id));
		// 查询价格更改记录
		CompletableFuture<List<ProgressLog>> progressLogFuture = ThreadUtil.supplyAsync(() -> progressLogService.getProgressLogListByRelationId(id));
		try {

			ProgrammeOrder programmeOrder = programmeOrderFuture.get();
			ProgrammeOrderVO programmeOrderVO = ProgrammeOrderWrapper.build().entityVO(programmeOrder);
			OrderChangeLog orderChangeLog = orderChangeLogFuture.get();
			List<ProgressLog> progressLogList = progressLogFuture.get();
			programmeOrderVO.setChangePriceOperator(Objects.nonNull(orderChangeLog) ? orderChangeLog.getUsername() : null);
			programmeOrderVO.setProgressLogList(progressLogList);
			return programmeOrderVO;
		} catch (Exception e) {
			log.error(e.getMessage(),e);
		}
		return null;
	}

1.线程池应改做成工具类,全部线程池不应在业务代码中直接创建

2.加锁一定要记得解锁,不解锁会造成资源永远得不到释放

(orderChangeLog) ? orderChangeLog.getUsername() : null);
programmeOrderVO.setProgressLogList(progressLogList);
return programmeOrderVO;
} catch (Exception e) {
log.error(e.getMessage(),e);
}
return null;
}






1.线程池应改做成工具类,全部线程池不应在业务代码中直接创建

2.加锁一定要记得解锁,不解锁会造成资源永远得不到释放

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值