进程和线程
- 在java语言中,并发编程最关心的是线程
一、进程
- 进程具有一个独立的执行环境
- 进程拥有一个完整的、私有的基本运行资源集合
- 每个进程都有自己的内存空间
- 一个单独的应用程序一般情况是一组相互协作的进程集合,至少是有一个进程的
二、线程
- 线程有时也被称为轻量级的进程
- 线程是在进程中存在的,每个进程最少有一个线程
- 进程和线程都提供了一个执行环境,但创建一个新的线程比进程需要的资源要少
- 线程共享进程的资源,包括内存和打开的文件
线程对象
- 在Java中,每个线程都是Thread类的实例
- 并发应用中一般有两种不同的线程创建策略:
- 直接控制线程的创建和管理,每当应用程序需要执行一个异步任务的时候就为其创建一个线程
- 将线程的管理从应用程序中抽象出来作为执行器,应用程序将任务传递给执行器,执行器负责执行
一、定义并启动一个线程
1、将线程对象和任务分离
- 这种方法更加灵活,适用于复杂的场景,一般就是这种用得多
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
2、直接继承 Thread 类
- Thread类自身已实现了Runnable接口,但它的run()方法中并没有定义任何代码
- 这里的 HelloThread 不能再继承其他类,使用有一定的局限性,但是简单场景更加易用
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new HelloThread()).start();
}
}
二、线程控制
1、线程暂停:sleep
- 在任何情况下,我们都不应该假定调用了
sleep()
方法就可以将一个线程暂停一个十分精确的时间周期 - 使用sleep()方法每四秒打印一个信息的例子
public class SleepMessages {
public static void main(String args[])
throws InterruptedException {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
for (int i = 0;
i < importantInfo.length;
i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
System.out.println(importantInfo[i]);
}
}
}
2、线程中断:interrupt
- 调用被中断线程的Thread对象的interrupt()方法,发送中断信号
- 调用 interrupt 方法仅仅是发送中断的通知,具体是否中断还是被通知的线程自己决定的
两种情况下的中断处理
- 一种是代码中使用了抛出 InterruptedException异常的方法(基于上面的信息打印)
for (int i = 0; i < importantInfo.length; i++) {
// Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// We've been interrupted: no more messages.
return;
}
// Print a message
System.out.println(importantInfo[i]);
}
- 另一种是没有任何的方法会抛出InterruptedException异常,需要自己周期性检查是否中断
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
中断标记
- 中断的处理通过线程内部的中断标志位来实现
- 中断标记的设置和清除
- 调用Thread.interrupt()设置这个标记,意味着向线程发送中断的信号
- 线程通过调用静态方法Thread.interrupted()检测中断会返回中断标志,返回后重置中断标志
- 非静态的isInterrupted()方法被线程用来检测其他线程的中断状态,不改变中断状态标记。
- 任何通过抛出一个InterruptedException异常退出的方法,当抛该异常时会清除中断状态。
线程等待:join
- Join()方法可以让一个线程等待另一个线程执行完成
- 若t是一个正在执行的Thread对象,
t.join()
将会使当前线程暂停执行并等待t执行完成 - join()方法响应中断并在中断时抛出InterruptedException
三、一个简单的线程示例
public class SimpleThreads {
// Display a message, preceded by
// the name of the current thread
static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
private static class MessageLoop
implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0;
i < importantInfo.length;
i++) {
// Pause for 4 seconds
Thread.sleep(4000);
// Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}
public static void main(String args[])
throws InterruptedException {
// Delay, in milliseconds before
// we interrupt MessageLoop
// thread (default one hour).
long patience = 1000 * 60 * 60;
// If command line argument
// present, gives patience
// in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument must be an integer.");
System.exit(1);
}
}
threadMessage("Starting MessageLoop thread");
long startTime = System.currentTimeMillis();
Thread t = new Thread(new MessageLoop());
t.start();
threadMessage("Waiting for MessageLoop thread to finish");
// loop until MessageLoop
// thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
// Wait maximum of 1 second
// for MessageLoop thread
// to finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience)
&& t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
// Shouldn't be long now
// -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}
同步
- 线程间的通信主要是通过共享域和引用相同的对象。这种通信方式非常高效
- 不过可能会引发两种错误:线程干扰和内存一致性错误
- 防止这些错误发生的方法是同步,不过同步会引起线程竞争,降低代码的执行速度
一、线程干扰
- 当两个运行在不同的线程中却作用在相同的数据上的操作交替执行时,就发生了线程干扰。
- 这意味着这两个操作都由多个步骤组成,而步骤间的顺序产生了重叠
模拟出现线程干扰的示例代码
- Counter 类
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
- MyThread 类
public class MyThread extends Thread {
private Counter counter;
private String opera;
MyThread(Counter counter, String opera) {
this.counter = counter;
this.opera = opera;
}
@Override
public void run() {
switch (opera) {
case "+":
for (int i = 0; i < 1000; i++) {
counter.increment();
System.out.println("当前线程:" + currentThread().getName() + " c的值:" + counter.value());
}
break;
default:
for (int i = 0; i < 1000; i++) {
counter.decrement();
System.out.println("当前线程:" + currentThread().getName() + " c的值:" + counter.value());
}
}
}
}
- 测试代码
public class Test1 {
@Test
public void test1() {
Counter counter = new Counter();
MyThread mt1, mt2;
mt1 = new MyThread(counter, "+");
mt2 = new MyThread(counter, "-");
mt1.start();
mt2.start();
try {
Thread.sleep(9000);
} catch (InterruptedException exp) {
exp.printStackTrace();
return;
}
}
}
- 最后的打印结果可能出现下面的情况:这里是一次0线程切换到1线程的一个过程,造成这样的原因就是在前一次的线程切换的时候,c 的值实际发生了改变但是还没来得及打印,CPU就从线程1切换到了0,等到线程0有了CPU时间,首先会打印上次没有打印的,所以就出现了打印值的而跳跃现象
二、内存一致性错误
- 当不同的线程对相同的数据产生不一致的视图时会发生内存一致性错误
- 避免内存一致性错误的关键是理解 happens-before 关系,这种关系确保一个特定语句的写内存操作对另外一个特定的语句可见
- 上面的这种关系个人的理解就是可以确定的代码执行的先后顺序,就是在有多个线程执行的环境下,我们能够完全确定的某些代码执行的先后的顺序
能够确定 happens-before 关系的情况
- 使用同步,后面会详细讲述
- 当一条语句调用Thread.start方法时,start 语句之前的代码的执行结果对线新线程是可见的。
- 当一个线程终止并且当导致另一个线程中Thread.join返回时,被终止的线程中代码的执行结果对执行join操作的线程是可见的。
三、同步方法&同步代码块
1、同步方法
- 要让一个方法成为同步方法,只需要在方法声明中加上synchronized关键字
- 同步(synchronized)方法使用一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,对象域上的所有读写操作都是通过synchronized方法来完成的(final域完全不用担心)
- 当创建一个在多个线程中共享的对象时,要特别小心对象的引用提早泄漏出去,避免出现对象的构建还没完成其他线程中就在通过引用对它进行操作了
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
2、对象的内部锁
- 每个对象都有一个与之关联的内部锁。
- 通常当一个线程需要排他性的访问一个对象的域时,首先需要请求该对象的内部锁,当访问结束时释放内部锁
- 没有锁的线程只能等待持有内部锁的线程执行完毕释放锁,这里就自然而然建立起了一个先后关系
- 同步方法就是依靠对象的内部锁完成功能的
- 对于静态同步方法的访问实际上得到的是类对象的内部锁,这和前面提到的内部锁是有差异的
3、同步代码块
- 同步代码块必须指定所请求的是哪个对象的内部锁
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
- 使用同步块对于更细粒度的同步很有帮助,下面的例子中需要十分确定c1和c2是彼此无关的域
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
4、锁的重入
- 当一个线程得到一个对象的锁时,如果再次请求此对象的锁时是可以再次获取该对象的锁的
- 这也证明在一个synchronized方法/块的内部调用本类中的其他synchronized的方法/块时,是永远可以得到锁的
public class Service {
synchronized public void service1() {
System.out.println("service1");
service2();
}
synchronized public void service2() {
System.out.println("service2");
service3();
}
synchronized public void service3() {
System.out.println("service3");
}
}
四、原子访问
- 编程操作中的原子操作的概念和数据库中的原子的概念是相似的
- 就是一个任务分很多步骤进行,这些步骤要么全部成功要么全部失败
- 无论成功或者失败操作完成前,对外是不可见的
1、原子操作定义
- 对引用变量和大部分基本类型变量(除long和double之外)的读写是原子的。
- 对所有声明为volatile的变量(包括long和double变量)的读写是原子的。
2、相关说明
- 原子操作不会交错,不必担心线程干扰,但是还是可能发生内存一致性错误
- 使用简单的原子变量访问比通过同步代码来访问更高效
活跃度
一、死锁
- 死锁是两个或多个线程永久阻塞,互相等待对方释放资源
- 示例中的二人在执行 bow 方法的时候都没有释放自己的对象锁,所以相应的 bow 方法里面就无法调用 bowback 方法,二者都在等待对方释放锁
public class FriendTest {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s"
+ " has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s"
+ " has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new Runnable() {
public void run() {
alphonse.bow(gaston);
}
}).start();
new Thread(new Runnable() {
public void run() {
gaston.bow(alphonse);
}
}).start();
}
}
二、饥饿
- 饥饿是指当一个线程不能正常的访问共享资源并且不能正常执行的情况
- 这通常在共享资源被其他“贪心”的线程长期占有时发生的
三、活锁
- 活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。
- 尽管这个问题不会阻塞线程,但是程序也无法继续执行。
- 活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。
- 解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务
保护块
- 多线程之间经常需要协同工作,最常见的方式是使用Guarded Blocks,它循环检查一个条件(通常初始值为true),直到条件发生变化才跳出循环继续执行
- 基本的使用方式就是一个循环,循环里面通过 wait 和 notify 等方法来进行线程间的通讯
一、生产者&消费者示例
1、消息容器Drop
public class Drop {
private String message;
private boolean empty = true;
public synchronized String take() {
while (empty) {
try {
wait();
} catch (InterruptedException e) {
}
}
empty = true;
notifyAll();
return message;
}
public synchronized void put(String message) {
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
}
}
empty = false;
this.message = message;
notifyAll();
}
}
2、消息生产者
public class Producer implements Runnable {
private Drop drop;
public Producer(Drop drop) {
this.drop = drop;
}
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
Random random = new Random();
for (int i = 0;
i < importantInfo.length;
i++) {
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {
}
}
drop.put("DONE");
}
}
3、消息消费者
public class Consumer implements Runnable {
private Drop drop;
public Consumer(Drop drop) {
this.drop = drop;
}
public void run() {
Random random = new Random();
for (String message = drop.take();
!message.equals("DONE");
message = drop.take()) {
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {
}
}
}
}
4、测试代码
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = new Drop();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
关于wait、notify 和 notifyAll 方法的说明
- wait 方法直接是当前的线程释放锁,让出CPU,进入等待的状态
- notify 和notifyAll 方法会唤醒一个或者多个等待的线程,然后继续向下执行直到执行完同步代码或者中间遇到wait,释放锁
- 实际编程中,notify 方法后面不要有过多的逻辑,应该最好是立即执行完释放锁
不可变对象
- 一个对象如果在创建后不能被修改,那么就称为不可变对象。
- 在并发编程中,一种被普遍认可的原则就是:尽可能的使用不可变对象来创建简单、可靠的代码。
- 由于创建后不能被修改,所以不会出现由于线程干扰产生的错误或是内存一致性错误。
- 创建不可变对象会增加创建对象的开销,但使用不可变对象降低了垃圾回收所产生的额外开销,也减少了用来确保使用可变对象不出现并发错误的一些额外代码,基本是可以抵消负面的影响的
一、通常可变对象的使用
1、同步颜色类的定义
public class SynchronizedRGB {
// Values must be between 0 and 255.
private int red;
private int green;
private int blue;
private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public SynchronizedRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public void set(int red,
int green,
int blue,
String name) {
check(red, green, blue);
synchronized (this) {
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
}
public synchronized int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public synchronized String getName() {
return name;
}
public synchronized void invert() {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
name = "Inverse of " + name;
}
}
2、同步颜色类的使用
- 如果有另外一个线程在Statement 1之后、Statement 2之前调用了color.set方法,那么myColorInt的值和myColorName的值就会不匹配。
- 为了避免出现这样的结果,必须要像下面这样使用同步代码块把这两条语句绑定到一块执行
SynchronizedRGB color =
new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
synchronized (color) {
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
}
二、定义不可变对象的策略
1、定义的策略
- 所有成员变量必须是private
- 最好同时用final修饰(非必须)
- 不提供能够修改原有对象状态的方法
- 最常见的方式是不提供setter方法
- 如果提供修改方法,需要新创建一个对象,并在新创建的对象上进行修改
- 通过构造器初始化所有成员变量,引用类型的成员变量必须进行深拷贝(deep copy)
- getter方法不能对外泄露this引用以及成员变量的引用
- 最好不允许类被继承(非必须),将类声明为final或者将构造器私有
参考:java中的不可变对象
2、示例代码的改造
final public class ImmutableRGB {
// Values must be between 0 and 255.
final private int red;
final private int green;
final private int blue;
final private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public ImmutableRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public String getName() {
return name;
}
public ImmutableRGB invert() {
return new ImmutableRGB(255 - red,
255 - green,
255 - blue,
"Inverse of " + name);
}
}
高级并发对象
- 锁对象提供了可以简化许多并发应用的锁的惯用法。
- Executors为加载和管理线程定义了高级API。Executors的实现由java.util.concurrent包提供,提供了适合大规模应用的线程池管理。
- 并发集合简化了大型数据集合管理,且极大的减少了同步的需求。
- 原子变量有减小同步粒度和避免内存一致性错误的特征。
- 并发随机数(JDK7)提供了高效的多线程生成伪随机数的方法
一、锁对象
- 每次只有一个线程可以获得锁对象,通过关联Condition对象,锁对象也支持wait/notify机制
- 锁对象有能力在当前锁对象不可用或者锁请求超时收回获得锁的尝试
- 使用锁对象很容易解决我们在活跃度中见到的死锁问题
死锁示例的改进
- 就是通过 impendingBow 方法进行锁的获取,最终达到的效果是,要么同时获取到两把锁,要么我一把锁也不要,这样就能完全避免一人一把锁,出现死锁的问题
public class Safelock {
static class Friend {
private final String name;
private final Lock lock = new ReentrantLock();
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public boolean impendingBow(Friend bower) {
Boolean myLock = false;
Boolean yourLock = false;
try {
myLock = lock.tryLock();
yourLock = bower.lock.tryLock();
} finally {
if (!(myLock && yourLock)) {
if (myLock) {
lock.unlock();
}
if (yourLock) {
bower.lock.unlock();
}
}
}
return myLock && yourLock;
}
public void bow(Friend bower) {
if (impendingBow(bower)) {
try {
System.out.format("%s: %s has"
+ " bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
} finally {
lock.unlock();
bower.lock.unlock();
}
} else {
System.out.format("%s: %s started"
+ " to bow to me, but saw that"
+ " I was already bowing to"
+ " him.%n",
this.name, bower.getName());
}
}
public void bowBack(Friend bower) {
System.out.format("%s: %s has" +
" bowed back to me!%n",
this.name, bower.getName());
}
}
static class BowLoop implements Runnable {
private Friend bower;
private Friend bowee;
public BowLoop(Friend bower, Friend bowee) {
this.bower = bower;
this.bowee = bowee;
}
public void run() {
Random random = new Random();
for (; ; ) {
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
}
bowee.bow(bower);
}
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new BowLoop(alphonse, gaston)).start();
new Thread(new BowLoop(gaston, alphonse)).start();
}
}
二、执行器(Executors)
- 实际大规模开发中比较好的实践是将线程的创建与管理和程序的其他部分分离开,封装这些功能的对象就是执行器
1、执行器接口
- java.util.concurrent中包括三个Executor接口:
- Executor,一个运行新任务的简单接口。
- ExecutorService,扩展了Executor接口。添加了一些用来管理执行器生命周期和任务生命周期的方法。
- ScheduledExecutorService,扩展了ExecutorService。支持Future和定期执行任务。
Executor接口
- 里面只有一个
e.execute(r);
其中的r就是Runnable - 这个方法是没有任何的实现的,需要自己来定义
ExecutorService
- 提供了execute方法的同时,新加了更加通用的submit方法
- submit方法可以接受Runnable对象作为参数,还可以接受Callable对象作为参数
- 通过submit方法返回的Future对象可以读取Callable任务的执行结果,或是管理Callable任务和Runnable任务的状态
- ExecutorService也提供了批量运行Callable任务的方法
- ExecutorService还提供了一些关闭执行器的方法
ScheduledExecutorService
- 调用schedule方法可以在指定的延时后执行一个Runnable或者Callable任务
- 还定义了按照指定时间间隔定期执行任务的scheduleAtFixedRate方法scheduleWithFixedDelay方法
2、线程池
- 在大规模并发应用中,创建大量的Thread对象会占用占用大量系统内存,分配和回收这些对象会产生很大的开销,使用线程池来管理线程就很方便了,而且可以减小开销
- 最常用的就是一种固定大小的线程池,内部维护了一个任务队列
Executors创建线程池
- newCachedThreadPool方法创建了一个可扩展的线程池,适合用来启动很多短任务的应用程序。
- newSingleThreadExecutor方法创建了每次执行一个任务的执行器
- newFixedThreadPool方法创建一个固定大小的线程池
3、Fork/Join
- fork/join框架是ExecutorService接口的一种具体实现
- 有助于更好地利用多处理器,尽可能利用多的算力
- 采用了工作窃取算法,大大提高了任务的执行效率
- 为那些能够被递归地拆解成子任务的工作类型量身设计
- fork/join框架的核心是ForkJoinPool类,可以执行ForkJoinTask任务
三、并发集合&原子变量&并发随机数
1、并发集合
- BlockingQueue定义了一个先进先出的数据结构,当你尝试往满队列中添加元素,或者从空队列中获取元素时,将会阻塞或者超时。
- ConcurrentMap是java.util.Map的子接口,定义了一些有用的原子操作。移除或者替换键值对的操作只有当key存在时才能进行,而新增操作只有当key不存在时。使这些操作原子化,可以避免同步。ConcurrentMap的标准实现是ConcurrentHashMap,它是HashMap的并发模式。
- ConcurrentNavigableMap是ConcurrentMap的子接口,支持近似匹配。ConcurrentNavigableMap的标准实现是ConcurrentSkipListMap,它是TreeMap的并发模式。
- 所有这些集合,通过 在集合里新增对象和访问或移除对象的操作之间,定义一个happens-before的关系,来帮助程序员避免内存一致性错误
2、原子变量
- java.util.concurrent.atomic包定义了对单一变量进行原子操作的类
- 同一变量上的一个set操作对于任意后续的get操作存在happens-before关系
Counter的改写
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.incrementAndGet();
}
public void decrement() {
c.decrementAndGet();
}
public int value() {
return c.get();
}
}
3、并发随机数
- 对于并发访问,使用TheadLocalRandom代替Math.random()可以减少竞争,从而获得更好的性能
int r = ThreadLocalRandom.current().nextInt(4,77);
参考:https://www.iteye.com/magazines/131-Java-Concurrency