知识点:
- 线程简介 。
- 启动和终止线程 。
- 线程间通信 。
1.线程简介
1.1 什么是线程?
要想明白什么是线程,必须先明白什么是进程!现在操作系统在运行一个程序时,会为其创建一个进程。而线程是操作系统调度的最小单元,也叫轻量级进程。一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、和局部变量等属性,并能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程是在同时执行。
1.2 为什么要使用多线程?
- 更多的处理器核心:随着处理器上的核心数量越来越多,以及超线程技术的广泛应用,目前大多数计算机比以前更擅长并行计算,处理器性能的提升方式,也从更高的主频向更多的核心发展。如何利用好处理器上的多个核心也成了突出问题。 线程作为操作系统调度的最小单位,在一个时刻内只能运行在一个处理器核心上。如果不使用多线程,那么引入在多的处理器核心也对提升性能没有意义。
- 更快的响应时间: 有时候我们会编写一些较为复杂的代码,其中包含很多个操作。那么用户提交请求和需要等待所有操作全部执行完毕才可以。如果使用多线程,我们可以把不需要同步返回信息的操作交给其他线程处理,以缩短响应时间。
- 更好的编程模型:Java为多线程编程提供了良好、考究且一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,而不是绞尽脑汁的考虑如何将其多线程化。
1.3 线程优先级
现在操作系统基本采用分配时间片的方式调度运行的线程,线程会分配到若干时间片,当线程时间片用完后就会发生调度,等待下次分配。线程分配到的时间片多少决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或少分配处理器资源的线程属性。
在Java中,通过一个整型变量priority来控制优先级,优先级范围从1~10,在线程构建时可以使用setPriority(int)来修改优先级,默认为5,优先级高的线程分配时间片的数量要多于优先级低的线程。在不同的JVM及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
【备注】:设置优先级时,针对频繁阻塞的线程需要设置较高的优先级,而偏重计算的线程则设置较低的优先级,确保处理器不会被独占。
1.4 线程的状态
Java线程在运行的生命周期中可能处于下表所示的6中不同的状态,在给定的一个时刻,线程只能处于其中一个状态:
实例:
package com.lipeng.fourth;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) {
final Object object1=new Object();
final Object object2=new Object();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object1) {
TimeUnit.SECONDS.sleep(30);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Demo-Thread-1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object1) {
object1.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Demo-Thread-2");
Thread thread3=new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object2) {
object2.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Demo-Thread-3");
Thread thread4=new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object2) {
object2.wait(30000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Demo-Thread-4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
通过Java VisualVM查看线程栈信息:
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间切换。其状态转换图如下:
【备注】:Java将操作系统中的运行和就绪两个状态合并称为运行状态。
1.5 守护(Daemon)线程
守护线程是一种支持型线程,主要用作程序中后台调度以及支持性工作(如JVM中负责GC的线程就是守护线程)。当JVM中不存在非守护线程时,JVM将退出。可以通过调用Thread.setDaemon(true)方法将线程设置为守护线程。
实例一
代码:
package com.lipeng.fourth;
import java.util.concurrent.TimeUnit;
public class DaemonDemo {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("daemon thread start");
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
}finally{
System.out.println("daemon thread done");
}
}
});
thread.setDaemon(true);
thread.start();
}
}
运行结果:
解析:main线程在启动守护线程之后main方法终止,目前JVM中无非Daemon线程,JVM退出。JVM中所有Daemon线程立即终止,所以finally块中的代码没有执行。
【备注】:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
实例二
代码:
package com.lipeng.fourth;
public class DaemonDemo {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("daemon thread start");
} catch (Exception e) {
}finally{
System.out.println("daemon thread done");
}
}
});
thread.setDaemon(true);
thread.start();
}
}
运行结果:
解析:之所以会执行守护线程finally块中的代码,是因为在守护线程执行完毕之前,main线程还没有执行完,所以JVM不会退出。
2. 启动和终止线程
2.1 构造线程
在运行线程之前首先要构造一个线程对象,线程对象在构造时需要提供线程所需要的属性。如线程所属线程组、优先级、是否是Daemon线程等信息。下图为java.lang.Thread中对线程进行初始化的部分:
一个新构造的线程对象其parent线程进行空间分配,而child线程继承了parent是否是Daemon、优先级和加载资源的contextClassLoader及可继承的ThreadLocal,同时会分配一个唯一的ID来标识此child线程。至此,一个能够运行的线程对象初始化完毕,在堆内存中等待运行。
【备注】:在Java中,构造线程有三种方式:实现Runnable接口、实现Callable接口、继承Thread类。
2.2 启动线程
调用start()方法可启动线程。其含义为:当前线程(parent)同步告知JVM,只要线程规划器空闲,应立即启动调用start()方法的线程。
【备注】:直接调用run()方法和调用start()方法的区别:调用run方法,实质上只是以普通的方式调用方法,并不能使JVM去启动一个新线程。
2.3 中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。线程通过检查自身是否被中断来进行响应,线程可通过isInterrupted()来进行判断是否被中断,也可调用Thread.interrupted()方法将中断标识位进行复位。如果该线程已经处于终结状态,即使被中断过,调用isInterrupted()时仍会返回false。
在Java中,许多声明抛出InterruptedException的方法在抛出异常之前,JVM会先将该线程的中断标识位清除,然后抛出异常,此时调用isInterrupted()时同样会返回false。
实例一
代码:
package com.lipeng.fourth;
import java.util.concurrent.TimeUnit;
public class InterruptDemo {
public static void main(String[] args) {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
while(true){
}
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
thread1.start();
thread2.start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
thread2.interrupt();
System.out.println("thread 1 interrupt is "+thread1.isInterrupted());
System.out.println("thread 2 interrupt is "+thread2.isInterrupted());
}
}
运行结果:
解析:线程2在抛出InterruptedException前,其中断标识位被清除,所以isInterrupted()方法返回false。
2.4 安全的终止线程
除了上面提到的使用中断来终止线程外,还可以利用一个boolean变量来作为条件控制线程是否终止。
实例一
package com.lipeng.fourth;
import java.util.concurrent.TimeUnit;
public class CancelThreadDemo {
static volatile boolean flag=true;
public static void main(String[] args) {
WorkThread workThread=new WorkThread();
new Thread(workThread).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=false;
}
static class WorkThread implements Runnable{
@Override
public void run() {
while(flag){
//do something
System.out.println("do something"+System.currentTimeMillis());
}
}
}
}
3. 线程间通信
线程开始运行时,拥有自己的栈空间,按照既定的代码一步一步执行,直到终止。但,每个运行中的线程,如果仅仅是孤立的运行,那么没有一点价值,或者说价值很少,如果每个线程能够相互配合完成工作,这将会带来巨大的价值。
3.1 volatile和synchronized关键字
Java支持多个线程同时访问一个对象或变量,由于每个线程都会保留共享变量的副本,以提高处理器的运行速度,所以,在程序执行过程中,一个线程看到的变量并不一定是最新的。
volatile可以用来修饰变量,告诉处理器任何对该变量的访问均需要从内存中读取,而对它的改变会立即刷新到内存中,从而保证可见性。(其实现依靠lock指令)
synchronized可以修饰方法或同步块,他确保多个线程在同一时刻,只有一个线程处于方法或同步块中,从而保证可见性。(其实现依靠监视器)
如下图,描述了对象、对象的监视器、同步队列和执行线程之间的关系:
任意一个对象都拥有自己的监视器,当这个对象由同步块或同步方法调用时,执行方法的线程必须先获取到该对象的监视器才可以进入同步块或同步方法,而没有获取到监视器的线程会被阻塞在入口处,进入BOLCKED状态。
3.2 等待/通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,如果把前者作为通知方(生产者),则后者就为等待方(消费者)。这种模式在功能层面上实现了解耦,体系结构上具备了良好的伸缩性。
等待方(消费者)遵循如下原则:
- 获取对象的锁。
- 如果条件不满足,那么调用对象的wait方法。被通知后仍要检查条件。
- 条件满足则执行对应的逻辑。
synchronized(对象){
while(条件不满足){
对象.wait();
}
//对应的处理逻辑
}
通知方(生产者)遵循如下原则:
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
synchronized(对象){
改变条件
对象.notifyAll();
}
实例:
package com.lipeng.fourth;
import java.util.concurrent.TimeUnit;
/**
*
* @author LiPeng
*
*/
public class WaitAndNoifyDemo {
static TestObj testObj=new TestObj();
public static void main(String[] args) {
try {
new Thread(new WaitAction(testObj),"WaitAndNoifyDemo-WaitThread").start();
//sleep 3s
TimeUnit.SECONDS.sleep(3);
new Thread(new NotifyAction(testObj),"WaitAndNoifyDemo-NotifyThread").start();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class WaitAction implements Runnable{
TestObj testObj;
public WaitAction(TestObj testObj) {
super();
this.testObj=testObj;
}
@Override
public void run() {
synchronized (testObj) {
while(!testObj.flag){
try {
System.out.println(Thread.currentThread().getName()+" is waiting...");
testObj.wait(); //会释放掉testObj 的monitor
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//do something
System.out.println(Thread.currentThread().getName()+" start do something");
}
}
}
class NotifyAction implements Runnable{
TestObj testObj;
public NotifyAction(TestObj testObj) {
super();
this.testObj=testObj;
}
@Override
public void run() {
synchronized (testObj) {
System.out.println(Thread.currentThread().getName()+" set flag is true and notify thread");
//改变条件并唤醒等待线程
testObj.flag=true;
testObj.notify();
}
}
}
class TestObj{
volatile boolean flag=false;
}
上面的程序可以用下面的图来表示:
WaitThread首先获取了对象的锁,然后调用wait()方法,放弃了锁并进入对象的等待队列WaitQueue中,其状态为等待状态。其次,NotifyThread获取对象锁,改变了条件并调用notify()方法,唤醒在对象上等待的线程,WaitThread从WaitQueue移动到了SynchronizedQueue中,并转换为阻塞状态。当NotifyThread释放掉锁后,WaitThread再次获取锁并从wait()处返回继续执行后续代码。
3.3 管道输入/输出流
管道输入/输出流和普通文件输入/输出流或网络输入/输出流不同之处在于,他主要用于线程之间的数据传输,而传输的媒介为内存。其有4中具体实现:PipedOutputStream、PipedInputStream、PipedReader、PipedWriter,前两种面向字节,后两种面向字符。
实例:
package com.thread;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
/**
*
* @author LiPeng
* @date 2017年10月18日
*/
public class PipeStream {
public static void main(String[] args) {
try {
PipedInputStream inStream=new PipedInputStream();
PipedOutputStream outStream=new PipedOutputStream();
inStream.connect(outStream);
//绑定
Thread print=new Thread(new Print(inStream));
print.start();
int reciver=0;
while((reciver=System.in.read())!=-1){
//reciver ascall 码
System.out.println("input info is :"+reciver);
outStream.write(reciver);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Print implements Runnable{
private PipedInputStream in;
public Print(PipedInputStream in) {
super();
this.in = in;
}
@Override
public void run() {
int reciver=0;
Thread.currentThread().interrupt();
Thread.currentThread().isInterrupted();
byte[] b=new byte[1024];
try {
while((reciver=in.read())!=-1){
System.out.println("Print Thread revice info is :"+(char)reciver);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.4 Thread.join()
如果一个线程A执行了thread.join()语句,其含义为:当前线程A等待线程thread线程终止后才会从join处返回。Thread还提供join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。如果在给定时间内线程没有终止,则会从该超时方法中返回。
实例:
package com.lipeng.fourth;
import java.util.concurrent.TimeUnit;
public class ThreadJoinDemo {
public static void main(String[] args) {
Thread thread=new Thread(new ThreadAction());
thread.start();
System.out.println("thread start TimeStrap:"+System.currentTimeMillis());
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread done TimeStrap:"+System.currentTimeMillis());
}
}
class ThreadAction implements Runnable{
@Override
public void run() {
try {
//sleep 5s
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.5 ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值得存储结构。这个结构被附带在线程上,相互独立。
实例:
package com.lipeng.fourth;
public class ThreadLocalDemo {
static ThreadLocal<String> threadLocal=new ThreadLocal<String>();
public static String getLocalVal(){
return threadLocal.get();
}
public static void setLocalVal(String val){
threadLocal.set(val);
}
public static void main(String[] args) {
ThreadLocalDemo.setLocalVal("myThreadLocalValue");
System.out.println(ThreadLocalDemo.getLocalVal());
}
}
【备注】:本文图片均摘自《Java并发编程的艺术》·方腾飞,若本文有错或不恰当的描述,请各位不吝斧正。谢谢!