【读书笔记】第二章:Java并行基础——《实战Java高并发程序设计》

第二章: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。

猜你喜欢

转载自blog.csdn.net/qq_43424037/article/details/113659484