java编程思想第二十一章---并发

1.并发多面性

并发编程令人疑惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系(而且通常只具有模糊的界线)。

用并发解决的问题大体上可以分为“速度”和“设计可管理性”两种。

1、更快的执行

如果你想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。但是并发通常是提高运行在单个处理器上的程序和性能。

在单个处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓的上下文切换的代价(从一个任务切换到另一个任务)。

然而阻塞使这个问题发生了变化,如果某个任务或者线程阻塞了,导致不能继续执行,那么我们可以说这个任务或者线程阻塞了。如果没有并发,那么整个程序都将停止下来。

并发在此时就发挥它的作用,当一个任务阻塞的时候,程序中其他任务还可以继续执行。(如果没有任务阻塞,那么单处理器机器上使用并发就没有任何意义。)

2、改进代码设计

在单cpu机器上使用多任务的程序在任意时刻仍旧只在执行一项工作,因此从理论上讲,肯定可以不用任何任务而编写出相同的程序。但是, 并发提供了一个重要的组织结构上的好处:你的程序设计可以极大地简化。

某些类型的问题,例如仿真,没有并发的支持很难解决。

解决这个问题的典型方式是使用协作多线程。java的线程机制是抢占式,这表示调度机制会周期性地中断线程,将上下文切换到另一个进程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

在协作式系统中,每个任务都会自动地放弃控制,这要求程序要有意识地在每个任务中插入某个类型的让步语句。

协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉许多,而且对可以同时执行线程数量在理论上是没有限制。

并发需要付出代价,包含复杂性大家,但是这些代价与在程序设计、资源负载均衡以及用户方便使用方面相比,就显得微不足道了。通常,线程是你能够创建更加松散耦合的设计,

否则,你的代码中各个部分都必须显式地关注那些通常可以由线程来处理的任务。

2.基本的线程机制

1.定义任务

实现Runnable 接口的类就是任务类(任务类不一定是实现Runnable接口的类)。

实现Runnable 接口,重写run()方法,run方法的返回值只能是 void
任务类就是表示这个类要做什么,run是任务执行的入口,可以在run中完成任务也可以run调用其他方法完成任务。
run 方法不会产生一个线程,必须把任务附着到线程上,线程才会执行任务。
有时run中通常存在某种形式的循环,一直等待着接收信息准备执行任务,多见于服务器。

2.Thread类

构造器接收一个 Runnable 对象,当线程启动时会调用Runnable对象的run方法。

Thread(Runnable target) 或者 传入一个string 作为线程的名字 Thread(Runnable target,String name)
调用Thread对象的start( )方法启动线程,它会自动调用Runnable的run( )方法。
start方法调用之后会迅速返回,执行main线程后面的代码,任务在启动的线程中继续执行。
Thread对象和其他对象不同,Thread对象启动线程后,线程任务未完成之前,即run 方法未退出之前,垃圾回收器不会回收它。
常用的一个方法currentThread() 返回当前线程对象的引用。

3.Executor

执行器,用来管理Thread对象,只有一个方法 execute(Runnable command) 执行提交的任务。
这是一个异步执行框架可以控制线程的启动、执行和关闭,可以简化并发编程的操作。

ExecutorService 线程池,该接口实现了 Executor 他知道如何根据恰当的上下文来执行Runnable对象,即根据线程环境,类型有顺序有条件的启动。
线程的创建代价非常高昂,最好是提前创建几个线程然后一直持有它,当执行完任务后不销毁而是重新放回线程池内等待执行下一个任务。
调用execute方法可以往线程池中添加任务,这些任务由线程池中的线程去执行。
调用 shutdown 之后可以阻止线程池再加入线程。
Executors 是一个工具类,他可以产生ExecutorService对象和一些线程相关的其他对象。  
newCachedThreadPool() 创建一个线程池
newFixedThreadPool(int i) 也可以创建一个固定线程数的程序池
newSingleThreadExecutor() 创建一个没有限制的队列池,放入的任务会排队执行,执行完一个任务才开始执行下一个任务。
线程池中的线程在可能情况下都会被复用,有个线程执行完一个任务会接着执行另一个任务。

4.从任务中产生返回值

Runnable 是执行工作的独立任务,他不能有任何返回值。
如果想要任务完成时可以返回一个值,那么创建任务类时不用继承Runnable 接口 而是实现 Callable 接口。

Callable 和 Runnable 接口使用方法一样 V call()执行任务 并且返回一个V类型的值
ExecutorService 的 submit() 会返回一个Future 对象。

5.休眠

终止任务一段时间

可以使用TimeUtil.MILLISECONDS.sleep(1000); 休眠1000毫秒,也可以使用其他时间单位,或者使用Thread.sleep(1000),单位只能是毫秒
对sleep调用要抛出InterruptedException异常并且在run,call 方法中捕获,异常不能跨线程被捕获。

6.优先级

线程的优先级将线程的重要性传递给了调度器,调度器
CPU 处理线程集的顺序不一定,但调度器倾向于让优先级高的线程执行。倾向于并不是一定。

优先权不会导致死锁,优先权低的只是执行频率较低,并不是不执行。
绝大多时间里所有线程应该使用默认优先级,试图操纵线程的优先级让其先执行往往的不到效果,CPU不一定按优先级执行。
Thread.currentThread().setPriority(int i) i 可以是1-10,未设置默认5,一般可以设置MAX_PRIORITY 最大10,或者MIN_PRIORITY最小1,NORM_PRIORITY中间5。
设置优先级一般在run 任务中第一句设置。
Thread.yield() 建议具有相同优先级的其他线程执行,这只是一种建议,无法确保一定执行。

7.后台线程(守护线程)

daemon, 程序进程在运行的时候在后台提供一种通用服务的线程,并且不属于不可获取,程序可以没有后台线程,有时候需要后台线程提供一些服务。

一个程序(进程)在运行的时候如果所有非后台线程结束,该程序也就终止了,同时会杀死所有后台线程,不管后台线程是否执行完毕。
程序只要有一个非后台线程没有结束,程序就不会结束,main线程就是一个非后线程。
在一个线程调用start()之前调用 setDaemon(true) 可以设置该线程为后台线程。
isDaemon() 判断一个线程是否为后台线程.
后台线程中创建的任线程都自动设置为后台线程。
所有非后台线程结束后,后台线程立刻被杀死即使有finally语句也不会执行。

8.ThreadFactory

ThreadLocal:提供线程内部的局部变量,不同的线程之间不可以互相干扰,这种变量只在线程的声明周期中起作用
总结:线程并发 传递数据 线程隔离

该接口可以设置线程的属性,仅有一个方法Thread newThread(Runnable r)

实现ThreadFacory接口可以定制有由Executors创建的 线程池中的所有线程的属性。
由Executors创建线程池的静态方法都被重载为一个可以接收ThreadFactory对象

 ExecutorService service = Executors.newCachedThreadPool(new DaemonThread(),);
        service.submit(new Prints());


class DaemonThread implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        Thread t  = new Thread(r);
        t.setDaemon(true);
        return t;
    }
}

9.代码的变体

一般都是任务类实现Runable接口,再传递给线程Thread启动,有时也可以用别的方法来替换这种方式。
任务类不一定要从Runnable或者Callable接口实现,只要有run 或者call方法 并且线程能够执行这两个方法中,他们就统称任务类。

1、 任务类直接从Thread继承:任务类直接继承Thread,在构造器中调用start(),当创建任务类对象时就启动了线程并执行任务

lass SimpleThread extends Thread{
    SimpleThread(){
        start();
    }
    @Override
    public void run() {
        System.out.println("继承Thread的任务类");
    }
}

2、 任务类实现Runnable,内置Thread对象并传入任务类自己的引用this,并在构造器中启动start()

class SelfManaged implements Runnable{
    SelfManaged(){
        thread.start();
    }
    Thread thread = new Thread(this);
    @Override
    public void run() {
        System.out.println("内置Thread的任务类");
    }
}

3、注意1、2都是在构造器中启动线程,这意味着这个任务对象还未完全稳定线程就启动了,有可能另一个线程会访问这个不稳定的对象,这样就存在隐患。显示创建Thread就会存在这种问题,这也是优先使用Executor的原因。
4 可以通过内部类或者匿名内部类将线程代码隐藏在类中或者方法中。

10.加入一个线程

1、 A线程调用B线程的join()方法,则A线程会被挂起直到B线程执行被中断或者正常结束再继续执行A。
2、 可以调用interrupt()中断一个线程
调用interrupt()中断一个线程后,此时线程会设置中断标志位 true
如果线程处于阻塞状态被中断那么会抛出InterruptedException异常,此异常被捕获之后会清理中断标志位。
调用isInterrupted() 可以查看中断标志位状态。中断标志位在其他地方也有用,某些线程可能会检查其他线程的中断状态。

11.捕获异常

一般无法捕获从线程逃逸出去的异常,run只能捕获run方法catch中 的异常其他异常无法捕获,一但异常逃出run方法那么会直接输出到控制台。

由于Runnable的run方法没有throws 异常所以run中没被catch捕获的异常不会抛给上一级所以就丢失了,而Callable 中 call throws Exception 所以抛出异常后被上一级捕获, 通过Future get结果是可以的到异常信息。
需要捕获逃出的异常可以修改Executor生产线程的方式来解决。
让线程具备捕获逃出run方法的异常就要使用ThreadFactory,让一个线程经过ThreadFactory包装后具有捕获异常的方法。
ThreadFactory中的Thread对象调用setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 方法可以设置 当出现未捕获的异常导致突然中止时 自动调用哪个异常处理程序去处理异常。需要将实现的异常处理对象当做参数传入。可以通过getUncaughtExceptionHandler() 的到这个异常处理程序的对象。
Thread.UncaughtExceptionHandler是Thread类的一个内部接口,用来创建那些 处理未被捕获的异常的对象。
可根据逃逸出的异常逐个设置处理程序,也可以setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 设置一个为默认处理程序。当专有处理程序没有时自动调用默认处理程序。默认处理程序不用使用ThreadFactory包装直接在当前线程设置即可

3.共享受限资源

多个线程同时访问同一资源(实例变量,类变量),有可能某个线程恰好在资源“不恰当”的状态下问了它,自然的到的结果也是不正确的。
原子性是指CPU在执行指令集的过程中不能发生上下文切换,易变性指变量的变化对所有线程可见,并且JVM对该变量的操作不能发生指令重排。
原子性

原子性操作是指:一旦该操作开始,则该操作一定可以完成。原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,原子操作可以保证操作不被中断。可以对long或者double变量使用volatile关键字,它们就会获得原子性。原子操作可以由线程机制来保证其不被中断。volatile还确保了应用中的可视性,该关键字会强制所有对被该关键字修饰的变量的读取操作都是在主存中进行的。如果一个变量被声明为volatile的,那么只要对这个变量进行了写操作,则该值会立即写回到主存中,所以此后所有的读操作读取的都是最新的值。原子性不能确保在其他线程中读到的值是最新的,要想保证其他线程读取的值是最新的,则就必须进行同步操作,即把更新后的值写回到主存中。所以多线程要保证同步问题,而不是是否是原子操作的问题,两者之间没有关系。
原子类中有AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供下面形式的原子性条件更新操作:

boolean compareAndSet(int expect, int update);
final int getAndSet(int newValue);
...

你如果可以熟练使用这些方法,就可以借助这些方法来消除synchronized关键字。
线程中断

测试结果: 不能结束子线程
实际上这个Interrupt() 方法
设置了 isInterrupted 布尔值

如果线程中有wait() 等待 或者 sleep 休眠
这时 调用 interrupt方法
会抛出一个异常 InterruptedException
并且清除中断状态

如果线程中没有等待 或者 休眠
这时调用interrupt方法
会设置中断状态(true/false的改变)

一、正常运行状态的线程被interrupt中断

线程中断,Thread的interrupt方法,当一个线程正常运行时(非休眠),调用线程对象的interrupt方法,将不会影响线程的执行,线程既不会停止,也不会抛出异常。只是设置一个中断标志,线程继续执行。

二、休眠状态的线程被interrupt中断

当线程正在sleep方法处休眠阻塞,调用它的Thread对象的interrupt方法,代码将会抛出InterruptedException异常,进入InterruptedException代码块

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值