Object是全部类的基类,它有5个方法组成了等待、通知机制的核心:notify()、notifyAll()、wait()、wait(long) 和wait(long,int)。在java中,全部的类都是从Object继承而来,所以,全部的类都拥有这些共同的方法可供使用。java
public final void wait() throws InterruptedException,IllegalMonitorStateException
该方法用来将当前线程置入休眠状态,直到接到通知或中断为止。在调用wait()以前,线程必需要得到对象的对象级别的锁,即只能在同步方法或同步代码块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其余线程竞争从新得到锁。若是调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,所以不须要try-catch结构。git
public final native void notify() throws IllegalMonitorStateException
该方法也要在同步方法或同步代码块中调用,即在调用前,线程也必需要得到该对象的对象级别锁,若是调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。github
该方法用来通知那些等待该对象的对象锁的其余线程。若是有多个线程等待,则线程规划器任意挑选其中一个wait()状态得线程来发出通知,并使它们等待获取该对象的对象锁。(notify后,当前线程不会立刻释放对象锁,wait所在的线程并不能立刻获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程才能够得到该对象锁)。编程
但不惊动其余一样等待该对象notify的线程们,当第一个得到了该对象的wait线程运行完毕以后,它会释放该对象的锁,此时若是该对象没有再次使用notify语句,则即使对象已经空闲,其余wait状态等待的线程因为没有获得该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。数组
wait状态等待的是被notify而不是锁。缓存
public final native void notifyAll() throws IllegalMonitorStateException
该方法与notify()方法的工做方式相同,重要的一点差别:
notifyAll使全部的方法在该对象wait的线程通通退出wait状态,变成等待获取该对象上的锁,一旦该对象锁被释放(即notifyAll线程退出同步代码块时),它们就会去竞争。若是其中一个线程得到了该对象的锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其余的已经被唤醒的线程将会继续竞争该锁,一直进行下去,直到全部被唤醒的线程都执行完毕。安全
public class WaitAndNotify { public static void main(String[] args) throws InterruptedException{ WaitAndNotify wan = new WaitAndNotify(); // synchronized(wan){ // wan.wait(); // System.out.println("wait"); // } new Thread(new Runnable(){ public void run(){ synchronized(wan){ try { wan.wait(); System.out.println("wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable(){ public void run(){ synchronized(wan){ try { wan.wait(); System.out.println("wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable(){ public void run(){ synchronized(wan){ wan.notifyAll(); //当notify方法时只执行一个wait、而notifyAll方法将执行两个wait System.out.println("notify"); } } }).start(); } }
建立以后未启动服务器
调用了start方法,不过还未被OS调用,或者正在等待CPU时间片。多线程
正在被OS执行。架构
阻塞状态分三种:
线程执行完毕或出现异常退了run()。
volatile能够保证线程的可见性并提供必定的有序性,可是没法保证原子性。
在JVM底层,volatile是采用内存屏障来实现的。
内存屏障是一个cpu指令,他的做用:
若是字段是volatile,java内存模型将在写操做后插入一个写屏障指令,在读操做前插入一个读屏障指令。这就意味着若是你对一个volatile字段进行写操做,那么,一旦你完成写入,任何访问这个字段的线程都将会获得最新的值;在你写入以前,会保证以前发生的事情已经发生,而且任何更新过的数据值都是可见的。
串行执行任务:在单个线程中串行地执行各项任务。
显式地为任务建立线程:经过为每一个线程建立一个新的线程来提供服务,从而实现更高的响应性。这种方式存在必定缺陷:
线程池
Executor虽然是一个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,应用Runnable来表示任务。同时它还提供了对生命周期的支持。基于生产者-消费者模式,提交任务的操做至关于生产者,执行任务的线程则至关于消费者。
ExecutorService的生命周期有三种状态:运行、关闭和已终止。
Java并无提供任何机制来安全的终止线程,但它提供了中断,这是一种协做机制,可以使一个线程终止另外一个线程的当前工做。
Thread的中断方法:
public class Thread{ public void interrupt(){….} public Boolean isInterrupted(){….} public static Boolean interrupted(){….} }
对中断的正确理解:他并不会真正的中断一个正在运行的线程,而只是发出中断请求,而后由线程在下一个合适的时刻中断本身。
Thread.sleep()和Object.wait()都会检查线程什么时候中断,而且在发现中断时提早放回。它们在响应中断时执行的操做:清除中断状态,抛出InterruptedException,表示阻塞操做因为中断而提早结束。可以中断处于阻塞、限期等待、无限期等待等状态,但不能中断I/O阻塞跟synchronized锁阻塞。
在调用interrupted()时返回true,除非像屏蔽这个中断,否则就必须对它进行处理。若是一个线程的 run() 方法执行一个无限循环,而且没有执行 sleep() 等会抛出 InterruptedException 的操做,那么调用线程的 interrupt() 方法就没法使线程提早结束。
可是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。所以能够在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提早结束线程。
线程池:管理一组同构工做线程的资源池,经过重用现有的线程而不是建立新线程,能够在处理多个请求时分摊在线程建立和销毁过程当中产生巨大开销。
类库提供了一个灵活的线程池以及一些有用的默认配置。能够用过调用Executors中的静态工厂方法之一来建立一个线程池:
ThreadPoolExecutor为一些Executor提供了基本实现,这些Executor时由Executors中的newFixedThreadPool、newCachedThreadPool和newScheduledThreadExecutor等工厂方法返回的。
ThreadPoolExecutor是一个灵活的、稳定的线程池,容许进行各类定制。若是默认的执行策略不能知足需求,那么能够经过ThreadPoolExecutor的构造参数来实例化一个对象,并根据本身的需求来定制。
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler){…}
线程池的基本大小,最大大小以及存活时间等因素公共负责线程的建立和销毁。
在线程池中,若是新请求的到达速率超过线程池的处理速率,那么新到来的请求将积累起来。在线程池中,这些请求会在一个由Executor管理的Runnable队列中等待。
ThreadPoolExecutor容许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有三种:有界队列、无界队列和同步移交。队列的选择与其余的配置参数有关,例如线程池的大小等。
当有界队列被填满以后,饱和策略开始发挥做用。JDK提供了集中不一样的RejectedExecutionHadler来实现。
每当线程池须要建立一个线程时,都是经过线程工厂方法来完成的,默认的线程工厂方法将建立一个新的、非守护的线程,而且不包含特殊的配置信息。经过制定一个特定的工厂方法,能够定制线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池须要建立一个新线程时都会调用这个方法。
public interface ThreadFactory { /** * Constructs a new {@code Thread}. Implementations may also initialize * priority, name, daemon status, {@code ThreadGroup}, etc. * * @param r a runnable to be executed by new thread instance * @return constructed thread, or {@code null} if the request to * create a thread is rejected */ Thread newThread(Runnable r); }
Java5.0增长了一种新的机制:ReentranLock。与内置加锁机制不一样的时,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操做,全部加锁和解锁的方法都是显式的。
Lock lock = new ReentrantLock(); //... lock.lock(); try { //更新对象状态 //捕获异常,并在必要时恢复不变形条件 } finally { lock.unlock(); //不会自动清除锁 }
当程序的执行控制单元离开被保护的代码块时,并不会自动清除锁。
可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具备更完善的错误恢复机制。在内置锁中,死锁是一个很严重的问题,恢复程序的惟一方式是从新启动程序,而防止死锁的惟一方法就是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的锁提供了另外一种选择:避免死锁的发生。
若是不能得到全部须要的锁,那么可使用可定时的或可轮询的锁获取方式,从而使你从新得到控制权,它会释放已经得到的锁,而后从新尝试获取全部锁。(或其余操做)
在实现具备时间限制的操做时,定时锁一样费用有用:当在带有时间限制的操做中调用一个阻塞方法时,它能根据剩余时间来提供一个时限,若是不能在制定的时间内给出结果,那么就会使程序提早结束。
内置锁是不可中断的,故有时将使得实现可取消得任务变得复杂,而显示锁能够中断,lockInterruptibly方法可以得到锁的同时保持对中断的响应。LockInterruptibly优先考虑响应中断,而不是响应锁的普通获取或重入获取,既容许线程在还等待时就中断。(lock优先考虑获取锁,待获取锁成功以后才响应中断)
在ReentrantLock的构造函数中提供了两种公平性选择:建立一个非公平的锁(默认)或者一个公平的锁,在公平的锁上,线程讲按照它们发出的请求的顺序来获取锁,但在非公平锁上,则容许插队(当一个线程请求非公平锁时,同时该锁的状态变为可用,那么这个线程将跳过队列中全部的等待线程并得到这个锁)。
在公平锁中,若是有一个线程持有这个锁或者有其余线程在队列中等待这个锁,那么新发的请求的线程将会放入队列中,而在非公平锁中,只有当锁被某个线程持有时,新发出的请求的线程才会放入队列中。
大多数状况下,非公平锁的性能要高于公平锁
Java虚拟机规范试图定义一种java内存模型(Java Memory Model,JMM)来屏蔽掉各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。
计算机系统中处理器上的寄存器的读写速度比内存要快几个数量级别,为了解决这种矛盾,在它们之间加入高速缓存。
加入高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,可是也为计算机系统带来了更高的复杂度,由于它引入了一个新的问题:缓存一致性。
多处理器系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存,当多个处理器的运算任务涉及到同一块主内存区域时,将可能致使各自的缓存不一致。
为了解决一致性问题,须要各个处理器访问缓存时都遵循一些协议,在读写时根据协议来进行操做。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存以及从内存中取出变量的底层细节。
JMM将全部变量(实例字段、静态字段、数组对象的元素,线程共享的)都存储到主内存中,每一个线程有本身的工做内存,工做内存中保存了被线程使用到的变量的主内存的副本拷贝,线程对变量的全部操做都必须在工做内存中进行。
Java内存模型定义了8个操做来完成主内存和工做内存间的交互操做。
Java内存模型保证了read、load、use、assign、store、write、lock和unlock操做具备原子性,例如对一个int类型的变量执行assign复制操做,这个操做就是原子性的。可是Java内存模型容许虚拟机将没有被volatile修饰的64位数据(long,double)的读写操做划分为两个32位的操做来进行,即load、store、read和write操做能够不具有原子性。
虽然Java内存模型保证了这些操做的原子性,可是int等原子性操做在多线程中仍是会出现线程安全性问题。
可经过两个方式来解决:
可见性指一个线程修改共享变量的值,其余线程可以当即得知这个修改。Java内存模型经过在变量修改后将新值同步回主内存中,在变量读取前从主内存刷新变量值来实现可见性。
主要有三种方式实现可见性:
有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。在Java内存模型中,容许编译器和处理器对指令进行重排序,重排序过程当中不会影响到单线程程序的执行,却会影响多线程并发执行的正确性。
volatile关键字经过添加内存屏障的方式禁止重排序,即重排序时不能把后面的指令放在内存屏障以前。
也能够经过synchronized来保证有序性,它保证每一个时刻只有一个线程执行同步代码,至关于让线程顺序执行同步代码。
synchronized和ReentranLock。、
互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题,所以这种这种同步烨称为阻塞同步。
互斥同步是一种悲观的并发策略,老是觉得只要不去作正确的同步策略,那就确定会出问题,不管共享数据是否真的会出现竞争,它都须要进行枷锁。
随着硬件指令集的发展,咱们可使用基于冲突检测的乐观并发策略:先进行操做,若是没有其余线程竞争共享资源,那么操做就成功了,不然采起补偿措施(不断尝试、知道成功为止)。这种乐观的并发策略的许多实现都不须要将线程阻塞,所以这种同步操做称为非阻塞同步。
乐观锁须要操做和冲突检测这两个步骤具有原子性,这里就不能再使用互斥同步来保证,只能靠硬件来完成。硬件支持的原子性操做最典型的是:比较和交换(Compare-And-swap,CAS)。CAS指令须要3个操做数,分别是内存地址V、旧的预期值A和新值B。当操做完成时,只有内存地址V等值旧的预期值时,才将V值更新为B。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,由于局部变量存储在虚拟机栈中,属于线程私用。
若是一段代码中所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。若是能保证,咱们就能够把共享数据的可见范围限制在同一个线程以内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特色的应用并很多见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽可能在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的普遍应用使得不少 Web 服务端应用均可以使用线程本地存储来解决线程安全问题。
可使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
synchronized关键字在通过编译以后,会在同步代码块先后分别造成monitorenter和monitorexit两个字节码指令。
在执行monitorenter指令时,首先尝试获取对象的锁,若是这个对象没有被锁定或者当前线程已经拥有了这个锁,就把锁的计算器+1,相应的执行完monitorexit指令时将锁计算器减1,当计算器为0时,锁就被释放。
指JVM对synchronized的优化。
互斥同步进入阻塞状态的开销都很大,应该尽可能避免,在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在共享数据的锁执行忙循环(自旋)一段时间,若是在这段时间内可以得到锁,就能够避免进入阻塞状态。
自旋锁虽然可以避免进入阻塞状态从而减小开销,可是它须要进行忙循环操做占用cpu时间,它只适用于共享数据的锁定状态很短的场景。
在JDK1.6中引入了自适应的自旋锁,自适应意味着自旋次数再也不是固定的,而是由前一次在同一个锁上的自旋次数及锁的拥塞者的状态来决定。
锁清除是指对被检测出不存在竞争的共享数据的锁进行清除。
锁清除主要经过逃逸分析来支持,若是堆上的共享数据不可能逃逸出去被其余线程访问到,那么就能够把他们当成私有数据,也就能够将他们的锁清除。
对于一些看起来没有加锁的代码,其实隐式的加了不少锁,例如一下的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
String是一个不可变的类,编译器会对String的拼接进行自动优化。在JDK1.5以前会转化为StringBuffer对象连续append()操做。
public static String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
每个append()方法中都有一个同步块,虚拟机观察变量sb,很快发现它的动态做用域被限制在concatString方法内部,也就是说,sb的全部引用永远不会逃逸到contatString()方法以外,其余线程没法访问到它,所以能够进行锁清除。
若是一系列的操做都对同一对象反复加锁和解锁,频繁的加锁操做就会致使性能消耗。
上一节的示例代码中连续的append()方法就属于这种状况。若是虚拟机探测到由这样的一串零碎的操做都是对同一个对象加锁,将会把锁的范围扩展(粗化)到整个操做序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操做以前直至最后一个 append() 操做以后,这样只须要加锁一次就能够了。
轻量级锁跟偏向锁是java1.6中引入的,而且规定锁只能够升级而不能降级,这就意味着偏向锁升级成轻量级锁后不能下降为偏向锁,这种策略是为了提升得到锁的效率。
Java对象头一般由两个部分组成,一个是Mark Word存储对象的hashCode或者锁信息,另外一个是Class Metadata Address用于存储对象类型数据的指针,若是对象是数组,还会有一部分存储的是数据的长度。
对象头中的Mark Word布局
轻量级锁是相对于传统的重量级锁而言,它使用CAS操做来避免重量级锁使用互斥量的开销。对于大部分的锁,在整个同步周期内都是不存在晶振的,所以也就不须要使用互斥量进行同步,能够先采用CAS操做进行同步,若是CAS失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,若是锁对象标记为0 01,说明锁对象的锁未锁定(unlock)状态,此时虚拟机在当前线程的虚拟机栈建立Lock Record,而后使用CAS操做将对象的Mark Word更新为Lock Record指针。若是CAS操做成功了,那么线程就获取了该对象上的锁,而且对象的Mark Word的锁标记变为00,表示该对象处于轻量级锁状态。
若是CAS操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的虚拟机栈,若是是的话说明当前线程已经拥有了这个锁对象,那就能够直接进入同步块执行,不然说明这个锁对象已经被其余线程抢占了。若是有两个以上的线程竞争同一个锁,那轻量级锁就再也不有效,要膨胀为重量级锁。
轻量级锁的步骤以下:
偏向锁的思想是偏向于让第一个获取锁对象的线程,在以后的获取该锁就再也不须要进行同步操做,甚至连CAS操做也不须要。
当锁对象第一次被线程得到的时候,进入偏向状态,标记为1 01。同时使用CAS操做将线程ID记录到Mark Word中,若是CAS操做成功,这个线程之后每次进入这个锁相关的同步块就不须要再进行任何同步操做。
当有另外一个线程去尝试获取这个锁对象,偏向状态就宣告结束,此时偏向取消后恢复到未锁定状态或者轻量级锁状态。
偏向锁得到锁的步骤分为: