并发
顺序编程:程序中的所有事物在任意时刻都只能执行一个步骤
-
并发的多面性
- 并发通常是提高运行在单处理器上的程序的性能
- 阻塞:如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。
- 使用并发编写程序,当一个任务阻塞时,程序的其他任务可以继续执行,因此这个程序可以保持继续向前执行
- 实现并发最直接的方式是在操作系统级别使用进程。进程是运行在他自己地址空间内自包容的程序。
- 操作系统通常会将进程相互隔离开,因此他们不会相互干涉。编写多线程程序最基本的困难在于协调不同的线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。
- Java在顺序型语言的基础上提供对线程的支持。线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性。
- Java的线程机制是抢占式的,这表示调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动他们的任务
- 协作式系统中,每个任务都会自动的放弃控制。协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉很多,并且对可以同时执行的线程数量在理论上没有限制。
-
线程的状态
-
基本的线程机制
-
并发编程可以将程序划分为多个分离、独立运行的任务。通过使用多线程机制,子任务中的每一个都将由执行线程来驱动。
-
线程就是在进程中的一个单一的顺序控制流。单个进程可以拥有多个并发执行任务。线程简化了在单一程序中同时交织在一起的多个操作的处理
-
实现Runnable接口并编写run()方法,使得该任务执行命令。任务的run()方法总会有某种形式的循环,使得任务一直循环直至不再需要,所以要设定跳出条件。从Runnable导出一个类时,他必须具有run()方法。但它不会产生任何内在的线程能力。要实现线程能力,需要将一个任务附着到线程上。将Runnable
-
对象提交给Thread构造器,使其转变为工作任务。Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动新任务
-
使用Executor(java.util.concurrent)管理Thread对象。Executor在客户端和任务执行间提供了一个间接层,与客户端执行任务不同,这个中介对象将执行任务。Executor允许管理异步任务的执行,而无需显示的管理线程的生命周期
1.ExecutorService exec = Executors.newFixedThreadPool(5);//创建一个定长线程池,每当提交一个任务时就创建一个线程,直到线程池的最大数量,这时线程池的规模将不再变化 2.ExecutorService exec = Executors.newSingleThreadExecutor();//就是线程数量为1的newFixedThreadPool,如果向其提交了多个任务,那么这些任务将会排队,每一个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程 3.ExecutorService exec = Executors.newCachedThreadPool();//创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池的规模不受限。将为每一个任务创建一个线程 4.ExecutorService exec = Executors.newScheduledThreadPool(1);//创建一个定长线程池,支持定时及周期性任务执行 exec.excute();//线程池服务启动线程 exec.shoutdown();//线程池服务停止
-
shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程将继续运行在shutdown()被调用之前提交的所有任务。这个程序将在Executor中的所有任务完成之前尽快退出。
-
Runnable是执行工作的独立任务,但是不返回任何值。实现Callable接口可以在任务执行结束后返回一个值。Callable是一种具有类型参数的泛型,他的类型参数是从call()方法返回的值,而且必须使用ExecutorService.submit()方法调用它
-
submit()方法会产生Future对象,他用Callable返回结果的特定类型进行了参数化。可以使用isDone()来查询Future是否已经完成。任务完成后,调用get()方法来获取该结果。也可以在isDone()未检查的情况下直接调用get(),此时,get()将阻塞,直至结果准备就绪
-
休眠:影响任务行为可以调用sleep()方法,这将使任务中止执行给定的时间。
-
让步:yield()的调用是对线程调度(Java线程机制的一部分,可以将CPU从一个线程转移到另一个线程)的一种建议。建议具有相同优先级的线程可以运行
-
优先级:线程的优先级将该线程的重要性传递给了调度器。绝大多数时间,所有线程都应该以默认的优先级运行,试图操纵线程优先级通常是一种错误的行为。
getPriority();//读取当前线程的优先级 setPriority();//修改当前线程的优先级 Thread.currentThread();//获得该驱动该任务的Thread对象的引用 Thread.MIN_PRIORITY;//1 Thread.MAX_PRIORITY;//10 Thread.NORM_PRIORITY;//5
-
后台线程:指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止了,同时会杀死进程中所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会中止。
-
在线程启动之前调用setDaemon()方法,将其设置为后台线程。通过isDaemon()方法来确定线程是否为后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置为后台线程
-
在Java中,Thread类自身不执行任何操作,他只是驱动富裕他的任务
-
加入一个线程:一个线程在其他线程之上调用join()方法啊,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程在另一个线程t上调用join();此线程将被挂起,指导目标线程t执行完毕才恢复
-
在调用线程上调用interrupt()可以中断对join()方法的调用
-
捕获异常:Thread.UncaughtExceptionHandler是JDK5 的新接口,他允许在每个Thread对象上都附带一个异常处理器,Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用
-
-
共享受限资源
-
对于并发工作,需要使用某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况
-
防止这种冲突的方法就是当一个资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前无法访问该资源。
-
基本所有的并发模式再解决线程冲突问题时,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。因为锁语句产生了一种互斥的效果,所以这种机制常被成为互斥量。
-
Java提供关键字synchronized的形式,为防止资源冲突提供了内置支持,当任务要执行被synchronized关键字保护的代码片段时,他将检查锁是否可用,然后获取锁,执行代码,释放锁
-
所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,这是该对象其他synchronized方法只有等到前一个synchronized方法执行完毕后并释放了锁才能被调用。所以,对某个特定对象来说,其所有synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存
-
每个访问临界共享资源的方法都必须被同步,否则他们就不会正常的工作
//线程同步可分为以下几种 //对象方法同步(每个对象有一个线程同步锁与之关联,同一个对象的不同线程在同一时刻只能有一个线程调用A方法。) public synchronized void A(){ System.out.println(this); } //类所有对象方法同步(静态方法的线程同步锁对类的所有对象都起作用,即所有对象的线程在同一时刻只能有一个类的一个线程调用该方法。) public synchronized static void B(){ System.out.println(this); } //对象同步代码块(使用当前对象作为线程同步锁,同一个对象的不同线程在同一时刻只能有一个线程调用C方法中的代码块。只有获得this对象的锁,才可以进入同步代码块) public void C(){ synchronized(this){ System.out.println(this); } }
-
Lock对象:Lock对象必须被显示的创建、锁定和释放。因此,它与内建的锁形式相比,代码缺乏优雅性。
-
使用Lock对象时,unlock()必须放置在try-finally语句的finally()子句中,return语句必须要在try子句中出现,以确保unlock()不会过早的发生,从而将数据暴露给下一个任务。
-
Lock对象较于synchronized优点:(1)如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常,但是没有机会做任何的清理工作,以维护系统处于良好的状态。有了显示的Lock对象,可以使用finally子句维护系统在正确的状态(2)显式的Lock对象在加锁和释放锁方面,相对于内建的synchronized来说,赋予了更细粒度的控制力。
-
ReentrantLock允许尝试获取但最终未获取锁,这样如果其他任务获取这个锁,那么就可以去做其他任务,而不是一直等待直至这个锁被释放。
-
原子操作是不能被线程机制中断的操作,一旦操作开始,那么他一定可以在可能发生的“上下文切换“之前执行完毕。原子性可以应用于除long和double之外所有基本类型之上的”简单操作“
-
volatile关键字:确保了应用中的可视性。使用volatile声明的域,只要发生了写改动,会立即写入主内存中,所有读取该字段的值都会跟着改变,即使使用了本地缓存仍然会被改变
-
volatile不适用的情况:
- 一个域完全由synchronized方法或语句块防护,就不必使用volatile。
- 当一个域的值依赖于它之前的值时(递增)
- 如果某个域的值受到其它域的限制
使用volatile唯一安全的情况是类中只有一个可变的域。
第一选择是使用synchronized,这是最安全的方式
-
-
临界区:防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,通过这种方法分离出来的代码块称之为临界区
-
防止任务在共享资源上产生冲突的第二种方法是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。创建和管理线程本地存储可以由java.lang.ThreadLocal类实现
-
ThreadLocal对象通常当做静态域存储。在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容。get()方法将返回与其线程相关联的对象副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象
1、 T get()//返回此线程局部变量的当前线程副本中的值。 2、 protected T initialValue()//返回此线程局部变量的当前线程的初始值。 3、 void remove()//移除此线程局部变量当前线程的值。 4、 void set(T value)//将此线程局部变量的当前线程副本中的值设置为指定值。
-
任务进入阻塞状态:
- 通过调用sleep()使任务进入休眠状态。这种情况下,任务在指定时间内不会运行
- 调用wait()使线程挂起。直到线程得到notify()或notifyAll(),线程会进入就绪状态
- 任务在等待某个输入/输出完成
- 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获得锁
-
Thread类包含interrupt()方法,因此你可以中止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位
-
当在线程上调用interrupt()方法时,中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时。
-
-
wait、sleep、yield的区别
(1).yield:
正在运行的线程让出CPU时间片,让线程调度器运行其他优先级相同的线程。使用yield()处于可运行状态的线程有可能立刻又进入执行状态。
yield不释放对象锁
(2).sleep:
当前正在运行的线程进入阻塞状态,sleep是线程Thread类的方法,在sleep的时间内线程肯定不会再运行,sleep可使优先级低得线程得到执行的机会,也可以让同优先级和高优先级的线程有执行机会。
sleep的线程如果持有对象的线程锁,则sleep期间也不会释放线程锁,即sleep线程对象中其他synchronized的数据不能被别的线程使用。
sleep方法可以在非synchronized线程同步的方法中调用,因为sleep()不释放锁。
(3).wait:
正在运行的线程进入等待队列中,wait是Object的方法,线程调用wait方法后会释放掉所持有的对象锁
wait(), notify()和notifyAll()方法必须只能在synchronized线程同步方法中调用,因为在调用这些方法之前必须首先获得线程锁,如果在非线程同步的方法中调用时,编译时没有问题,但在运行时会抛IllegalMonitorStateException异常
-
线程之间的协作
-
wait()与notifyAll()
- wait使得一个正在执行的线程进入阻塞状态,wait也可以象sleep一样指定时间,但是常用的是无参数的wait()方法。notify和notifyAll唤醒处于阻塞态的线程进入可运行状态,通知他们当前的CPU可用。这三个方法都是Object中的方法,不是线程Thread的特有方法。
- wait()会在等待外部世界产生变化的时候将任务挂起,只有在notify()或notifyAll()发生时,这个任务才会被唤醒并去检查所发生的的变化。
-
notify()与notifyAll()
- 使用notify()时,在众多等待同一个锁的任务中只有一个被唤醒。如果要使用notiy(),就必须保证唤醒的是恰当的任务。另外,为了使用notify(),所有的任务必须等待相同的条件。如果使用notify(),当条件发生变化时,必须只有一个任务满足条件。
- 当notifyAll()因某个特定的锁而被调用时,只有等待这个锁的任务才会被唤醒
- waite (),notify()和notifyAll()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常
-
显式的Lock和Condition对象
- 使用互斥并允许任务挂起的基本类是Condition,可以在Condition上使用await()来挂起一个任务。当外部条件发生变化,意味着某个任务应该继续被执行时,使用signal()来通知这个任务,从而唤醒下一个任务。或者使用signalAll()来唤醒所有在Condition上被其自身挂起的任务。
-
-
同步队列
- 同步队列在任何时候都只允许一个任务插入或移除元素(java.util.concurrent.Blockingqueue接口)
- LinkedBlockingQueue:无界队列 ArrayBlockingQueue:具有固定尺寸,可以在他被阻塞之前放入有限数量的元素
-
死锁
- 死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
- 死锁产生的四个条件:
- 互斥条件:任务使用的资源至少有一个是不能共享的
- 至少有一个任务他必须持有一个资源且正在等待获取一个当前被别的任务持有的资源
- 资源不能被强占,任务必须把资源释放当做普通事件
- 循环等待
-
新类库的构建
- CountDownLatch
- 被用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成
- 对CountDownLatch对象设置一个初始计数值,任何在这个对象上调用wait()的方法都将阻塞,直至这个计数值到0.其他任务在结束其工作时,可以在该对象上调用countDown()来减小这个计数值。
- 调用countDown()的任务在产生调用时并没有阻塞,只有对await()的调用会被阻塞,直至计数值为0
- CountDownLatch只触发一次,计数值不能重置
- CyclicBarrier
- 适用情况:创建一组任务,并行的执行工作,然后在进行下一个步骤之前等待,直至所有任务完成。它使得所有的并行任务都将在栅栏处列队,可以一致地向前移动
- CountDownLatch只触发一次,CyclicBarrier可以多次重用
- DelayQueue
- 无界的BlockingQueue,用于放置实现了Delay接口的对象,其中的对象只能在其到期时才能从队列中取走。这个队列是有序的,即对头对象的延迟到期的时间最长。如果没有延迟到期,那么就不会有头元素。poll()将返回null
- Delayed接口继承Comparable接口,因此必须实现comparaTo(),使其可以产生合理的比较
- PriorityBlockingQueue
- 基础的优先级队列,具有可阻塞的读取操作
- 使用ScheduledExecutor设置定时任务。
- 通过schedule()【运行一次的任务】或者scheduleAtFixedRate()【每隔规则的时间重复执行任务】,可以将Runnable对象设置为在将来的某个时刻执行
- Semaphore
- 正常的锁(来自concurrent.lock或内建的synchronized锁),在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源
- Semaphore是一个计数信号量,维护了一个许可集,通常用于限制可以访问某些资源的线程数目
- 在访问资源前,每个线程必须首先从信号量获取许可,从而保证可以使用该资源。该线程结束后,将资源释放回资源池中并将许可返回给信号量,从而允许其他线程获取和使用该资源。如果当前信号量中没有资源访问许可,则信号量会阻塞调用的线程直到获取一个资源许可,否则,线程将被中断。
- Exchanger
- Exchanger是在两个任务之间交换对象的栅栏。当这些任务进入栅栏式,他们各自拥有一个对象,当他们离开时,他们都拥有之前由对象持有的对象
-
ReadWriteLock
- ReadWriteLock使得可以拥有多个读者,只要他们不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者将不能访问,直至写锁被释放
-
活动对象
- 每个对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队。任何时刻都只能运行一个。有了活动对象,就可以串行化消息而不是方法
- 向一个活动对象发送消息时,这条消息将会变成一个任务,该任务会被插入到这个对象的队列中,等待在以后的某个时刻运行
- 优点
- 每个对象都可以拥有自己的工作器线程
- 每个对象都将维护对他自己的域的全部控制权
- 所有在活动对象之间的通信都将以在这些对象之间的消息形式发生
- 活动对象之间的所有消息都将排队