java并发编程实战-初阅读-持续更新

第一章:简介
主要说了线程的好处和线程的重要性,总之一句话。多线程好!

第一部分:基础知识

第一部分主要为概念性的知识,这里先暂时跳过,看具体的实现

第二章:线程安全性

        首先解释一下什么是加锁。从应用的角度来看,加锁实际上就是用synchronized关键字对一段代码或一个方法进行修饰。比如说,对一个类的实例(对象)中的某一个属性或方法进行访问时,从逻辑的角度上讲我们希望同时只有一个线程可以访问这个属性或方法,这时就需要加锁。就相当于我们有一个房间,但同一时间仅希望一个一个人进入,很简单地方法就是给这个房间加个锁。房间就相当于将要访问的属性或方法,人就相当于线程,加锁的动作就相当于用synchronized关键字修饰,加的锁就是使用关键字时加的参数。前面都比较好理解,这里再解释一下这个关键字。关键字可以是“this”或是“Example.class”。当用this时,就是用当前对象作为锁(一个类可以有很多对象),线程只有获取这个对象才能执行synchronized中的代码。当使用Example.class时,可以简单理解为使用当前类作用锁(也就是说一个类只有一个)。

        然后进入第二章的部分,总的来说,第二章主要说了两件事情,一是什么时候需要加锁,二是如何加锁。此外还需要解释一下什么是线程安全什么是原子性。

        对于第一个问题,当有多个线程会同时对一个属性进行操作时,需要加锁。书中将他分为了两种细分情况,一种是“先检查后执行”,一种是“读取-修改-写入”。具体来说,第一种说的是,对于一个变量,先检查他是否满足一定条件,然后再执行一些操作,第二种则有一个很简单的例子就是count++。

        对于第二个问题,加锁时需要注意几点,一是如果一个变量可能会被多线程访问,那么需要全程加锁;二是一个线程访问某个变量时需要持有同一个锁;三是不要将一个需要很长执行时间且不影响共享状态的代码块加锁

        对于线程安全来说,我现在的理解是在不同线程执行时,不变性条件不被改变,一个对象不要被同时操作(书上对于安全的定义也并不很明确,这里是我的想法,供参考)

        对于原子性来说,这里主要指“一组语句作为不可分割的单元被执行”。多个语句或多个原子操作可以通过加锁而变成一个原子操作。换句话说,原子操作是某个对象只能被一个线程进行操作(我的理解)

第二部分:结构化并发应用程序

第六章:任务执行

本章主要概述了应该用什么样的思路来处理许多任务

        首先最简单的想法就是创建一个线程来处理所有任务或为每一个任务都创建一个线程。这两个思路都有一定的问题。对于一个线程处理所有任务来说,如果任务多的话会导致排队时间很长,并且CPU资源没有得到充分利用。如果每个任务都创建一个线程,会由于线程过多导致浪费,并且创建和删除线程也是需要消耗资源的。我们希望找到二者的一个平衡点。

        因此决定使用Executor框架。说白了就是用线程池来处理任务:先把任务准备好,然后找几个线程来处理这个任务。

        在我们启动线程的时候,一般是需要重写Runnable接口中的run方法,然后用一个Thread来启动他。在Executor框架中,用Runnable表示任务,有任务之后execute它。

private static final Executor exec = Executors.newFixedThreadPool(100);
Runnable task = new Runnable(){
    public void run(){
        int a = 10;
    }
};
exex.execute(task);

在上述代码中,首先定义了一个线程池exec,然后定义了任务task,最后用线程池来处理这个task。

        在上面的内容中,我们说的十分简单,“有很多任务”。这是默认任务之间大小差不多并且界限比较明显,在实际情况中可能并不是这样。比如说在浏览器加载一个页面是,页面上会有很多文字和很多图片。文字会一次性从服务器中获取但图片则不是:每张图片都需要向服务器发送一次请求。如果我们简单地把加载页面任务划分为加载文字和加载图片,那么即使我们使用线程池,程序性能也不能获得很大提升。就好比我们有任务A和任务B,处理A需要十秒,B需要一秒。那么即使我们让两个人来处理这两个任务,处理时间也不过是从十一秒下降到十秒而已。

        因此我们需要找出“可利用的并行性”。书中并没有给出一个指导性的方法告诉我们如何将任务划分。只是给出了上述浏览器加载的例子,并告诉我们可以将每次申请图片都当做一个任务。我想在具体实践中这可能需要我们自己的智慧吧。

        这一章主线思路到这里就基本上结束了,下面是实现时的一些细节。

        首先,Runnable类有一个缺点就是没有返回值,这样我们就看不到任务是否执行成功。因此可以用Callable和Future来替代他。Callable作为任务,而Future则作为接受结果。在下面的自我总结部分会说一下Callable和Runnable的关系、Callable和Future如何共用。

        其次,如果我们要获取返回值的话,一个简单的思路就是保留每个任务的Future,然后通过轮询的方法判断任务是否完成。但这样有点繁琐,我们可以考虑用CompletionService接口来处理。CompletionService与ExecutorCompletionService共生,前一个是接口后一个是类。后面的类实现了前面的接口。在实际实现中用ExecutorCompletionService来新建一个CompletionService的对象,然后用这个CompletionService对象来提交任务。任务的处理结果会以Future的形式储存在CompletionService对象中。这样就实现了一种相对简单的Future储存方法。

        最后,我们可以为每一个任务设置处理实现,如果超过这个时限就返回异常。具体的实现可以借助Future来完成。

        上面就是书中内容的大致总结,下面是我自己的疑问以及相应的处理。

        一:Executor和Executors、ExecutorService的关系

                Executor是一个接口,只有一个execute方法。可以把他理解成这个框架的底层。ExecutorService也是一个接口,他继承了Executor并添加了一些方法。他属于是增强版的Executor。Executors是一个(工厂)类,他具体给出了一些方法来创建ExecutorService

        二:Callable和Runnable的关系

                Callable和Runnable在作用上都可以理解成一个任务(线程就是用来处理任务的)。Runnable需要重写run方法,Callable需要重写call方法。只不过是Callable功能比Runnable更强大一点:Callable有返回值Future而Runnable没有返回值。在实际应用的时候,一般使用ExecutorService来处理这些任务。Callable用submit而Runnable用execute。此外需要注意一点的是他们之间并不存在继承关系。

        三:Callable和Future如何共用

                其实只需要记住一点:Future是Callable的返回值。

ExecutorService executorService = Executors.newSingleThreadExecutor();

        // 创建一个 Callable 任务
        Callable<Integer> callableTask = () -> {
            // 具体任务内容,这里我没写
        };

        // 提交任务并获取 Future 对象
        Future<Integer> future = executorService.submit(callableTask);

                关于Future,我们可以调用get方法获取结果(这样会阻塞线程直到任务处理完毕)。此外Future还可以用于中断任务,这个应该下一章会讲

        四:调用run方法和 .start() 有什么区别

                从表面来看这两个区别不大,但从线程的角度来讲这两个有很大区别。如果只是调用run方法,那么只会在当前线程中执行这个run方法而不会启动新线程,用start的话则是启动一个新线程,这个新线程用来处理run方法中定义的任务。

        五:新建线程池的时候是会直接启动一些线程吗

                一般情况不会,线程会在任务被提交时按需启动

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值