第一章 走入并行世界
1、你必须知道的几个概念
(1)同步(Synchronized)和异步(Asynchronous)
同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。
(2)并发(Concurrency)和并行(Parallelism)
并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交的,一会儿运行任务A一会儿执千对于务B,系统会不停地在两者间切换。
(3)临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
(4)阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响。
(5)死锁(Deadlock )、饥饿(Starvation)和活锁(Livelock)
死锁:多个线程之间资源互相被占用,导致全都处于等待状态。
饥饿:饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
活锁:多个线程之间进行资源的相互“谦让”,导致资源在线程之间来回跳动。
2、并发的级别
由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为阻塞、无饥饿、无障碍、无锁、无等待。
3、回到Java:JMM
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。因此,我们首先必须了解这些概念。
(1)原子性(Atomicity)
原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
(2)可见性(Visibility)
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
(3)有序性(Ordering)
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程中,可能会因为指令重排导致有序性被破坏,运行出现错误。指令重排的好处在于提高了CPU 的处理性能。
但是有一些指令不能重排:Happen-Before规则
第二章 Java并行程序基础
1、线程与进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程就是轻量级进程,是程序执行的最小单位。使用多线程而不
是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
线程的所有状态:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
2、线程的基本操作
(1)新建线程
Thread t1 = new Thread();
t1.start();
那线程start()后,会干什么呢?这才是问题的关键。线程Thread,有一个run()方法,start()方法就会新建一个线程并让这个线程执行run()方法。
也可以通过继承Runnable接口来创建新线程。
(2)等待(wait)和通知(notify)
当一个对象调用wait方法以后,当前线程就会进入等待队列,并释放锁,一直等到其他线程调用了notify()为止,注意:当其他对象调用notify()时,会随机唤醒等待队列中的一个线程,这个选择是不公平的。notifyAll()可以将等待队列中的所有的线程唤醒。
(3)等待线程结束(join)和谦让(yield)
join()的调用会使得当前线程进入等待,直至加入的线程执行结束才能接着执行。join()的本质是让调用线程wait()在当前线程对象实例上。
yield()方法会让出CPU的执行权,但是还会进行CPU资源的争夺。
3、volatile与Java内存模型(JMM )
当你用volatile去申明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。
但是volatile并不能保证操作的原子性。
4、分门别类的管理:线程组
示例:
public class Num02 implements Runnable{
public static void main(String[] args) {
ThreadGroup print = new ThreadGroup("Print");
Thread t1 = new Thread(print, new Num02(), "T1");
Thread t2 = new Thread(print, new Num02(), "T2");
t1.start();
t2.start();
System.out.println(print.activeCount());
print.list();
}
@Override
public void run() {
}
}
上述代码建立了一个名字为print的线程组,将t1和t2加入中,个组中。其中,展示了线程组的两个重要的功能,activeCount()可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确,list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助。
5、驻守后台:守护线程(Daemon )
守护线程:系统的守护者,当一个Java应用内,只有 守护线程时,Java虚拟机就会自然退出。
6、线程优先级
从1到10表示线程的优先级。用setPripority()来进行线程优先级的设置。
7、线程安全的概念与synchronized
关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
关键字synchronized可以有多种用法。这里做一个简单的整理。
指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。
8、隐蔽错误
(1)并发下的ArrayList
ArrayList是线程不安全的。改进的方法可以使用Vector代替。
HashMap可以使用ConcurrentHashMap代替。
不可以将Integer这样的包装类作为锁对象,因为每次Integer改变都会创建一个新的Integer对象。
第三章 JDK并发包
1、多线程的团队协作:同步控制
(1)synchronized的功能扩展:重入锁
重入锁使用java.util.concurrent.locks. ReentrantLock类来实现。
class ReenterLock implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock t1 = new ReenterLock();
Thread t2 = new Thread(t1);
Thread t3 = new Thread(t1);
t2.start();
t3.start();
t2.join();
t3.join();
System.out.println(i);
}
}
开发人员必须手动 的释放锁资源。
lock锁是允许重入的。
中断响应:
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这
把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被
中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
锁申请等待限时:
在这里,***tryLock()***方法接收两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位设置为秒,时长为5,表示线程在这个锁请求中,最多等待5秒。如果超过5秒还没有得到锁,就会返回false。如果成功获得锁,则返回true。
公平锁
公平锁不会产生饥饿现象。
构造函数为:
public ReentrantLock(boolean fair)
当参数为true时,为公平锁,默认为非公平锁。
重入锁的实现包含以下三个要素:
(1)原子状态,使用CAS操作。
(2)等待队列。
(3)阻塞原语park和unpark。
(2) 重入锁的好搭档:Condition条件
await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似,signal()也会释放锁。
使用方法:
public static ReentrantLock lock = new ReentrantLock();
public static Condition con = lock.newCondition();
public void run() {
for (int j = 0; j < 10000000; j++) {
lock.lock();
try {
con.await();
i++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
con.signal();
lock.unlock();
}
}
}
(3)允许多个线程同时访问:信号量(Semaphore )
信号量可以指定多个线程,同时访问某一个资源。信号量提以下的构造函数:
public Semaphore(int permits)
public Semaphore(int permits,boolean fair)
在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。
主要的方法:
acquire()方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断。acquireUninterruptibly()方法和acquire()方法类似,但是不响应中断。tryAcquire()尝试获得一个许可,如果成功返回hue,失败则返回false,它不会进行等待,立即返回。release()用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。
意味着可以同时有5个线程同时进入第7-9行,**务必使用release()释放信号量。**否则会发生信号泄露。