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
- ExecutorService 线程池,该接口实现了 Executor 他知道如何根据恰当的上下文来执行Runnable对象,即根据线程环境,类型有顺序有条件的启动。
- 线程的创建代价非常高昂,最好是提前创建几个线程然后一直持有它,当执行完任务后不销毁而是重新放回线程池内等待执行下一个任务。
- 调用execute方法可以往线程池中添加任务,这些任务由线程池中的线程去执行。
- 调用 shutdown 之后可以阻止线程池再加入线程。
- Executors 是一个工具类,他可以产生ExecutorService对象和一些线程相关的其他对象。
- newCachedThreadPool() 创建一个线程池
- newFixedThreadPool(int i) 也可以创建一个固定线程数的程序池
- newSingleThreadExecutor() 创建一个没有限制的队列池,放入的任务会排队执行,执行完一个任务才开始执行下一个任务。
- 线程池中的线程在可能情况下都会被复用,有个线程执行完一个任务会接着执行另一个任务。
4.从任务中产生返回值
- Runnable 是执行工作的独立任务,他不能有任何返回值。
- 如果想要任务完成时可以返回一个值,那么创建任务类时不用继承Runnable 接口 而是实现 Callable 接口。
- Callable<V> 和 Runnable 接口使用方法一样 V call()执行任务 并且返回一个V类型的值
- ExecutorService 的 submit() 会返回一个Future 对象。
<T> Future<T> submit(Callable<T> task) 返回Callable任务执行结果的状态 Future<?> submit(Runnable task) 返回Runnable任务执行的状态 <T> Future<T> submit(Runnable task, T result) 返回Runnable任务执行的状态和 result 值
- Future(接口)对象可以对Runnable和Callable任务进行取消、查询结果是否完成、获取结果。
- Future<V> 接口提供了一些方法用来对结果进行检测
boolean cancel(boolean mayInterruptIfRunning) 尝试取消执行此任务。 V get() 等待计算完成,然后检索其结果。 V get(long timeout, TimeUnit unit) 如果在规定时间内得到结果就立刻返回,超时抛出TimeoutException boolean isCancelled() 如果此任务在正常完成之前被取消,则返回 true 。 boolean isDone() 如果任务已完成返回true。 完成可能是由于正常终止,异常或取消
调用get 之前最好 isDone判断一下,否则get将一直阻塞直到得到结果,或者使用超时get
ExecutorService pool = Executors.newFixedThreadPool(1); Future<String> submit = pool.submit(new Callable<String>() { @Override public String call() throws InterruptedException { Thread.sleep(5000); return "时间到"; } }); pool.shutdown(); System.out.println(new Date()); System.out.println(submit.get()); System.out.println(new Date()); Wed Aug 22 17:31:02 CST 2018 时间到 Wed Aug 22 17:31:07 CST 2018
- FutureTask 类 构造器FutureTask(Callable<V> callable)
FutureTask(Runnable runnable, V result) //Runnable 自身不返回结果,但可以设定返回一个V型值
- FutureTask实现了 RunnableTask接口,RunnableTask接口又实现了Runnable,Future接口
- FutureTask 也是 Future接口的唯一实现
- 因为FutureTask 实现了Runnable接口 所以Thread可以通过启动FutureTask 启动Callable任务
FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() { @Override public String call() throws InterruptedException { Thread.sleep(5000); return "时间到"; } }); Thread thread = new Thread(futureTask); thread.start(); System.out.println(new Date()); System.out.println(futureTask.get()); System.out.println(new Date());
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
- 该接口可以设置线程的属性,仅有一个方法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方法 并且线程能够执行这两个方法中,他们就统称任务类。
- 任务类直接从Thread继承:任务类直接继承Thread,在构造器中调用start(),当创建任务类对象时就启动了线程并执行任务
lass SimpleThread extends Thread{ SimpleThread(){ start(); } @Override public void run() { System.out.println("继承Thread的任务类"); } }
- 任务类实现Runnable,内置Thread对象并传入任务类自己的引用this,并在构造器中启动start()
class SelfManaged implements Runnable{ SelfManaged(){ thread.start(); } Thread thread = new Thread(this); @Override public void run() { System.out.println("内置Thread的任务类"); } }
- 注意1、2都是在构造器中启动线程,这意味着这个任务对象还未完全稳定线程就启动了,有可能另一个线程会访问这个不稳定的对象,这样就存在隐患。显示创建Thread就会存在这种问题,这也是优先使用Executor的原因。
- 可以通过内部类或者匿名内部类将线程代码隐藏在类中或者方法中。
10.加入一个线程
- A线程调用B线程的join()方法,则A线程会被挂起直到B线程执行被中断或者正常结束再继续执行A。
class A extends Thread{ Thread b; A(Thread b){ this.b = b; start(); } @Override public void run() { System.out.println("A线程启动"); System.out.println("B线程加入"); try { b.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("A线程结束"); } } class B extends Thread{ B(){ start(); } @Override public void run() { System.out.println("B线程执行"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("B结束执行"); } } B b = new B(); A a = new A(b);
- 可以调用interrupt()中断一个线程
- 调用interrupt()中断一个线程后,此时线程会设置中断标志位 true
- 如果线程处于阻塞状态被中断那么会抛出InterruptedException异常,此异常被捕获之后会清理中断标志位。
- 调用isInterrupted() 可以查看中断标志位状态。中断标志位在其他地方也有用,某些线程可能会检查其他线程的中断状态。
11.捕获异常
- 一般无法捕获从线程逃逸出去的异常,run只能捕获run方法catch中 的异常其他异常无法捕获,一但异常逃出run方法那么会直接输出到控制台。
public void run() { try { Thread.sleep(5); System.out.println("Prints"); throw new RuntimeException(); //任何地方无法捕获此异常 } catch (InterruptedException e) { e.printStackTrace(); } } Exception in thread "pool-1-thread-1" java.lang.RuntimeException at Prints.run(Main.java:46) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
由于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包装直接在当前线程设置即可
public class Main { public static void main(String[] args) throws Exception { /* 设置默认处理程序 Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); ExecutorService service = Executors.newCachedThreadPool(); */ // 专有处理程序 ExecutorService service = Executors.newCachedThreadPool(new MyThreadFactory()); service.execute(new Prints()); } } class Prints implements Runnable { @Override public void run() { try { Thread.sleep(5); System.out.println("Prints"); throw new RuntimeException(); } catch (InterruptedException e) { e.printStackTrace(); } } } class MyThreadFactory implements ThreadFactory{ @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); return t; } } class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println(e); } }
12.共享受限资源
- 多个线程同时访问同一资源(实例变量,类变量),有可能某个线程恰好在资源“不恰当”的状态下问了它,自然的到的结果也是不正确的。
- 永远不知道一个线程何时在运行。
- 并发模式解决线程冲突时都是采用 序列化访问共享资源 的解决方案,这意味这某一时刻只能有一个任务访问共享资源。
- 通常在代码上加上锁语句来实现 序列化访问 这意味着某段时间只能有一个任务可以执行这段代码,因为锁产生了一种 相互排斥的效果 所以常常称为互斥量。
13.synchronized
- 同步锁 。为防止资源冲突提供内置支持,保证原子性。
- 当任务要执行被synchronized保护的代码片段时,会检查它的锁能不能用,然后获取锁执行代码后释放锁。
- 共享资源一般指以对象形式存在的内存片段(实例变量,类变量),要控制对内存片段访问,首先把它置于某个对象中 再把所有要访问这个资源的方法加synchronized.
- 所有对象都自动含有单一的锁(监视器),当对象上任意一个synchronized方法被调用时,此方法被加锁,对象中其他synchronized方法无法拿到该对象的锁就无法进入方法执行代码。
- 一个synchronized方法拿到锁还没释放锁之前,其他任何线程想要执行synchronized 方法都会被阻塞,因为同一对象中所有synchronized方法都共享同一把锁,别的线程拿到了锁,其他线程无法再拿到锁。
- 把域设为private,只有通过方法才能访问,synchronized就可以阻止其他对象同时访问该域
- 一个任务可以多次获得同一个对象的锁。当一个synchronized方法调用了同一个对象的synchronized方法那么锁的数量就会 +1,执行完方法后锁数量 -1。jvm负责跟踪锁的数量。
- 每个Class对象也有锁,使用synchronized static 方法就可以控制住对类成员(static数据)的并发访问。
14.何时使用synchronize
- 何时使用同步,可以运用 Brian 同步规则
- 当你正在写一个变量接下来可能被另一个线程读取,或者正在读取一个上一次已经被另一个线程修改过的变量,你就得使用同步,并且 读写线程都得使用相同的 对象的锁 同步。
- 如果类中有超过一个方法处理共享数据那么必须同步所有相关的方法。如果只同步一个有可能在其他线程中被同步方法调用。
15.显示Lock对象
- Lock(接口)对象必须被显式的创建、锁定、销毁。
- 创建锁对象 Lock lock = new ReentrantLock(); 或 ReentrantLock lock = new ReentrantLock(); 与synchronized有相同的作用,但也可以扩展功能。
- lock() 必须放置在要加锁的方法的第一句,紧接着try - finally 语句在 finally中执行 unlock(),因为锁必须要被释放。
- synchronized 失败时会抛出异常,没有机会去做任何清理工作,而显式的Lock锁就可以使用finally 去做清理工作。
- ReentrantLock 的 tryLock() 方法是尝试获取锁如果可以则获得锁并且返回true,tryLock(long timeout, TimeUnit unit)在规定时间内尝试获得锁。synchronized 则没有这个能力。
16.原子性与易变性
- 原子操作:不能被线程机制中断的操作,一旦操作开始那么他一定可以在可能发生上下文切换(即切换到其他线程)之前执行完毕。
- 一个不正确的认知 : 原子操作不需要进行同步控制
- 除了 long 和 double 之外所有的基本数据类型的 简单操作如 赋值 i = 2,返回 return i 这都是原子操作,其他的操作不一定是原子操作。
- jvm 将64位的操作如 double long 的读写分为2个32位的读写操作所以这就有可能产生了上下文切换,有时也称 字撕裂
17.Java内存模型 JMM
- JMM : 所有数据都在主存中,每个线程从主存复制一份数据到自己的工作内存(高速缓存),所有对变量的操作都在自己的工作内存中,不能对主存直接操作,不同的线程不能访问其他线程的工作内存。
- 数据的读写过程:从主存读取数据到工作内存,CPU再从工作内存中加载数据计算后回写到缓存,缓存再刷新到主存。
18.volatile
- 保证了可见性,不能保证原子性
- 可见性:对一个volatile变量进行了写操作,那么所有读操作都会看到这个修改,因为对volatile变量修改之后会从缓存立刻刷回到主存。同时通知其他所有在缓存中的volatile变量失效,重新从主存中读取。
- 非volatile变量的原子操作不会立刻刷回到主存,其他读取该变量的线程也立刻看不到更新后的值
- long double 变量用volition修饰后 简单的赋值,返回就是原子操作。
- 同步 也会 使工作内存失效,导致从主存重新获取数据,使用了同步 就没有必要使用volition
- 当一个值 依赖于它之前的值 或者受限制与于其他值,那么使用volition之后就无法正常工作了得到正确结果,如 计数器 i++ (依赖于前一个值),多线程下i++会计算错误且比理论值小。参考博客
19.使用synchronized和volatile的要求
- 如果一个变量与其他变量无关且对会被多个任务访问,而且不在synchronized中那么必须使用volatile,如果有多个类似的值那么都要使用volition
- 第一选择synchronized,只有确保使用volatile是安全的才使用。
20.原子类
- SE5加入了 特殊的原子性变量,在java.util.concurrent.atomic.* 包中,以Atomic开头
- 对这些原子类的操作都有相应的原子操作方法
- 这些类在性能调优是作用大,一般编程使用地方少
- 其中一部分方法
int addAndGet(int delta) 将给定的值原子地添加到当前值。 int decrementAndGet() 减1再返回。 int get() 获取当前值。 int getAndAdd(int delta) 将给定的值原子地添加到当前值。 int getAndDecrement() 返回当前值再减1。 int getAndIncrement() 返回当前值再加1。 int getAndSet(int newValue) 将原子设置为给定值并返回旧值。 int getAndUpdate(IntUnaryOperator updateFunction) 用应用给定函数的结果原子更新当前值,返回上一个值。 int incrementAndGet() 加1再返回。
21.临界区
- 同步代码块。 希望防止多个线程同时访问方法中的部分代码,而不是防止整个方法。可以提高任务访问对象的时间性能。
public void println(String x) { synchronized (this) { print(x); newLine(); } }
进入代码块必须获得synchronized 同步锁
- 也可以使用Lock 来创建临界区
22.其他对象上同步
- synchronized 同步方法是拿到的锁 是调用这个方法的当前对象的锁,同步代码块可以使用别的对象的锁。
- 使用同步代码块是要给synchronized传递一个对象来获取这个对象的锁
- 同步代码块获得的锁不同,则不同锁之间的方法不受同步控制的影响,只有争夺同一把锁才可以达到互斥同步的要求
public void print(){ synchronized(new Object()){ System.out.println("同步代码块"); } }
这是一个有问题的方法。不同线程new Object()肯定不是同一个对象,所以任何线程都能同时执行这段代码,要想做到同步就要使用唯一不变的对象作为锁。
- 如果锁的对象时this,也就是当前对象那么同步代码块拿到了锁,任何synchronized方法谁都没法获得锁。
public void print(){ synchronized(this){ System.out.println("同步代码块"); } } public synchronized void say(){ System.out.println("同步方法"); }
23.线程本地存储
- 防止任务在共享资源上产生冲突,一种解决办法是同步,第二种办法就是不共享。
- ThreadLocal<T> 类 用哈希表的形式为每个不同的线程创建一个存储副本。
- 可以set (T value ) 往 本地线程存储一个对象,使用 T get()取出对象。(只能存一个对象,因为set底层是个map,map的key是本地线程对象所在的线程,所以set第二个值会覆盖第一个)
- ThreadLocal 使用时作为某个类的静态成员,但是多线程不会出现竞争。因为每个线程的本地线程独立的。
private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();
- ThreadLocal 在web开发中也应用广泛,一个用户的与服务器的交互就是一条线程,所以可以使用本地线程来存储一下相关信息如用户ID ,判断用户是否登录可以在本地线程中去找用户ID.
24.线程的状态
-
线程的四种状态
- 新建 new : 线程被创建时短暂处于这种状态,此时已经分配了资源,执行了初始化有了资格获得CPU时间,之后调度器会把这个线程转变为可运行状态或者阻塞。
- 就绪 Runnable :这种状态下只要调度器把时间片分给线程,线程就开始运行。也就是说任意时刻线程可以运行也可以不运行,只要调度器能分配时间片给线程那么线程就可以运行,这不同于死亡或者阻塞。
- 阻塞 Blocked : 线程可以运行但是有个条件阻止他运行。当线程处于阻塞状态,调度器将忽略该线程,不会分配CPU时间给线程,除非他重新进入就绪状态,他才有可能执行。
- 阻塞状态说明 执行Thread.sleep(1000) 代码 在1 秒内线程将被调度器忽略,1s 后进入就绪转态而不是立刻执行,还要等调度器给他分配时间片才执行。
- 死亡 Dead : 处于死亡状态的或者终止状态的线程将不再时可调度的,并且也不会得到CPU的时间,这种状态通常是从run方法返回,但任务线程还是可以被中断的
-
进入阻塞状态
- 调用sleep 使任务休眠
- 使用wait( ) 使线程挂起,直到的到notify()或者notifyAll( )消息进入就绪状态。
- 任务正在等待某个输入输出
- 试图获得某个对象的锁,但是锁不可用,另一个线程已经的到了这个锁。
-
中断
-
在中断就是为了结束任务,但Java没有提供中断任务的机制而是给出了中断标志位,让你自己去实现中断。
- 每个线程都有一个中断标志,初始值时为false,调用Thread 的interrupt()方法就会将中断标志设为true。并且只有这个方法可以设置中断标志位为true
- 如果一个阻塞任务被中断,那么它会结束阻塞继续执行任务再恢复中断标志位为false并且抛出InterruptedException 异常,这样就要在run的catch中捕获它。
- I/O 、synchronized锁的阻塞任务不可被中断,所以设为true之后也不会抛异常。可以通过关闭阻塞的底层资源中断阻塞任务,但这时就会抛IO异常而不是InterruptedException异常。
- Lock使用lock()获取锁时的阻塞不可以被中断,lockInterruptibly())获取锁时的阻塞可以被中断,而且会抛出InterruptedException 异常。
- NIO类通道可以自动响应中断,不必显示关闭底层资源。
- 非阻塞任务在执行过程中中断标志被设为true,不会对之这个任务的执行有任何影响,除非你有检测中断标志的代码进行控制。
- Thread.interrupted() 会返回当前中断标志位,并且恢复到初始状态false。
- isInterrupted( )也会返回当前中断标志位,但是不会去恢复中断标志 。
-
-
设置中断标志
- 一般的线程可以调用interrupt() 方法设置中断标志。
- 对使用Executor创建的线程要中断其中某一条线程就要调用submit()启动任务, 调用返回的Futuer的cancel(true)方法,传递参数true,它就会代用执行次任务线程的interrupt方法,并且返回阻塞任务的列表。
- Executor调用 shutdownNow() 会发送interrupt给它所有启动的任务。
- Executor调用shutdown() 只是阻止往Executor添加任务,不会改变中断标志。
-
中断检查
- Thread.interrupted() 会返回当前中断标志位,并且恢复到初始状态false。
- isInterrupted( )也会返回当前中断标志位,但是不会去恢复中断标志 。
-
优雅的处理中断
- 当想处理一个中断时可以使用Thread.interrupted() 或isInterrupted( )作为判断条件来处理中断。
- 抛出InterruptedException 异常来处理中断,抛此异常说明这是一个阻塞方法。
- 查看如何处理InterruptedException 异常
25.线程之间的协作
- 任务协作时关键的问题是这些任务之间的握手,为了实现握手,使用了相同的基础特性:互斥。
- 互斥 消除了竞争条件,在互斥上在添加一种途径就是将自身挂起,直到某个条件达到,任务继续执行。
-
wait() 和 notifyAll()
- wait 使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力,所以需要将任务挂起,等待满足条件的通知到来
- wait 将任务挂起之后只有 notify()或者notifyAll()方法调用时才能唤醒任务。
- wait 挂起任务之后会释放的到的锁,而sleep、yield 则不会释放锁。正是因为这样其他同步方法才可以被其他任务调用。
- 两种wait 无参的只有通知才能被唤醒,带参数的需要2个精确的参数 (毫秒,纳秒) 超过参数时间或者得到通知才能被唤醒。
- wait 、notify()、notifyAll() 这都是Object 的方法因为他们要操作 锁 ,锁也是对象的一部分所以在Object中可以控制所有对象的锁。
- wait 、notify()、notifyAll()因为要控制所,所以他们只能放在同步方法或者同步块中。
- 在同步方法中调用wait它操作的锁就是对象本身的锁,在同步代码块在则操作代码块加锁对象的锁
- 没有同步使用 wait 、notify()、notifyAll() 虽然编译可以通过,但是运行会抛出IllegalMonitorStateException异常。
-
wait() 、notify()、notifyAll()
- 首先这些方法要使用在共享对象上,因为他们要拿到共享对象的锁,且只有 wait notify notifyAll 的锁相同才能被唤醒。
- 不同的任务执行共享对象的方法时如果被挂起,就是该任务被挂起,然后释放了锁,其他任务就可以得到锁然后执行任务。
- 其他任务在执行过程中达到条件 使用 notify 或者 notifyAll 唤醒刚刚被挂起的任务。
- 使用notifyAll 会唤醒所有wait的任务,notify只会唤醒一个,如果有多个wait时使用notify那就要确保唤醒的是正确的那一个,最好是多个wait任务在等待相同的条件,否则使用notify将唤醒不正确的任务。
- 任务在被真正唤醒之前首先要得到所在方法的锁,如果得不到锁那么就不能被唤醒。
-
错失信号
-
- 要确保 wait 执行之前 另一个线程的 notify没有执行,否则会错失notify信号造成wait无限制等待,造成死锁。
26.阻塞队列
- 使用 wait notifyAll 解决任务互操作是一种非常低级的方式,Java 并发包提供了阻塞队列来解决任务互操作问题。
- BlockQueue 阻塞队列接口 有大量的实现来解决任务之间同步互操作的问题,无需考虑同步 挂起等问题,BlockQueue已经都为你做好。
- 只需往BlockingQueue中put 队列满了会挂起,在可以put进去时就put进去,take也是一样。
- BlockingQueue
boolean add(E e) 将指定的元素插入,不成功则抛出IllegalStateException。 boolean offer(E e) 将指定的元素插入,不成功则返回false。 boolean offer(E e, long timeout, TimeUnit unit) 将指定的元素规定时间内插入。 void put(E e) 将指定的元素插入到此队列中,不成功则阻塞 E poll(long timeout, TimeUnit unit) 指定时间内取出头,否则返回false boolean remove(Object o) 从该队列中删除指定元素的单个实例(如果存在) E take() 取出队头元素,如果没有则一直阻塞
BlockingQueue部分实现
- SynchronousQueue 同步队列,任何时刻都只允许一个任务插入或移除元素。他并没有存储能力,不算一个真正的队列。
- ArrayBlockingQueue 创建时必须指定大小,看名称也能知道内部是个数组
- LinkedBlockingQueue 无界队列,可以存放大量的任务.
- BlockingQueue 的实现类的构造器 一些可以接受 boolean fair 参数,它指定了访问的公平性。
- 默认 fair 为 false ,即 不公平访问 就是 很多个put都被阻塞后如果可以有空位可以插入那么不会安照阻塞先后顺序插入。
- 公平访问就是按照阻塞先后迅速插入,take也一样。公平访问会对性能有一定影响。
- 任务使用管道进行输入输出
- 这种通信多出现于阻塞队列出现以前的Java代码中。
- 一对管道流其实就是一个阻塞队列。
- 管道流和普通I/O直接的区别就是管道阻塞可以被中断。
27.死锁
- 死锁发生的四个条件
- 互斥条件 :资源中至少有一个是不能共享的。
- 不可剥夺性: 任务获得的资源未使用完之前,别的任务不能强行夺走。
- 请求和保持条件:至少有一个任务持有一个资源且正在等待获取一个当前正被别的任务持有的资源。
- 循环等待:若干个任务形成一个首尾相接等待获取资源的环
- 防止死锁发生只需破坏四个条件中的一个即可。
- 最容易的办法就是破坏循环等待条件。
- 可以用 初始化限制动作 达到破坏循环等待的条件
28.concurrent 类库
- SE5 加入 java.util.concurrent 包来解决办法问题。
-
CountDownLatch 发令枪
- CountDownLatch(int count) 计数不可逆。一个用来同步线程开始的工具类,可以使多个线程并行开始执行。也可以多个发令枪形成一条链分别控制独立的任务,形成一个解决问题的链路。
void await() 时当前任务挂起,直到计数器为0,自动唤醒执行。 boolean await(long timeout, TimeUnit unit) 设置超时时间 void countDown() 减少锁存器的计数,每次减1,如果计数达到零,释放所有等待的线程。 long getCount() 返回当前计数。 String toString() 返回一个标识此锁存器的字符串及其状态。
- CountDownLatch(int count) 计数不可逆。一个用来同步线程开始的工具类,可以使多个线程并行开始执行。也可以多个发令枪形成一条链分别控制独立的任务,形成一个解决问题的链路。
-
CyclicBarrier 同步屏障
- 若干个任务分别到达某一步骤前调用await()停下,当所有任务都到达时再同步开始执行。
- CyclicBarrier(int parties) 当 parties 个任务调用了await()时开始再开始执行接下来的步骤。
CyclicBarrier(int parties, Runnable barrierAction) 当 parties 个任务调用了await()时执行barrierAction任务。
int await() 挂起线程 int await(long timeout, TimeUnit unit) 设置超时时间 int getNumberWaiting() 返回目前正在等待障碍的各方的数量。 int getParties() 返回所需要的await数量。 boolean isBroken() 查看处于wait的线程是否被中断 void reset() 将屏障重置为初始状态。
-
DelayQueue 延迟队列
- 这是一个无界的BlockingQueue,用于放置实现了 Delayed 接口的对象。(其实就是一个Delayed的PriorityQueue)
- DelayQueue 内的Delayed对象只能在延迟期时间过后才能取出,队头元素一点定是到期时间最长的元素。比方某一时刻同时放入A,B 两种元素A延迟期5s,B延迟期3s,那么10s后队头就是元素B,因为他到期时间最长。
- 如果没有元素到期,那么pull就会拿到null,take自然就是阻塞。
-
Delayed 接口
- 此接口只有一个方法 long getDelay(TimeUnit unit) 来返回 元素剩余的延迟时间。
- TimeUtil 是一个时间单位的枚举类,它会根据传入的时间单位,把剩余时间换算成对应单位的时间返回。
- 这句就表示过期时间:long expireTime = TimeUnit.NANOSECONDS.convert(saveTime, TimeUnit.SECONDS) + System.nanoTime();
- 返回剩余时间:return unit.convert(this.expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
- System.nanoTime() 表示随机从某一时刻(同一线程这个时刻是固定的)到现在所经过的纳秒数,所以有可能是负数,只适合用于计算时间段。
- System.currentTimeMillis()是1970.1.1 UTC 零点到现在的时间单位是毫秒,可以用来计算各种日期。
- 2个计算时间的方法都是返回一个long型值,显示日期是因为底层经过了数据计算和格式化显示。
- Delayed接口实现了Comparable接口,所以要创建Delayed 对象就要同时实现getDelay()和compareTo() 方法。
- getDelay() 将延迟到期时间剩余时间返回
- compareTo() 比较 延迟到期时间剩余时间 的大小。
- 此接口只有一个方法 long getDelay(TimeUnit unit) 来返回 元素剩余的延迟时间。
-
PriorityBlokckingQueue 优先级阻塞队列
- 这是一个很基础的优先队列,它具有阻塞 读 的操作。
- 可以放入实现了Comparable接口的对象。
- 也可以构造器中传入一个比较器Comparator,使用这个比较器进行排序。
- 可以限制队列大小也可以不限制。
-
ScheduledThreadPoolExecutor 定时任务
void execute(Runnable command) 执行 command零要求延迟。 <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) 在到达延迟时间后执行任务并返回一个ScheduledFuture对象 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 在到达延迟时间后执行任务并返回一个ScheduledFuture对象
-
Semaphore 计数信号量
- 正常锁只允许一个任务访问资源,Semaphore 可以允许若干个任务同时访问资源,可以看出是给任务发放一个访问许可标签。
Semaphore(int permits) 创建一个许可数量为permits的 Semaphore,默认非公平模式。 Semaphore(int permits, boolean fair) 创建一个许可数量为permits的 Semaphore,可设置是否是公平模式。
- Semaphore 使用方法和Lock类似 。
void acquire() 取许1个可证,获取不到则一直阻塞直到可用,或线程为 interrupted void acquire(int permits) 取许permits个可证,获取不到则一直阻塞直到可用,或线程为 interrupted void release() 释放1个许可证,将其返回到信号量。 void release(int permits) 释放permits个许可证,将其返回到信号量。
- 能不能访问资源或者阻塞就看acquir能不能拿到需要的许可证,释放的许可重新回到池里被其他acquire获取。
- 正常锁只允许一个任务访问资源,Semaphore 可以允许若干个任务同时访问资源,可以看出是给任务发放一个访问许可标签。
-
Exchanger<V> 交换者
- V 是2个任务要交换的数据对象类型。
- 该对象只有一个方法V exchange(V x) 或者设置超时时间V exchange(V x, long timeout, TimeUnit unit)
- 当第一个任务调用 V exchange(V x) 就会挂起,等到第二个任务调用V exchange(V x) 2个任务就会交换x对象,所以V exchange(V x) 只能成对使用。
- 第一个可以看成生产者,第二个可以看成消费者
29.性能调优
- concurrent类库中的部分类适用于常规应用,如BlockingQueue,而部分类只适用于提高性能。
- Lock 通常比 synchronized 要高效许多,但synchronized可读性好,再者ring如何退出互斥的开销远比方法内运算时间小很多,所以2者速度对外影响不大。
-
免锁容器
- SE5 加入了新容器,通过更灵巧的技术来避免加锁,从而提高线程安全的性能。
- 免锁容器背后的策略就是:对容器的修改和读取可以同时发生,只要读取者能看到修改的结果即可,修改其实在容器的一个副本中进行的,副本在修改过程中是不可视的。
- CopyOnwriteArrayList : 往里加元素时会创建一个副本,加成后把原来引用指向副本。好处是可以一边加一边读。
- CopyOnwriteSet: 使用了CopyOnwriteArrayList 来实现。
- ConcurrentHashMap和ConcurrentHashMap 采用了同样的思想。
- 从免锁容器中读要比synchronized对应读快许多,因为加锁释放锁的开销省掉了。
- 乐观锁 : 假设不会发生并发冲突,只在提交的时候检查数据有没有被改变过(数据完整性)。Atomic类就是如此。
- 悲观锁: 假设会发生并发冲突,屏蔽一切可能违反数据完整性的操作,如synchronized。
- ReadWriterLock 一个特殊的锁接口
- 对于不频繁写但频繁读取的数据结构进行了优化。可以同时多个任务读取,但是如果有写入任务持有锁,那么所有读都不可以。
- 只有一个实现类 ReentrantReadWriteLock 。要想实现多读的功能就可以使用 readLock()在读取的前面加读锁,在写入数据的前面加 writeLock()写锁
- ReentrantReadWriteLock 还有不少方法,涉及公平性 决策性等等。
30.活动对象,一种解决并发竞争的方式
- 活动对象
- 每个活动对象都有自己的线程
- 每个对象都将维护对他自己的域的全部权利。
- 所有活动对象间的通信都将以这些对象之间的消息形式发生。
- 活动对象之间的消息都要排队。
- 活动对象之间的消息都成了一个任务,而任务会被传入一个List保存,然后逐个执行,任何时刻都只有一个调用,所以不会产生死锁。
31.理解并发 ,多线程和线程不安全
- 多线程并不是并发,并发是一种状态,是指系统同时遇到大量请求。
- 多线程是解决并发的一种方法。
- 线程不安全是指 线程之间可能需要竞争同一个资源,而出现的逻辑错误。
- 一个类,只有方法和成员变量,方法用来执行逻辑计算,成员变量用来保存值。
- 出现不同的线程同一时间访问同一个servlet的时候,servlet的对象只有一个,但是由于是tomcat支持多线程的原因,每个客户端请求执行的servlet中的方法都是在分配给自己的线程里执行。
- 由于servlet的对象只有一个所以成员变量是所有线程共享的,所以是共享资源,只要所有方法不访问成员变量就会出现线程不安全的问题。
知识点:
- 如果要保证 好几个方法调用同一个对象,那么最好给这个对象的引用加上 final 。
- Random.nextInt(i) 是线程安全的。
- 线程安全是指一个对象在多个线程下运行不用加任何的同步或者其他操作总能的到正确的结果,就说这个对象是线程安全的。
- 线程的好处就是提供了轻量级的上下文切换(100)条以内,而不会重量级的上下文切换(上千条)。
- 进程内的线程共享相同的内存空间(线程独享各自的工作内存),轻量级的上下文切换只改变程序的执行序列和局部变量,而进程切换(重量级上下文切换)必须改变所有内存空间。