第二章:Java并行基础
2.1 进程和线程
- 进程:系统进行资源分配和调度的基本单位;
- 线程:程序执行的最小单位。
进程是线程的容器。
2.2 线程的基本操作
2.2.1 新建线程
Thread t1 = new Thread(); //创建线程对象
t1.start();//启动线程
start()会新建一个线程,并让这个线程执行run()方法,所以如果我们需要新的线程执行什么,就需要重写run()方法。
例:
Thread t1 = new Thread(){//使用匿名类重写run()方法
@Override
public void run(){
System.out.println("Hello,I am t1");
}
};
t1.start();
更合理更常用的启动新线程的方法:(以下来自《Head First Java》)
①建立Runnable对象(线程的任务)
Runnable threadJob = new MyRunnable();
MyRunnable类是继承自Runnable接口的,这个接口只有一个方法run(),必须要重写run()方法。
②建立Thread对象,并赋值Runnable(任务)
Thread myThread = new Thread(threadJob);
③启动Thread
myThread.start();
start()方法会执行Runnable(任务)的run()方法。
完整示例:
public class MyRunnable implements Runnable{
public void run(){
go();
}
public void go(){
System.out.println("Hello,I am myThread!");
}
}
class ThreadTester{
public static void main(String [] args){
Runnable threadJob = new MyRunnable(); //①
Thread myThread = new Thread(threadJob); //②
myThread.start(); //③
}
}
2.2.2 终止线程
线程Thread提供了一个stop()方法,可以立即终止线程。但是此方法并不推荐使用,因为该方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
正确的方法是自己写一个stopMe()的方法:
例子:
public static class ChangeObjectThread extends Thread {
volatile boolean stopme = false;
public void stopMe() {
stopme = true;
}
@Override
public void run() {
while (true) {//一直执行,直到调用stopMe方法
if (stopme) {
System.out.println("exit by stop me");
break;
}
synchronized (u) {
int v = (int) (System.currentTimeMillis() / 1000);
u.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();//yield()是让出资源
}
}
}
2.2.3 线程中断
“线程中断并不是线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程得到通知后如何处理,则完全由目标线程自行决定。”
三个与线程中断有关的方法:
public void Thread.interrupt();//中断线程(通知目标线程中断,即设置中断标志位)
public boolean Thread.isInterrupted();//判断是否中断(通过检查中断标志位)
public static boolean Thread.interrupted();//判断是否中断,并清除当前中断状态
例子:
public class CreateThread implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {//如果检测到当前线程被标志为中断状态,则结束执行
System.out.println("Interrupted");
break;
}
try {
Thread.sleep(2000);//休眠2s,当线程休眠中被中断会抛出异常
} catch (InterruptedException e) {
System.out.println("Interrupted When Sleep");
Thread.currentThread().interrupt();//再次设置终端标记位
}
Thread.yield();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new CreateThread());
t1.start();
Thread.sleep(1000);//休眠1s
t1.interrupt();//将t1设置为中断状态,需要在run()中进行中断处理
}
}
注:Thread.sleep()方法由于中断而抛出异常,此时它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中再次设置中断标记位。
2.2.4 等待(wait)和通知(notify)
wait()方法和notify()方法是在Object类中的,任何对象都可以调用这两个方法。
当在一个对象实例上调用wait()方法后,那么该对象就会停止继续执行,而转为等待状态,等到其他线程调用了notify()方法为止。
wait()和notify()的工作流程:
如果一个线程调用了object.wait(),那么它就会进入object对象的等待队列。这个等待队列中可能会有多个线程,因为系统运行多个线程同时等待某一个对象(资源)。当object.notify()被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。
图示:(注:notifyAll():唤醒等待队列中所有等待的线程。)
注:wait()方法和notify()方法是包含在相应的synchronized语句中的,即在调用wait()或者notify()之前都需要先取得object监视器,执行完后要释放object监视器。
例子:
public class SimpleWN {
final static Object object = new Object();
public static class T1 extends Thread {
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T1 start!");
try {
System.out.println(System.currentTimeMillis() + ":T1 wait for object ");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + ":T1 end! ");
}
}
}
public static class T2 extends Thread {
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T2 start! notify one thread ");
object.notify();
System.out.println(System.currentTimeMillis() + ":T2 end! ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
运行结果:
如上,在T2唤醒T1后,T1并不能立即执行,而需等到T2释放了object的锁之后,T1成功获得了object的锁才能继续执行。
2.2.5 挂起(suspend)和继续执行(resume)进程
被挂起的进行必须等到resume()方法操作之后才能继续执行。
不推荐使用这对方法,因为suspend()方法在导致线程暂停的同时并不会释放任何锁资源,可能会引发类似死锁的糟糕情况。
等待线程结束(join)和谦让(yield)
public final void join() throws InterruptedException
public final synchronized void join(long millis)throws InterruptedException
join():表示无线等待,它会一直阻塞当前线程,直到目标线程执行完毕。
join(long millis):给出了一个最大等待时间,如果超出给定时间目标线程还在执行,当前线程也会因为等待不及了而继续往下执行。
例子:
public class JoinMain {
public volatile static int i = 0;
public static class AddThread extends Thread {
public void run() {
for (i = 0; i < 10000000; i++) ;
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
如上,主函数中如果不是用join()等待AddThread,那么得到的i 很可能是0或者一个非常小的数字,但在join()后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,故在join()返回时,AddThread已经执行完了,故i 永远是10000000。
public static native void yield();
yield():使当前线程让出CPU。(让出CPU并不代表当前线程不执行了,当前线程让出CPU后还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了)
2.3 volatile与Java内存模型(JMM)
当你用 volatile去申明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到” 这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。
2.4 分门别类的管理:线程组
在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。
线程组的使用:
// 创建线程组
ThreadGroup tg = new ThreadGroup("PrintGroup");
// 使用Thread构造方法指定线程所属的县城组
Thread t = new Thread(tg,new ThreadTest(),"T1");
完整的使用例子:
public class ThreadGroupName implements Runnable {
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("PrintGroup");
Thread t1 = new Thread(tg,new ThreadGroupName(),"T1");
Thread t2 = new Thread(tg,new ThreadGroupName(),"T2");
t1.start();
t2.start();
System.out.println(tg.activeCount());
tg.list();
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName()+ "-" + Thread.currentThread().getName();
while(true) {
System.out.println("I am " + groupAndName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.5 驻守后台:守护线程(Daemon)
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部结束,这也意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。因此,当一个Java 应用内,只有守护线程时,Java 虚拟机就会自然退出。
守护线程的使用:
public class DaemonDemo {
public static class DaemonT extends Thread{
public void run(){
while (true){
System.out.println("I am Alive");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t=new Daemon();
t.setDaemon(true);//设置守护线程(该设置一定要在start()之前设置,否则设置无效)
t.start();
Thread.sleep(2000);
}
}
如上,t被设置成守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时整个程序也随之结束。若没将t设置为守护线程,则main线程结束后t线程还会不停的打印,永远不会结束。
2.6 线程优先级
Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这只是一个概率问题。如果运气不好,高优先级线程可能也会抢占失败。
2.7 线程安全的概念与关键字synchronized
线程安全就是并行程序的根基。
关键字synchronized可用于保证线程安全:
关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
关键字 synchronized 可以有多种用法:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
除了用于线程同步、确保线程安全外,synchronized 还可以保证线程间的可见性和有序性。 从可见性的角度上讲,synchronized 可以完全替代 volatile 的功能,只是使用上没有那么方便。就有序性而言,由于 synchronized 限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决(换言之,被 synchronized 限制的多个线程是串行执行的)。
2.8 程序中隐蔽的错误
详见《实战Java高并发程序设计》P61—P69。