java编程思想并发

21.1.2java线程作用

1.可以创建模拟真实的事件,使每个元素都有其自己的处理器并且都是独立的任务
2.解决这个问题典型的方式是使用协作多线程。
java的线程机制是抢占式的,这表示调度机制会周期性的的中断线程,将上下文切换到另一个线程。
3.理解并发可以掌握基于消息机制的架构,这些架构在创建分布式系统时是更主要的方式
4.线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。虽然不知道如何切分CPU时间(比如多个cpu)的具体情况,但线程机制会做这些事情,使程序员从这个层次抽身出来,它是一种建立透明的、可扩展的程序的方法

21.2基本的线程机制

21.2.1使用线程的编写方式

1.一个类实现Runnable接口变成任务类,然后再new Thread类的构造方法中加入这个任务类,最后start();
2.new 一个Thread类,重写run方法;
3.一个类实现Runnable接口变成任务类,利用Executor这个执行器管理,常用方法是ExecutorService.execute(任务类对象)
4.一个类实现Callable接口,在ExecutorService.submit()中以参数的形式传入调用。这个任务有返回值。

21.2.3执行器——Executor

Executor在客户端和任务执行之间提供了一个间接层;他是启动任务的优选方法。
Executor可以创建的线程池:
1.Executor.newCachedThreadPool()——创建所需数量相同的线程,这个是首选
2.Executor.newFixedThreadPool(number)——固定数量的线程池,预先创建,这可以节省时间
3.Executor.singleThreadExecutor()——多个任务按照提交的顺序执行,每个任务都会在下一个任务执行前执行结束;比如文件系统,使用这个确保任意时刻任何线程中只有一个任务在运行,不需要在共享资源上解决同步问题。

21.2.6优先级

设置优先级:
Thread.currentThread().setPriority(优先级)
可移植的优先级:MAX _PRIORITY、NORM_PRIORITY、MIN_PRIORITY

21.2.9编码的变体,可以有5种内部类写法隐藏线程代码

1.InnerThread1——类中有一个完整的内部类,此内部类继承Thread,外部类构造方法中创建内部类,内部类的构造方法调用start()
2.InnerThread2——类中有一个Thread成员变量,构造方法中初始化这个Thread成员变量,并调用start()
3.InnerRunnable1——类中有一个完整的实现Runnable接口内部类,内部类构造方法中new一个Thread对象,传入此Runnable接口对象this,并调用start(),外部类构造方法创建这个内部类对象。
4.InnerRunnable2——外部类构造方法中new 一个Thread对象,以匿名内部类的方式创建Runnable对象,并传入Thread对象构造方法,调用start()
5.ThreadMethod——一个方法中调用线程的start()方法

21.2.10任务和线程

从概念上讲,我们希望创建独立于其他任务运行的任务,因此我们应该能够定义任务,然后说“开始”,并且不用操心其细节。但是在物理上,创建线程可能会代价高昂,因此你必须保存并管理他们。这样,从实现的角度看,将任务从线程中分离出来是很有意义的。

21.2.11 加入线程

我常常困惑join方法到底是调用完谁在谁的前面执行,直到我看见join()方法没有参数,它是线程实例调用的,比如有两线程A、B他们都有各自的join()方法,在A的run方法中执行new B().join(),那么A会暂停,直到B线程结束后再执行A

21.2.14 捕获异常

线程直接在调用创建线程的线程上,是不能捕获新创建的线程的异常的,如何捕获呢:
1.在创建线程池的时候,在构造参数中传入一个继承自ThreadFactory对象,之后在这个线程池运行的任务抛异常会用这个处理,这个对象会实现一个抽象方法public Thread newThread(Runnable r)方法,在这个方法中加入如下代码:
Thread t = new Thread®;
t.setUncaughtExceptionHandler(一个实现Thread.UncaughtExceptionHandler接口的对象,简称线程未捕获异常处理器);
return t;
实现这个线程未捕获异常处理器的对象,会实现一个抽象方法public void uncaughtException(Thread t,Throwable e){},可以在方法体重直接写
System.out.println(e);
2.在线程池创建前,调用
Thread.setDefaultuncaughtExceptionHandler(线程未捕获异常处理器);
注意:如果同时存在1方法优先于2方法捕获异常

21.3共享受限资源

synchronized——锁对象,锁类

对象锁:
将域设置为private,否则synchronized关键字就不能防止其他任务之间访问域,会产生冲突。
synchronized是用在普通方法上的:synchronized void f(){…},锁的是对象
一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了对象上的另一个方法,就会发生这种情况。
类锁:
synchronized static 可以在类的范围内防止static数据的并发访问,锁的是类;
什么时候应该同步呢?
如果正在写一个变量,它可能接下来被另一个线程读取,或者正在读一个上一次已经被另一个线程写过的变量,那么必须使用同步,并且线程要使用相同的锁。
多个方法在处理共享资源时,必须同步所有相关方法;

synchronized临界区

就是同步代码块;
在synchronized(this)这种方式中,如果获得该对象的锁,那么该对象的其他同步方法和临界区就不能被调用了;

显示的lock对象——ReentrantLock

用法:先创建ReentrantLock对象,紧接着调用lock()方法,再紧接着写一个try-finally语句,unlock()方法必须放置在finally语句中,return语句必须在try子句中出现,以确保unlock()不会构造发生,把数据暴露给第二个任务。
与synchronized比较的优点:
1.如果抛异常,synchronized没有机会做任何清理工作,而ReentrantLock可以在finally语句中维护系统状态(但处理异常来使系统恢复稳定状态,也不是java推荐,只是有这个功能);
2.可以尝试获取锁且最终获取会失败;
3.可以尝试获取锁一段时间,然后放弃它;
优点2、3是你可以尝试获取但是最终未获取锁,这样这一判定其他人获取了这个锁,那么你就可以决定去执行其他一些事情。

原子性与易变性

一个不正确的知识是“原子操作不需要进行同步控制”。原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完毕,或者理解为切换到其他线程前执行完毕。
不要考虑用原子性代替同步除非你能做到:“可以编写用于现代微处理器的高性能JVM”
除了long和double,所有基本类型都有原子性,因为long跟double型占64位,而JVM可以将他们的读取和写入当作两个分离的32位操作来执行,这样如果长生了一个在读取和写入中间的上下文切换,导致不同任务可以看到不同结果。可以使用volatile来回到原子性

volatile的可视性

在多处理系统中,可视性远比原子性问题多得多。一个任务作出的修改,即使是原子性的,对其他任务也可能是不可视的,例如,修改只是暂时性的储存在本地处理器的缓存中,而读取操作是发生在主存中。
如果任务所做的任何写入操作都是针对这个任务来说的,那么这个域是可视的,也不需要将其设为volatile。
解决这个问题可以使用volatile,它的原理是即便使用了本地处理器缓存volatile域也会立即写入到主存中。
同步也会导致向主存中刷新,所以如果一个域不是synchronized方法或语句块防护,那就不必将其设置为volatile。当然锁的是在并发情况下;

一个线程不安全的重要例子

public class SerialNumberGenerator{
	private static volatile int serialNumber = 0;
	public static int nextSerialNumber(){
		return serialNumber ++;
	}
}

这个例子在并发中也是不安全的,因为递增在java中不是原子性操作,它包括一个读操作和一个写操作,不信可以javap -c SerialNumberGenerator反编译看一下:
在这里插入图片描述
基本上,如果一个域可能被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么就应该将这个域设为volatile。如果将一个域设为volatile,那么它就会告诉编译器不要只想任何移除读取和写入操作的优化,这些操作的目的是用线程中局部变量维护队这个域的精确同步。实际上,读取和写入都是针对内存的,而去没有被缓存。但是volatile并不能对递增不是原子性操作这一事实产生影响。所以上边的例子即使加入volatile这个程序也不是线程安全的。

关于synchronized方法被继承后重写

synchronized不属于方法特征前面的组成部分,所以可以在覆盖方法的时候取消;

线程的状态

1.新建——可以转换为运行状态和阻塞状态
2.就绪——只要调度器分配时间片给线程,它就可以运行
3.阻塞——调度器将忽略线程不会分配给线程任何cpu时间片;直到转换为就绪状态,它才可能执行操作
4.死亡

进入阻塞有如下原因

1.通过sleep()是任务进入休眠状态
2.通过wait()是线程挂起,知道notify()或notifyAll()
3.任务在等待输入/输出完成
4.在试图调用同步控制方法,但是锁被其他任务获得

两种不能中断阻塞的情况

1.同步控制块
2.IO流输入输出
解决办法,同步控制块,用ReentrantLock可以中断;IO流输入输出中断可以关闭IO底层资源;

21.5线程之间的协作

21.5.1错失的信号

T1:
synchronized(sharedMonitor){
	<setup condition for T2>
	sharedMonitor.notify();
}
T2:
while(someCondition){
//point1
	synchronized(sharedMonitor){
		sharedMonitor.wait();
	}
}
  • < setup condition for T2>是防止T2调用wait()的一个动作,当然前提是T2还没有调用wait()。
  • 假设T2对someCondition求值并发现其为true.在Ponit1,线程调度器可能切换到T1,而T1将执行其设置,然后调用notify()。当T2得以继续执行时,此时对于T2来说,时机已经太晚了,一直与不能意识到这个条件已经发生了变化,因此会盲目的进入wait(0.此时notify()将错失,而T2也将无限制地等待这个已经发生过的信号,从而产生死锁。

21.5.2notify()与notifyAll()

  • 使用notify()而不是notifyAll()是一种优化。使用notify()时,在众多等待同一锁的任务中只有一个会被唤醒,因此如果希望使用notify(),就必须保证被唤醒的是恰当的任务.另外,为了使用notify(),所有的任务必须使用相同的条件,因为如果你有个多个任务在等待不同的条件,那么你就不知道是否唤醒了恰当的任务。如果使用notify(),当条件发生变化时,必须只有一个任务能够从中受益。最后,这些限制对所有可能存在的子类都是必须总是起作用的。如果这些规则中有任何一条不满足,那么你就必须使用notifyAll而不是notify()。

21.5.3生成者与消费者

  • 使用显示的lock和Condition对象
    使用互斥并允许任务挂起的基本类是Condition,你可以通过在Condition上调用await()来挂起一个任务。当外部条件发生变化,意味着某个任务应该继续执行时,你可以通过调用sign()来通知这个任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务(与使用notifyAll()相比,signalAll()是更安全的方式)
  • Condition对象可以通过Lock创建
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

21.5.4生产者-消费者与队列

wait()和notifyAll()方法以一种非常低级的方式解决了任务互操作问题,即每次交互时都握手,可以瞄向更的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时刻只允许一个任务插入或移除元素。

  • 父接口BlockingQueue
  • 实现类1:linkedBlockingQueue——无界队列
  • 实现类2:ArrayBlockingQueue——有界队列
  • 使用BlockingQueue的实现类对象时,只需要正常编写代码就可以,只是在放入和取出的时候用put()和take()方法即可。

21.5.5任务间使用管道进行输入/输出

  • 可以通过管道在线程之间进行通信。提供线程功能的类库以“管道”的形式对线程间的输入/输出提供了支持。他们在java输入/输出类库中的对应物就是PipedWriter和PipedReader。这个模型可以看做是“生产者-消费者 ”问题的变体,这里的管道就是一个封装好的解决方案。
  • PipedReader和PipedWriter是可以通过线程池的shutdownNow()关闭的,它与普通I/O流不同,普通的I/O流不能通过线程池的shutdownNow()关闭。

21.6 死锁

  • 死锁必须同时满足四个条件,才会死锁
    1.互斥条件。任务使用的资源至少有一种资源是不能共享的
    2.至少有一个任务它必须有一个资源并当前正在等待获取被别的任务持有的资源
    3.资源不能被抢占,任务必须把资源释放当作普通条件
    4.必须循环等待,这时一个任务等待其他任务所持有的资源,后者有等待另一个任务持有的资源,这样一直等待下去,直到一个任务等待第一个任务持有的资源。使得大家都锁住
  • 科学家加餐死锁案例
    有5个科学家,5只筷子,做座一个圆圈,他们都是先拿起右边的筷子,再拿起左边的筷子,当有两双筷子时才能吃饭,当只拿起右边的筷子时会等待,直到左边的筷子能拿到,最后一个人(第五个人)的右手筷子是第一个人的左手筷子(他们坐成一个圆)。吃完饭就把筷子放回去,然后去思考,思考一会后再去吃饭,吃饭和思考交替进行;
    当思考的时间非常短时,他们会频繁的吃饭,导致筷子产生竞争,当所有人都拿着右手的筷子等待左手的筷子时,就产生死锁了(他们是一个圆)
    解决办法之一:可以把最后一个人(或任意一人)拿筷子的顺序改成先左后右,那么就不会有死锁了
  • 练习题31中,引用LinkedBlockingQueue,把5根筷子都放入LinkedBlockingQueue中,每次科学家从这个阻塞队列里面拿筷子,是否会避免死锁?
    通过跑习题答案代码,把思考的时间设为零,很快结果显示也会死锁,虽然LinkedBlockingQueue能很好的互斥资源,但是他并不能完全消除每个科学家都拿起同一方向的筷子,等待另一个方向筷子的可能性。

21.7.1CountDownLatch

可以向CountDownLatch对象设置一个初始计数值,任何在这个对象上调用wait()的方法都将阻塞,直至这个计数器值到达0。而其他任务调用相同的CountDownlatch对象上的CountDown()来减小这个计数值。CountDownLatch被设计只触发一次,这个值不能被重置。重置版本是CyclicBarrier。

21.7.3 DelayQueue、PriorityBlockingQueue、ScheduledThreadPoolExecutor、Semaphore、Exchanger

  • DelayQueue——一个无界的BlockingQueue,用于放置实现了Delayed接口的对象(Delayed接口又继承Comparable接口),其中的对象只能在其到期时才能从队列中取走。它是有序的,估计使用Comparable接口排的序。直接打印这个队列是无序的,take()方法取元素的时候才会找到最先到期的元素,是优先级队列的一种变体
  • PriorityBlockingQueue——一个基础的优先级队列,对象是按照优先级顺序从队列中出现的任务,也用take(),元素也要实现Comparable接口,否则抛异常,PriorityBlockingQueue本身是无序,在调用take()方法的时候会排序。
  • ScheduledThreadPoolExecutor——每个任务在预定的时间运行。通过schedule()(运行一次)或者scheduleAtFixedRate()(每隔规则的时间重复执行任务)
  • Semaphore——简称计数信号量,允许多个任务同时访问这个资源,它的构造函数new Semaphore(SIZE,true),允许有SIZE个共享次数,它的签入、签出调用的方法是acquire()和release();每次acquire执行一次,共享次数减一,release()后共享次数加一。
  • Exchanger——典型应用场景是:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象。通过Exchanger,可以有更多的对象在创建的同时被消费。
    创建一个生产者任务和一个消费者任务,他们都有一个共同的Exchanger,在各自的Exchanger中调用exchanger(有参函数)方法,会阻塞直到有对象消费;

21.8仿真

  • 仿真中使用并发,仿真的每个构件都可以为其自身的任务,这使得仿真更容易编程。
  • 仿真中使用队列的一个好处就是,反转控制,把原来的并发需要注意的,比如synchronized方法或者各种锁问题,抽离出来,交给队列来处理,这样我们可以像编写普通程序一样编写代码;用处理消息(就是一个方法)方式来对待并发;这样构建出健壮的并发系统的可能性就会大大增加。
  • 还有一个启发:一个资源可能会被多个线程使用,因此我们需要以明显的方式使其成为线程安全的。把这种方式可以描述为:在公园中,你会在陡峭的坡路上发现一些保护围栏,并且可能会发现标记声明:“不要依靠围栏。”当然,这条规则的真是目的不是要阻止你借助围栏,而是防止你跌落悬崖。但是“不要依靠围栏”与“不要跌落悬崖”相比,是一条遵循起来要容易得多的规则。

21.9性能调优

比较速度synchronized,lock,Atomic

  • 当竞争的任务比较少时,synchronized比Lock和Atomit更高效(当共享资源时被读写两个方面的任务数量分别小于40万),当竞争的任务比较多时,synchronized比Lock和Atomic更高效(当共享资源时被读写两个方面的任务数量分别大于80万时),Lock和Atomit比synchronized更高效——java编程思想作者的机器上结果,自己的机器实际不一定。
  • 很明显,synchronized关键字所产生的代码,与Lock所需的“加锁-try/finally-解锁”惯用法所产生的代码相比,可读性提高了许多,这就是为什么Java编程思想中主要使用synchronized关键字的原因。
  • 代码阅读的次数远超过被编写的次数。在编程时,与其他人交流相对于与计算机交流而言,要重要得多,因此代码的可读性至关重要。因此,以synchronized关键字入手,只有性能调优是才替换为Lock对象,具有实际意义。
  • Atomic对象只有在非常简单的情况下才有用,这些情况通常包括你只有一个要被修改的Atomic对象,并且这个对象独立于其他所有对象。更安全的做法是:以更加传统的互斥方式入手,只有在性能方面的需求能够明确指示时,再替换为Atomic。

21.9.2 免锁容器

  • 免锁容器的通用策略是:对容器的修改可以与读取同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独的副本(有的时候是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。
  • 免锁容器:
    CopyOnWriteArrayList——较SynchronizedArraylist,速度快
    CopyOnWriteSet——使用CopyOnWriteArrayList来实现其免锁行为。
    ConcurrentHashMap——较SynchronizedHashMap,速度快
    ConcurrentLinkedQueue

21.9.3 乐观加锁

  • 尽管Atomic对象将执行像decrementAndGet()这样的原子性操作,但是某些Atomic类还允许执行所谓的“乐观加锁”。这意味着执行某项计算时,实际上没有使用互斥,但是在这项计算完成,并且你准备更新这个Atomic对象时,需要使用comparaAndSet(oldValue,newValue)方法,返回boolean;这个方法将旧值和新值一起提交给这个方法,如果旧值与它在Atomic对象中发现的值不一致,那么这个操作就失败——这意味着某个其他的任务已经于此操作执行期间修改了这个对象。
  • 如果compareAndSet()操作失败会发生什么?这正是棘手的地方,也是你在应用这项技术时的受限之处,即只能针对能够吻合这些需求的问题。如果compareAndSet()失败,那么就必须决定做些什么,这是一个非常重要的问题,因为如果不能执行某些恢复操作,那么就不能使用这些技术,从而必须使用用传统的互斥(synchronized、Lock);

21.9.4 读写锁——ReadWriteLock

  • ReadWriteLock对向数据结构相对不频繁地写入,但是有多个任务经常读取这个数据结构的这类情况进行了优化。ReadWriteLock使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直到这个写锁被释放为止。
  • ReadWriteLock是否能够提高程序的性能是完全不可确定的,它取决于诸如数据被读取的频率与修改的频率相比较的结果,读取和写入操作的时间,有多少线程竞争以及是否在多处理器上运行等因素。最终,唯一可以了解ReadWriteLock是否能够给你的程序带来好处的方式就是用实验来证明。

性能调优总结

  • 通过免锁容器,乐观锁,读写锁,并没有具体说明他们之间谁更优秀,只是说这是synchronized和Lock的调优版;具体哪个更好要实践;
  • 比较synchronized,lock和Atomic的性能,直接上我本地执行结果吧:
    Warmup
    BaseLine : 26441173
    ============================
    Cycles : 50000
    BaseLine : 16705954
    synchronized : 75387393
    Lock : 62490711
    Atomic : 19972922
    synchronized/BaseLine : 4.51
    Lock/BaseLine : 3.74
    Atomic/BaseLine : 1.20
    synchronized/Lock : 1.21
    synchronized/Atomic : 3.77
    Lock/Atomic : 3.13
    ============================
    Cycles : 100000
    BaseLine : 35186613
    synchronized : 176808307
    Lock : 101511639
    Atomic : 28749973
    synchronized/BaseLine : 5.02
    Lock/BaseLine : 2.88
    Atomic/BaseLine : 0.82
    synchronized/Lock : 1.74
    synchronized/Atomic : 6.15
    Lock/Atomic : 3.53
    ============================
    Cycles : 200000
    BaseLine : 72937783
    synchronized : 362912569
    Lock : 191037966
    Atomic : 56978576
    synchronized/BaseLine : 4.98
    Lock/BaseLine : 2.62
    Atomic/BaseLine : 0.78
    synchronized/Lock : 1.90
    synchronized/Atomic : 6.37
    Lock/Atomic : 3.35
    ============================
    Cycles : 400000
    BaseLine : 125146045
    synchronized : 795358837
    Lock : 387549478
    Atomic : 117009415
    synchronized/BaseLine : 6.36
    Lock/BaseLine : 3.10
    Atomic/BaseLine : 0.93
    synchronized/Lock : 2.05
    synchronized/Atomic : 6.80
    Lock/Atomic : 3.31
    ============================
    Cycles : 800000
    BaseLine : 276322325
    synchronized : 1481222942
    Lock : 780410346
    Atomic : 214803740
    synchronized/BaseLine : 5.36
    Lock/BaseLine : 2.82
    Atomic/BaseLine : 0.78
    synchronized/Lock : 1.90
    synchronized/Atomic : 6.90
    Lock/Atomic : 3.63
    结果如下:synchronized在任何时候都比lock和Atomic慢,Atomic用起来注意方面比较多,建议在提高性能方面首先用lock

21.10 活动对象

  • 有一种可替换的方式被称为活动对象或行动者。之所以称这些对象是“活动的”,是因为每个对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中的一个。因此有了活动对象,我们就可以串行化信息而不是方法,这意味着不再需要防备一个任务在其循环的中间被中断这种了。
  • 听起来很复杂,作者就是把所有的方法都会生成一个callable任务对象,用一个newSingleThreadExecutor来执行,每次只能执行一个,newSingleThreadExecutor每次用submit方法返回多线程的future对象,调用future的get()方法获取值,又说这种几乎可以立即返回
  • 为了能够在不经意间就可以防止线程之间的耦合,任何传递给活动对象方法调用的参数都必须是只读的其他活动对象,或者简单叫做“不连接对象”,即没有连接任何其他任务的对象,有了活动对象:
    1.每个对象都可以拥有自己的工作器线程。
    2.每个对象都将维护对它自己的域的全部控制权
    3.所有在活动对象之间的通信都将以在这些对象之间的消息形式发生。
    4.活动对象之间的所有消息都要排队
  • 这些结果很吸引人。由于从一个活动对象到另一个活动对象的消息只能被排队时的延迟所阻塞,并且因为这个延迟总是非常短且独立于任何其他对象的,所以发送消息实际上是不可阻塞的,由于一个活动对象系统只是经由消息来通信,所以两个对象在竞争调用另一个对象上的方法时,是不会被阻塞的(发送消息肯定能发送过去,至于什么时候执行,在队列里等着,反正我发消息(调用活动对象的方法)不会阻塞),而这意味着不会发生死锁。这是一种巨大的进步,而你也不必操心应该如何同步方法。同步仍旧会发生,但是它通过将方法调用排队,使得任何时刻都只能发生一个调用,从而将同步控制在消息级别上发生。
  • 遗憾的是,如果没有直接的编译器支持,上面这种编码方式实在太过于麻烦了。但是,这在活动对象或行动者领域,或者更有趣的被称为基于代理得编程领域,确实产生了进步。代理实际上就是活动对象,但是代理系统还支持跨网络和机器的透明性。如果代理编程最终称为面向对象的继承者,作者一点也不会觉得惊讶,因为它把对象和相对容易的并发解决方案结合了起来。

总结

  • 明白什么时候应该使用并发,什么时候应该避免使用并发是非常关键的。使用它的原因主要是:
    (1)要处理很多任务,他们交织在一起,应用并发能够更有效地使用计算机
    (2)要能更好地组织代码
    (3)要更便于用户使用
  • 线程的一个额外好处是它们提供了轻量级的执行上下文切换(大约100条指令),而不是重量级的进程上下文切换(要上千条指令)。因为一个给定进程内的所有线程共享相同的内存空间,轻量级的上下文切换只改变程序的执行序列和局部变量。进程切换必须改变所有内存空间
  • 线程的主要缺陷有:
    (1)等待共享资源的时候性能降低
    (2)需要处理线程的额外CPU花费
    (3)糟糕的程序设计导致不必要的复杂度
    (4)有可能产生一些病态行为,如饿死、竞争、死锁和活锁
    (5)不同平台导致的不一致性。比如,作者在编写书中的一些例子时发现,竞争条件在某些机器上很快出现,但在别的机器上根本不出现。如果你在后一种机器中做开发,那么当你发布程序时就要大吃一惊了。
  • 不管在使用某种特定的语言或类库时,线程机制看起来是多么的简单,你都应该视其为魔法。总有一些你最不想碰见的事物会反噬你一口。哲学家用餐问题之所以很有趣,就是因为它可以进行调整,使得死锁极少的发生,这给了你一个印象:每件事物都是美好的。
  • 通常,使用线程机制需要非常仔细和保守。如果你的线程问题变得大而复杂,那么就应该考虑使用Erlang这样的语言,这是专门用于线程机制的集中函数形语言之一。你可以将这种语言用于程序中要求使用线程机制的部分,前提是你经常要使用线程机制,货值线程问题复杂度足以促使你这么做
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值