《Java并发编程的艺术》第4章 Java并发编程基础 ——学习笔记
参考https://www.cnblogs.com/lilinzhiyu/p/8086235.html
4.1 线程简介
进程:操作系统在运行一个程序时,会为其创建一个进程。
线程:是进程的一个执行单元。在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。(处理器在这些线程上高速切换,让作用者感觉到这些线程在同时执行。)
1. 线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10(超出就会报出异常),
在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
2. 线程的状态
Java线程在运行的生命周期中可能处于下表所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态:
事实上,线程在整个运行过程中是会随着代码的运行而不断变化状态的:
- 线程创建后,调用start()方法开始运行(运行状态)。
- 当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时间到达时将会返回到运行状态。
- 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。
- 线程在执行Runnable的run()方法之后将会进入到终止状态。
3. Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。如下代码:
public class Daemon { public static void main(String[] args) { Thread thread = new Thread(new DeamonRunner(),"DeamonRunner"); thread.setDaemon(true); thread.start(); } static class DeamonRunner implements Runnable{ @Override public void run() { try { Thread.sleep(2000l); } finally { System.out.println("DeamonThread finally run."); } } } } 运行Deamon程序,可以看到在终端或者命令提示符没有任何输出。
没有任何输出原因:mian()线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机 中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。
4.2 启动和终止线程
4.2.1 构造线程
线程运行前需要先构造一个线程,下面是java.lang.Thread中对线程进行初始化的部分
从这里也能看到父线程与子线程的关系:
父线程就是当前线程(开启多线程的线程),子线程会具有与父线程一致的优先级, 守护线程,线程组,还会有父线程的可继承ThreadLocal。还会分配给一个唯一的ID。
init()运行完毕,线程对象就初始化好了,在堆内存中等待运行。
4.2.2 启动线程
线程完成初始化后,调用start()方法就可以启动这个线程,
线程start()的含义:当前线程同步告知JVM,只要线程规划器空闲,应立即启动调用start()方法的线程。
4.2.3 理解中断
中断:一个标识位属性,通过调用线程的 interrupt() 方法使其进入中断状态。
线程可以通过检查自身是否被中断来进行响应。
线程通过方法 isInterrupted() 来判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断进行复位。
注:线程已经结束,即使线程曾经处于中断状态,调用线程对象的isInterrupted()依旧会返回false。
只要线程进入打断状态(调用interrupt()方法),再调用sleep()会抛出异常InterruptedException。同时JVM会将线程的打断状态清空,此时再调用isInterrupted()会返回false。
4.2.4 过期的suspend()、resume()和stop()方法
suspend():暂停线程、resume():恢复线程、stop():停止线程,这三个方法都过期了。
原因:suspend()会导致线程占用资源进入休眠状态,容易导致死锁。stop()不能保证线程资源的正确释放,一旦调用直接结束,可能会导致程序运行在不确定的状态。
暂停恢复方法可以用后面的等待/通知机制完成。
4.2 线程间通信
线程自启动时,就拥有了自己的栈空间。然后会一直运行直到结束。
多线程的目的是多条线程执行不同的逻辑业务从而能够提升业务整体的响应速度,如果线程仅仅是孤零零的执行,这些不同的逻辑业务就不能最终汇聚成一个完整的业务那么多线程也就失去了意义,这就是为什么要有线程间通信的存在。
4.3.1 volatile与synchronized关键字
java支持多个线程访问一个对象或对象的成员变量,由于每个线程可以拥有这个变量的拷贝,所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
在不使用关键字时,每一个线程是从自己的内存区域获取相应对象的拷贝的。(线程有自己的内存区域,默认会将共享内存中的数据拷贝到自己的内存区域)
关键字volatile可以修饰字段(成员变量),就是告知程序,任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
注:过多地使用volatile是不必要的,因为它会降低程序执行的效率。
(前面已经提到每一个线程都有自己的内存区域,从自己的内存区域对值操作肯定最快的,使用了volatile的话就会对共享内存进行操作,相比之下自然速率就慢了)
关键字synchronized:修饰代码块、方法、静态方法。
实质上是对一个对象的监视器(monitor)的获取,而且这个获取过程是排他的,也就是说同一时刻只有一个线程获取由synchronized所保护对象的监视器。
任何对象都有自己的监视器,当对象由同步块或者对象的同步方法调用时,执行方法的线程必须先获取对象的监视器才能进入同步块或者同步方法,而没有获取监视器的线程会阻塞在同步块与同步方法的入口,进入BLOCKED(阻塞)状态。
由上图可以看到,任何线程对Object的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
4.3.2 等待/通知机制
等待/通知的相关方法是任意java对象都具备的,因为该方法被定义在所有对象的超类上java.lang.Object
等待通知机制:
线程A调用了对象O的wait()方法进入了等待状态,而线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
注:上述两个线程通过对象O来完成交互,而对象的wait()与notify()或notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
使用注意:
1)使用wait()、notify()、notifyAll()方法都需要先对调用对象加锁。(即锁对象应该为调用对象)
2)调用wait()方法后,线程状态由RUNNING(运行)变为WAITTING(等待),并将当前线程放到对象的等待队列。
3)notify()或notifyAll()方法调用后,等待线程依旧不会从waut()返回,需要调用notify()、notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
4)notify()方法将对象的等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的状态由WAITING变为BLOCKED。
5)从wait()返回的前提是获得了调用对象的锁。
public class WaitNotify { static boolean flag = true; static Object lock = new Object(); public static void main(String[] args) throws Exception{ Thread waitThread = new Thread(new Wait(),"WaitThread"); waitThread.start(); TimeUnit.SECONDS.sleep(1); Thread notifyThread = new Thread(new Notify(),"NotifyThread"); notifyThread.start(); } static class Wait implements Runnable{ @Override public void run() { //加锁,拥有lock的monitor synchronized (lock){ //当条件不满足时,继续wait,同时释放了lock的锁 while (flag){ try { System.out.println(Thread.currentThread() + " flag is true. wait @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.wait(); }catch (InterruptedException e){ } } //条件满足时,完成工作 System.out.println(Thread.currentThread() + "flag is false. running @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); } } } static class Notify implements Runnable{ @Override public void run() {
//加锁,拥有lock的monitor synchronized (lock){
//获取lock的锁,然后再进行通知,通知时不会释放lock的锁,直到当前线程释放了lock之后,WaitThread才能从wait方法返回 System.out.println(Thread.currentThread() + "hold lock. notify @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.notifyAll(); flag = false; try { Thread.sleep(5000); } catch (InterruptedException e) { } }
//再次加锁 synchronized (lock){ System.out.println(Thread.currentThread() + "hold lock again. sleep @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); try { Thread.sleep(5000); } catch (InterruptedException e) { } } } } }
运行结果:
Thread[WaitThread,5,main] flag is true. wait @ 13:22:12
Thread[NotifyThread,5,main]hold lock. notify @ 13:22:13
Thread[WaitThread,5,main]flag is false. running @ 13:22:18
Thread[NotifyThread,5,main]hold lock again. sleep @ 13:22:18
WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SychronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取锁并从wait()方法返回继续执行。
4.3.3 等待/通知的经典范式
经典范式可以分为两部分:等待方(消费者)与通知方(生产者)。
等待方遵循原则:
1)获取对象的锁
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足执行对应的逻辑
通知方遵循原则:
1)获得对象锁
2)改变条件
3)通知所有等待在对象上的线程
4.3.4管道输入/输出流
管道输入/输出流用于线程间的数据传输,传输的媒介是内存。
PipedOutputStream / PipedIntputStream
PipedReader / PipedWriter
注:对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法。如果没有将输入/输出流绑定起来,对于该流的访问将抛出异常。
public class StreamTest { public static void main(String[] args) throws Exception{ PipedWriter out = new PipedWriter(); PipedReader in = new PipedReader(); //将输入输出流进行连接,否则会爆出异常IOException out.connect(in); Thread printThread = new Thread(new Print(in),"PrintThread"); printThread.start(); int receive = 0; while ((receive = System.in.read()) != -1) { out.write(receive); } out.close(); } static class Print implements Runnable { private PipedReader in; public Print(PipedReader in) { this.in = in; } @Override public void run() { int receive = 0; try { while ((receive = in.read()) != -1) { System.out.println((char)receive); } }catch (IOException e) { e.printStackTrace(); } } } }
4.3.5 Thread.join()的使用
如果一个线程A执行了thread.join(),含义:当前线程A等待thread线程终止后才从thread.join()返回。
除了join()外还有join(long millis)和join(long millis,int nanos)两个具备超时的方法,这两个方法表示:如果线程thread没有在指定时间内停止,那么线程A会从该超时方法返回。
下面是Thread.join()的部分源码:
4.3.6 ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。
这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)来设置值,然后在当前线程下使用get()来获取原先设置的值。
注:只能放一个值。再次调用set设置值,会覆盖前一次set的值。