2.1 有关线程你必须知道的事
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。线程就是轻量级进程,是程序执行的最小单位。
2.2 初始线程:线程的基本操作
2.2.1 新建线程
(1)继承Thread类,重写run()方法
Thread t=new Thread(); t.start(); |
默认情况下,Thread的run()方法什么也没有做,因此,一个线程一启动就结束了。
如果想让线程做点什么,就必须重写run()方法,把你的“任务”填进去。
//此代码采用匿名内部类,重写run()方法 Thread t=newThread(){ @Override public void run(){ System.out.println(“I am 线程t”) } }; t.start(); |
(2)实现Runnable接口
Runnable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable{ public abstract void run(); } |
2.2.2 终止线程
(1)stop()方法:stop()方法已被废弃,因为它太过暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
(2)设置标记变量:用于指示线程是否需要退出
2.2.3 线程中断
严格地讲,线程中断并不会使线程立即退出,而是给目标线程一个通知,告知目标线程有人希望你退出了。
与线程中断有关的3个方法:
1.public void Thread.interrrupt() //中断线程 |
2.public boolean Thread.isInterrrupted() //判断是否被中断 |
3.public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态 |
方法签名:方法名+参数列表
Thread.sleep()方法签名:public static native void sleep(long millis) throws InterruptedException
InterruptedException是非运行时异常,程序必须捕获并处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。
注意:Thread.sleep()方法由于中断而抛出异常,此时,他会清除中断标记,如果不加出理,那么下一次循环开始时,就无法捕获这个中断,故异常出理中,再次设置中断标记位。
2.2.4 等待(wait)和通知(notify)
wait()和notify()方法位于Object类中,任何对象都可调用这两个方法。
方法签名:
public final void wait() throws InterruptedException |
public final native void notify() |
当在一个对象实例上调用wait()方法时候,当前线程就会在这个对象上等待。等到何时结束?等到其它线程调用了notify()方法为止。
多个线程同时等待某一个对象,当object.notify()被调用时,他就会从这个等待队列中,随机选择一个线程,并将其唤醒。这是不公平的。
Object对象还有一个类似的notifyAll()方法,它和notify()的功能基本一致,不同的是,它会唤醒在这个等待队列中所有等待的线程。
注意:Object.notify()并不是可以随意调用,它必须包含在对应的synchronized语句中,无论是wait()和notify()都需要首先获得目标对象的一个监视器。
注意:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间,除了wait()可被唤醒外,另外一个主要区别就是wait()ff会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。
2.2.5 挂起(suspend)和继续执行(resume)线程
一对相反的操作。它们已被标记为废弃方法,不推荐使用。
不推荐使用suspend()去挂起线程的原因:suspend()导致线程暂停的同时,它并不会释放任何锁资源。其它任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续执行。
2.2.6 等待线程结束(join)和谦让(yield)
join()
一个线程的输入可能依赖另外一个或多个线程的输出
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException |
Thread.yield()
public static native void yield();
这是一个静态方法,一旦执行,它会使当前线程让出CPU。
注意:让出CPU并不表示当前线程不执行了,当前线程在让出CPU后,还会进行CPU的争夺。
3.1.1 Synchronized的功能扩展:重入锁
重入锁可以完全替代Synchronized关键字。
重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。
重入锁有着显示的操作过程,开发人员必须手动指定何时加锁,何时释放锁。
注意:退出临界区时,记得释放锁,否则,其它线程就没有机会再访问临界区了。
重入:一个线程可以反复进入。
ReentrantLock lock=new ReentrantLock();//创建一个重入锁对象
lock.lock(); lock.lock(); try{ i++; }finally{ lock.unlock(); lock.unlock(); }
|
中断响应
注意:如果同一个线程多次获得锁,那么释放锁时,也需要释放相同的次数。如果释放锁的次数多,会得到一个java.lang.IllegalMonitorStateException,反之,如果释放次数少了,相当于该线程还持有锁,其它线程无法进入临界区。
对于Synchronized,如果一个线程在等待锁,只有2种情况:(1)它获得锁继续执行(2)保持等待
重入锁可以提供中断处理的能力。
中断: 等待锁的过程中,程序可以根据需要取消对锁的请求。
锁申请等待限时
公平锁
公平锁特点:不会产生饥饿现象。
如果使用Syschronized进行锁控制,那么产生的锁就是非公平的。而重入锁可以允许我们对其公平性进行设置。
public ReentrantLock(boolean fair) |
当参数fair为true时,表示锁是公平的。
实现公平锁要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下,因此,默认情况下锁是非公平的 。如果没有特别的要求,也不需要使用公平锁。公平锁和非公平锁在线程调度表现上非常不一样。
ReentrantLock几个重要的方法:
lock():获得锁,如果锁已经被占用,则等待
lockInterruptibly():获得锁,但优先响应中断。
tryLock():尝试获得锁,如果成功,则返回true,失败返回false。该方法不等的,立即返回。
tryLock(long time,TimeUnit unit):给定时间尝试获得锁。
unlock():释放锁。
在重入锁的实现中主要包含3个要素:
(1)原子状态
(2)等待队列
(3)阻塞原语park()和unpark(),用来挂起和恢复线程。
3.1.2 重入锁的好搭档:Condition条件
它和Object.wait()和Object.notify()的作用大致相同。Object.wait()、Object.notify()与Syschronized关键字结合使用。Condition条件和ReentrantLock结合使用。
Condition是接口。
在singal()方法调用后,系统会从当前Condition对象的等待队列中,唤醒一个线程。
在singal()方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。
3.1.3 允许多个线程同时访问:信号量(Semaphore)
广义上说,信号量是对锁的扩展。内部锁Synchronized和重入锁ReentrantLock,一次都只允许一个线程访问资源。信号量可以指定多个线程,同时访问某一个资源。信号量提供主要构造函数:
public Semaphore(int permits); public Semaphore(int permits, boolean fair); //第二个参数可以指定是否公平 |
注意:在构造信号量对象时,必须指定信号量的准入数,即同时能申请多少个许可。
信号量的主要逻辑方法:
public void acquire() public void acquireUninterruptibly() public boolean tryAcquire() public boolean tryAcquire(long timeout, TimeUnit unit) public void release() |
3.1.4 ReadWriteLock读写锁
ReadWriteLock读写锁是JDK5提供的读写分离锁。它可以有效帮助减少锁竞争,以提升系统性能。
3.1.5 倒计时器:CountDownLatch
它是多线程控制工具类。它通常用来控制线程等待,它可以让某一线程等待直到倒计时器结束,再开始执行。
典型场景:火箭发射
构造方法:
public CountDownLatch(int count)//参数:当前这个计数器的计数个数 |
主线程在CountDownLatch上等待,当所有检查任务全部完成后,主线程方能继续执行。
3.1.5 循环栅栏:CyclicBarrier
它是多线程并发控制工具。它也可以实现线程间的计算等待,但它的功能比CountDownLatch更加复杂且强大。
使用场景:如司令下达命令(要求10个士兵一起去完成一个任务:10个士兵先集合,去做任务。当10个士兵的任务都完成了,司令才能对外宣布任务完成)
它比CountDownLatch略微强大一些,CyclicBarrier可以接收一个参数作为barrierAction。
barrierAction:当计数器一次计数完成后,系统会执行的动作。
构造方法:
public CyclicBarrier(int parties,Runnable barrierAction)//parties:计数总数,也就是参与的线程总数 |
3.1.6 线程阻塞工具类:LockSupport
它是方便实用的线程阻塞工具类。它可以在线程内的任意位置让线程阻塞。
和Thread.suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。
和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。
LockSupport的静态方法park()可以阻塞当前线程,类似还有ParkNanos()、parkUntil()等方法。它们实现一个限时的等待。
3.2 线程复用:线程池
3.2.1 什么是线程池
3.2.2 不要重复发明轮子:JDK对线程池的支持
为了更好控制多线程,JDK提供了一套Executor。
3.2.3 刨根究底:核心线程池的内部实现
3.2.4 负载了怎么办:拒绝策略
JDK内置4种拒绝策略:
3.2.5自定义线程创建:ThreadFactory
线程池主要作用:为了线程复用,避免了线程的频繁创建。
线程池线程来源:ThreadFactory
ThreadFactory是一个接口,它只有一个方法,用来创建线程:
自定义线程池作用:跟踪线程池究竟在何时创建了多少线程、自定义线程名称、组以及优先级、可以将所有的线程设置为守护线程。
3.2.6 我的应用我做主:扩展线程池
ThreadPoolExecutor是一个可以扩展的线程池。
3.2.7 合理的选择:优化线程池中线程的数量
3.2.8 堆栈去哪里了:在线程池中寻找堆栈
3.2.9 分而治之:Fork/Join框架
“分而治之”
Fork:餐叉
join()表示等待。
recursive:回归的、递归的
在Linux平台中,函数fork()用来创建子进程,使得系统进程多一个执行分支。Java沿用类似的命名方式。
使用fork()后系统多了一个执行分支(线程)。
分而治之:
注意:使用ForkJoin时,如果任务划分的层次很深,一直得不到返回,那么可能出现两种情况:(1)系统内的线程数量越积越多,导致性能严重下降(2)函数的调用层次变得很深,最终导致栈溢出。
3.3 不要重复发明轮子:JDK的并发容器
3.3.1 超好用的工具类:并发集合简介
JDK提供的这些容器大部分在java.util.concurrent中
ConcurrentHashMap:这是一个高效并发的HashMap。可以理解为一个线程安全的HashMap
CopyOnWriteArrayList:这是一个List,是和ArrayList一族,在读多写少的情况下,它性能好于Vector
ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看作一个线程安全的LinkedList。
BlockingQueue:这是一个接口,JDK内部通过链表、数组等实现了这个接口。表示阻塞队列,非常适合于数据共享的通道。
ConcurrentSkipListMap:跳表的实现,这是一个map,使用跳表的数据结构进行快速查找。
java.util下的Vector是线程安全的。Collections工具类可以帮助我们将任意集合包装成线程安全的集合。
3.3.2 线程安全的HashMap(HashMap本身不是线程安全的)
HashTable是线程安全的。
实现HashMap线程安全的方法:使用Collections.syschronizedMap()包装HashMap。
public static Map m=Collections.syschronizedMap(new HashMap()); |
Collections.syschronizedMap()会生成一个名为SyschronizedMap的Map。它使用委托,将自己的所有Map相关的功能交给传入的HashMap实现,而自己主要负责保证线程安全。
3.3.3 有关list的线程安全
ArrayList、LinkedList不是线程安全的,Vector是线程安全的。
list实现线程安全的方法:可以使用Collections.syschronizedList( )方法来包装任意list
public static List |
此时的list是线程安全的。
3.3.4 高效读写的队列:深度剖析ConcurrentLinkedQueue
它使用链表作为数据结构。
ConcurrentLinkedQueue应该算是高并发环境中性能最好的队列。它之所以有很好的性能,是因为内部复杂的实现。
高并发的环境下,激烈的锁竞争会导致程序性能的下降。
4.1 有助于提高“锁”性能的几点建议
4.1.1 减少锁持有时间
注意:减少锁的持有时间有助于减少锁冲突的可能性,进而提升系统的并发能力。
场景:处理正则表达式的Pattern类。
4.1.2 减少锁粒度
它是一种削弱多线程锁竞争的有效手段。
它通过分割数据结构来实现的。
场景:ConcurrentHashMap类的实现。
ConcurrentHashMap最重要的两个方法:get()和put()
减少锁粒度引入的新问题:当系统需要全局锁时,其消耗的资源比较多。
高并发环境下ConcurrentHashMap的size()的性能依然要差于同步的HashMap。
注意:所谓减少锁粒度,就是指缩小锁定对象的范围,从而减少锁冲突的可能性,进而提高系统的并发能力。
4.1.3 读写分离锁来替换独占锁
它是减少锁粒度的一种特殊情况。
读写锁是对系统功能点的分割。
场景:读多写少的场合,使用读写锁可以有效提高系统的并发能力。
4.1.4 锁分离
读写锁思想的进一步延伸就是锁分离。
典型案例:java.util.concurrent.LinkedBlockingQueue的实现
4.1.5 锁粗化
虚拟机遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作就叫锁粗化。
4.2 Java虚拟机对锁优化所做的努力
4.2.1锁偏向
锁偏向是一种针对加锁操作的优化手段。
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需再做任何同步操作。
几乎没有锁竞争的场合,偏向锁有比较好的优化效果。因为连续多次极有可能是同一线程请求相同的锁。
使用Java虚拟机参数-XX:+UseBiasedLock可以开启偏向锁。
biased:有偏见的
4.2.2 轻量级锁
如果偏向锁失败,虚拟机并不会立即挂起线程。他还会使用轻量级锁优化手段。
轻量级锁:它只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。
如果轻量级锁加锁失败,则表示其它线程抢先争夺到了锁,那么当前的锁请求就会膨胀为重量级锁。
4.2.3 自旋锁
锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力。
系统进行一次赌注:它会假设在不久的将来,线程可以的到这把锁。因此,虚拟机会让当前线程做几个空循环(自旋的含义),经过若干次循环后,如果获得锁,那么就进入临界区。如果还不能获得锁,才真实地将线程在操作系统层面挂起。
4.2.4 锁消除
它是更彻底的锁优化。
Java虚拟机在JIT编译时,通过运行上下文扫描,去除不可能存在共享资源竞争的锁。
局部变量是在线程栈上分配的,属于线程私有的数据。、
锁消除涉及一项关键技术为逃逸分析。
逃逸分析:观察某一变量是否会逃出某一个作用域。
4.3 人手一支笔:ThreadLocal
ThreadLocal是一个线程的局部变量。只有当前线程可以访问,是线程安全的。
//TODO
4.4.1 与众不同的并发策略:比较交换(CAS)compare and swap
它要比基于锁的方式拥有更优越的性能。其非阻塞性,它对死锁问题天生免疫。
使用无锁的方式。
CAS的算法过程:它包含3个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值。仅当V=E值时,才将V设为N;如果V值和E值不同,则说明已经有其它线程做了更新,则当前线程什么也不做。最后,CAS返回当前V的真实值。
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅会被告知失败,并且允许再次尝试,也允许失败的线程放弃操作。
4.4.2 无锁的线程安全整数:AtomicInteger
可以把它看成一个整数,但它与Integer不同,它是可变的,线程安全的。
对其修改等任何操作,都是用CAS指令进行的。
4.4.3 Java中的指针:Unsafe类
sun.misc.Unsafe封装了一些不安全的操作,它封装了一些类似指针的操作。
4.4.4
4.4.5
4.4.6
4.4.7 让普通变量也享受原子操作:AtomicIntegerFieldUpdater
4.4.8 挑战无锁算法:无锁的Vector实现
4.4.9 让线程之间互相帮助:细看SyschronizedQueue
4.5 有关死锁的问题
第5章 并行模式与算法
5.1探讨单例模式
它可以确保系统中一个类只产生一个实例。
在Java中这样的行为能带来两大好处:
(1) 对于频繁使用的对象,可以省略new操作花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销
(2)由于new操作次数的减少,因而对系统的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间
5.2 不变模式
尽可能地去除同步操作,提高并行程序的性能,使用一种不可改变的对象,依靠对象的不可变性,可以确保其在没有同步操作的多线程的环境中依然始终保持内部状态的一致性和正确性。
核心思想:一个对象一旦被创建,则它的内部状态永远不会改变。所以,没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。基于这些特性,对不变对象的多线程操作不需要进行同步控制。
不变模式的使用场景需要满足2个条件:
当对象创建后,其内部状态和数据不再发生任何变化
对象需要被共享,被多线程频繁访问
为确保对象创建后,不发生任何改变,并保证不变模式正常工作,只需要注意4点:
去除setter方法以及修改自身属性的方法
将所有属性设置为私有,并用final标记,确保其不可修改
确保没有子类没有重载修改它的行为
有一个可以创建完整对象的构造函数
在不变模式中,final关键字起到了非常重要的作用
5.3 生产者-消费者模式
生产者-消费者模式是一个经典的多线程设计模式。它通常有两类线程,即若干个生产者线程和若干个消费者线程。生产者负责提交用户请求,消费者负责具体处理生产者提交的任务。生产者和消费者之间通过共享内存缓冲区进行通信。
注意:生产者与消费者模式中的内存缓冲区的主要功能是数据在多线程中的共享,通过缓冲区,可以缓解生产者与消费者的性能差。
此模式的核心组件是共享内存缓冲区,它作为生产者与消费者的通信桥梁,避免了生产者与消费者直接通信,从而将生产者和消费者进行解耦。生产者不需要知道消费者的存在,消费者也不需要知道生产者的存在。
5.4 高性能的生产者与消费者:无锁的实现
BlockingQueue用于实现生产者与消费者一个不错的选择。它可以自然地实现作为生产者与消费者的内存缓冲区。但是BlockingQueue并不是一个高性能的实现。
5.4.1 无锁的缓存框架:Disruptor
5.5 Future模式
//TODO