总结Java中实现并发的几种方法 附完整代码

城春草木深 2021-08-18 11:42:38 浏览数 (4593)
反馈

最近,小编发现了一篇关于Java实现并发的方法总结的文章,来和大家分享一下。下面是详细内容,有兴趣的小伙伴们可以阅读一下。

Java实现并发的几种方法

Java程序默认以单线程方式运行。

synchronized

Java 用过synchronized 关键字来保证一次只有一个线程在执行代码块。

public synchronized void code() {
    // TODO
}

Volatile

Volatile 关键字保证任何线程在读取Volatile修饰的变量的时候,读取的都是这个变量的最新数据。

Threads 和 Runnable

public class MyRunnable implements Runnable {
    @Override
    public void run() {
     // TODO
    }
}
import java.util.ArrayList;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        Runnable task = new MyRunnable();
        Thread worker = new Thread(task);
        worker.setName('Myrunnable');
        worker.start();
}

创建thread会有很多overhead,性能低且不易管理

Thread pools

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
    private static final int NUMOFTHREDS = 5;
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(NUMOFTHREDS);
        for (int i = 0; i < 50; i++) {
            Runnable worker = new MyRunnable(i);
            executor.execute(worker);
        }
        // executor不接受新的threads
        executor.shutdown();
        // 等待所有threads结束
        executor.awaitTermination();
        System.out.println("Finished all threads");
    }
}

Futures 和 Callables

因为Runnable对象无法向调用者返回结果,我们可以用Callable类来返回结果。

package de.vogella.concurrency.callables;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Long> {
    @Override
    public Long call() throws Exception {
  // TODO
  int sum = 1;
        return sum;
    }
}
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableFutures {
    private static final int NUMOFTHREDS = 5;
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(NUMOFTHREDS);
        List<Future<Long>> list = new ArrayList<Future<Long>>();
        for (int i = 0; i < 10; i++) {
            Callable<Long> worker = new MyCallable();
            Future<Long> submit = executor.submit(worker);
            list.add(submit);
        }
        long sum = 0;
        for (Future<Long> future : list) {
            try {
                sum += future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        System.out.println(sum);
        executor.shutdown();
    }
}

CompletableFuture

CompletableFuture 在Future的基础上增加了异步调用的功能。callback()函数Thread执行结束的时候会自动调用。

CompletableFuture既支持阻塞,也支持非阻塞的callback()

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureSimpleSnippet {
    public static void main(String[] args) { 
        CompletableFuture<Integer>  data = createCompletableFuture()
                .thenApply((Integer count) -> {
                    int transformedValue = count * 10;
                    return transformedValue;
                });
            try {
              int count = futureCount.get();
             } catch (InterruptedException | ExecutionException ex) {
            }
    }
    private static CompletableFuture<Integer> createCompletableFuture() {
        CompletableFuture<Integer> futureCount = CompletableFuture.supplyAsync(
                () -> {
                    return 1;
                });
        return futureCount;
    }
}

补充:Java如何处理高并发的情况

为了更好的理解并发和同步,需要先明白两个重要的概念:同步和异步

所谓同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其它的命令。 同步就是一件事,一件事情一件事的做。

异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。异步就是,做一件事情,不影响做其他事情。

同步关键字synchronized,假如这个同步的监视对象是类的话,那么如果当一个对象 访问类里面的同步方法的话,那么其它的对象如果想要继续访问类里面的这个同步方法的话,就会进入阻塞,只有等前一个对象 执行完该同步方法后当前对象才能够继续执行该方法。这就是同步。相反,如果方法前没有同步关键字修饰的话,那么不同的对象可以在同一时间访问同一个方法,这就是异步。

脏数据:就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(Dirty Data),依据脏数据所做的操作可能是不正确的。

1、什么是并发问题

多个进程或线程同时(在同一段时间内)访问同一资源会产生并发问题。

比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户减去 50元,A先提交,B后提交。 最后实际账户余额为1000-50=950元,但本该为 1000+100-50=1050。这就是典型的并发问题。如何解决?

处理并发和同同步问题主要是通过锁机制。

2、如何处理并发和同步

一种是java中的同步锁,典型的就是同步关键字synchronized。

另外一种比较典型的就是悲观锁和乐观锁。

在java中有两种方式实现原子性操作(即同步操作):

1)使用同步关键字synchronized

2)使用lock锁机制其中也包括相应的读写锁

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自 外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。

乐观锁,大多是基于数据版本 Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。

【谨防在此,面试官会问到死锁的相关问题!!!关于死锁的问题,在其余某篇博客都有说明】

3、常见并发同步案例分析

案例一、订票系统案例

某航班只有一张机票,假定有1w个人打开你的网站来订票,问你如何解决并发问题(可扩展到任何高并发网站要考虑的并发读写问题)

假定我们采用了同步机制或者数据库物理锁机制,如何保证1w个人还能同时看到有票,显然会牺牲性能,在高并发网站中是不可取的。

采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样即保证数据的并发可读性又保证保存数据的排他性,保证性能的同时解决了并发带来的脏数据问题。

如何实现乐观锁:

前提:在现有表当中增加一个冗余字段,version版本号, long类型

原理:

1)只有当前版本号>=数据库表版本号,才能提交

2)提交成功后,版本号version ++

案例二、股票交易系统、银行系统,大数据量你是如何考虑的

首先,股票交易系统的行情表,每几秒钟就有一个行情记录产生,一天下来就有(假定行情3秒一个) 股票数量×20×60*6 条记录,一月下来这个表记录数量多大? 一张表的记录数超过100w后 查询性能就很差了,如何保证系统性能?

再比如,中国移动有上亿的用户量,表如何设计?把所有用于存在于一个表?

所以,大数量的系统,必须考虑表拆分-(表名字不一样,但是结构完全一样),通用的几种方式:(视情况而定)

1)按业务分,比如 手机号的表,我们可以考虑 130开头的作为一个表,131开头的另外一张表 以此类推

2)利用表拆分机制做分表

3)如果是交易系统,我们可以考虑按时间轴拆分,当日数据一个表,历史数据弄到其它表。这里历史数据的报表和查询不会影响当日交易。

此外,我们还得考虑缓存

这里的缓存独立于应用,依然是内存的读取,假如我们能减少数据库频繁的访问,那对系统肯定大大有利的。比如一个电子商务系统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑这部分商品列表存放到缓存(内存中去),这样不用每次访问数据库,性能大大增加。

4、常见的提高高并发下访问的效率的手段

首先要了解高并发的的瓶颈在哪里?

1、可能是服务器网络带宽不够

2.可能web线程连接数不够

3.可能数据库连接查询上不去。

根据不同的情况,解决思路也不同。

1、像第一种情况可以增加网络带宽,DNS域名解析分发多台服务器。

2、负载均衡,前置代理服务器nginx、apache等等

3、数据库查询优化,读写分离,分表等等

最后复制一些在高并发下面需要常常需要处理的内容

1、尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与数据库的交互,提高性能。

2、用jprofiler等工具找出性能瓶颈,减少额外的开销。

3、优化数据库查询语句,减少直接使用hibernate等工具的直接生成语句(仅耗时较长的查询做优化)。

4、优化数据库结构,多做索引,提高查询效率。

5、统计的功能尽量做缓存,或按每天一统计或定时统计相关报表,避免需要时进行统计的功能。

6、能使用静态页面的地方尽量使用,减少容器的解析(尽量将动态内容生成静态html来显示)。

7、解决以上问题后,使用服务器集群来解决单台的瓶颈问题。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持W3Cschool


0 人点赞