如何创建线程
线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程来进行并发程序的设计,是因为线程间的切换和调度的成本要远远小于进程。
一般创建线程有两种方式“
- 继承Thread类
- 实现Runnable接口,同时将实现Runnable接口的对象作为构造方法参数传入Thread,创建Thread对象。
- JDK1.8以上,通过传入lambda的方式创建Thread对象,启动一个线程。实质也是创建了一个Runnable的实现类
public class CreateThread {
public static void main(String[] args){
new TaskByThread().start();
new Thread(new TaskByRunnable()).start();
new Thread(()-> System.out.println("I am create by <lambda>!")).start();
}
private static class TaskByThread extends Thread{
@Override
public void run() {
System.out.println("I am create by extends <Thread>!");
}
}
private static class TaskByRunnable implements Runnable{
@Override
public void run() {
System.out.println("I am create by implements <Runnable>!");
}
}
}
注意:启动线程调用的是Thread的start()方法,而不是run方法,如果调用run方法,相当于在当前线程执行该”线程的逻辑“,而不是新创建一个线程。
Thread的默认run方法实现如下:其中target是Runnable类型,这也是Thread构造参数中传入一个Runnable对象可以创建一个线程的原因。
public void run() {
if (target != null) {
target.run();
}
}
线程生命周期
-
线程从生到死会经历如下生命周期:
- NEW------------new一个新的线程t
- Runnable/Ready-----t.start()
- Running----------------线程t获取到CPU时间片
- WAITING ------------- 线程等待:obj.wait()
- WAITING ------------- 线程等待,超时结束等待。obj.wait(timeout)
- BLOCKED ----------- 线程阻塞,等待唤醒。
- TERMINATED ------ 线程结束,即死亡。
-
状态迁移如下图所示:
-
轻量级阻塞与重量级阻塞
- 能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING。
- 不能被中断的阻塞称为重量级阻塞,如synchronized,对应的线程状态为BLOCKED。
-
t.interrupt(),t.isInterrupted(),Thread.interrupted()方法的区别:
- t.interrupt():精确含义是“唤醒轻量级阻塞”线程,而不是中断一个线程。相当于给线程发送一个唤醒的信号,所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并被唤醒。
- t.isInterrupted():判断自己是否收到过中断信号,非静态函数,仅仅读取中断状态,而不修改状态。
- Thread.interrupted():判断自己是否收到过中断信号,静态函数,不仅仅读取中断状态,还会重置中断标志位。
如何优雅的关闭线程
运行到一半的线程能否强制杀死?
肯定是不能的。因为如果强制杀死线程,则线程中所使用的资源,例如文件描述符、网络连接等资源就无法正常关闭。
- 一个线程一旦运行起来,就不要去强行打断它,合理的关闭方法是让其运行完(也就是线程执行结束),释放所有的资源后,退出线程。所以Java中不建议使用stop()和destory()方法。
- 设置守护线程
t.setDaemon(true)
,使得所有非守护线程执行结束后,守护线程自动优雅关闭。如经典的JVM中的垃圾回收线程。 - 设置关闭的标志位。如下所示
/**
* 执行结果如下:
* I am running
* I am running
* I am running
* I am running
* I am running
* shutdown the task.....
*/
public class ShutdownThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
task.start();
TimeUnit.SECONDS.sleep(5);
task.stopTask();
System.out.println("shutdown the task.....");
}
private static class MyTask extends Thread{
private volatile boolean flag = true;
@Override
public void run() {
while (flag){
try {
System.out.println("I am running");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stopTask(){
this.flag = false;
}
}
}
问题:如果在while循环中调用了obj.wait()方法阻塞了线程,则线程无法执行到while的循环条件判断,就无法结束线程。
- 通过线程的interrupt()方法中断线程的阻塞状态。注意,这里的interrupt方法只能中断处于阻塞状态的线程,而无法中断如while循环这类运行状态的线程。
/*
* 执行结果如下:
* I am running
* shutdown the task.....
* Thread-0 is interrupted...
* */
public class ShutdownThreadByInterruptDemo {
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
task.start();
TimeUnit.SECONDS.sleep(5);
task.interrupt();
System.out.println("shutdown the task.....");
}
private static class MyTask extends Thread{
@Override
public void run() {
while (true){
try {
System.out.println("I am running");
Thread.sleep(1000000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " is interrupted...");
break;
}
}
}
}
}
task.interrupt()只能中断那些声明了会抛出InterruptedException异常的函数,如sleep(), wait(), join()方法。
线程间通信
- 线程的内存模型
Java线程之间的通信采用的是共享内存模型(JMM),它决定一个线程对共享变量的写入何时对另一个线程可见。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(即线程共享内存)中,线程被CPU执行,每个线程都有一个私有的本地内存(如CPU的高速缓存),本地内存中存储了该线程以读/写共享变量的副本。
重点注意:(重点)本地内存是JMM的一个抽象概念,并不真实存在;它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
Java 内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(比如CPU的高速缓存)。
线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。
- 为什么Java的内存模型规范要这样定义导致出现线程本地内存和主存的值不同步呢?为啥线程要有自己的本地内存?
- CPU缓存的速度远远大于内存的读写速度。
- 保障多个CPU核心的缓存的一致性是极具消耗性能的事情,所以默认CPU架构不会保障CPU缓存(如一级缓存)的一致性。
- 利用缓存和改变执行代码顺序达到程序执行效率优化。
- 线程之间的通信。
wait():线程是否获得的资源,并阻塞(WAITING状态)等待被唤醒
notify(): 线程唤醒处理WAITING状态的其中一个线程。
notifyAll():线程唤醒处于WAITING状态的所有线程。
join(): 等待该线程执行结束,会阻塞当前线程的执行。
例如:两个线程计算1到100的和,其中A线程计算1到50的和,B线程计算51到100的和,最后相加得到1到100的和
public class SumByMultiThread {
/*
* 运行结果:1+100=5050
* */
public static void main(String[] args) throws InterruptedException {
// 创建两个任务
SumTask headTask = new SumTask(1, 50);
SumTask tailTask = new SumTask(51, 100);
// 创建两个线程
Thread headThread = new Thread(headTask);
Thread tailThread = new Thread(tailTask);
//启动线程
headThread.start();
tailThread.start();
// 同步阻塞两个线程,知道线程完成
headThread.join();
tailThread.join();
int sum = headTask.getSum() + tailTask.getSum();
System.out.println("1+100=" + sum);
}
/*计算[start,end]的和*/
private static class SumTask implements Runnable{
private final int start;
private final int end;
private int sum;
private SumTask(int start, int end) {
this.start = start;
this.end = end;
this.sum = 0;
}
@Override
public void run() {
for (int i=start; i<=end; i++){
sum += i;
}
}
public int getSum(){
return this.sum;
}
}
}
synchronized关键字
- synchronized关键字的含义其实就是给某个对象加锁。
- 作用于静态方法:给当前类对象,即Class(class本身也是对象)加锁。
- 作用于非静态方法:给当前对象加锁。
- 作用于静态代码块:即给指定的对象的加锁,只有加锁的对象,才能够执行wait(),notify(),notifyAll()等方法。
- 锁的本质。
- 线程----------是一段段运行的代码
- 资源 --------- 就是一个变量、一个对象或一个文件等。
- 锁 ------------ 就是要实现线程对资源的访问控制,保证同一时间只能有一个线程去访问资源。
锁的本质就是一个“对象”
- 这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。
- 如果这个对象被某个对象占用了,它得记录这个线程的thread id。知道自己被哪个线程占用了。
- 这个对象还得维护一个thread id list,记录其他所有阻塞的、等待拿这个锁的线程。当前线程释放锁之后,从这个thread id list里面取一个线程唤醒。
-
synchronized实现原理。
上节说道,锁的本质就是对象,synchronized也不例外,也是一个对象。对象由三部分组成:对象头、实例数据和对齐填充。而对象头就是我们讨论的关键所在。
3.1 对象头主要包括两部分信息- 自身运行时的数据,比如:锁状态标志、线程持有的锁…等等。(此部分内容被称之为Mark Word)
- 类型指针:JVM通过这个指针来确定这个对象是哪个类的实例。
3.2 synchronized同步代码块原理:
举例说明,看如下代码: - 自身运行时的数据,比如:锁状态标志、线程持有的锁…等等。(此部分内容被称之为Mark Word)
public class SynchronizedDemo {
private int i = 0;
/**同步代码块*/
public void syncBlock(){
synchronized (this){
i ++;
}
}
/**同步方法*/
public synchronized void syncMethod(){
i ++;
}
}
反编译以上代码:javap -v synchronizedDemo
得到字节码如下:
/**同步代码块*/
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
......
/**同步方法*/
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
结合以上字节码可知,同步代码块和同步方法是有显著区别的。
- 同步代码块:根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器(_count)加1。当然与之对应执行monitorexit指令时,锁的计数器(_count)也会减1。
如果当前线程获取锁失败,那么就会被阻塞住,进入_WaitSet 中,等待锁被释放为止。- 同步方法:由以上字节码可知,同步方法中并没有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED标识,JVM通过ACC_SYNCHRONIZED标识,就可以知道这是一个需要同步的方法,进而执行上述同步的过程,也就是_count加1,这些过程。
同步代码块中出现两个monitorexit指令的原因:编译器需要确保方法中调用过的每条monitorenter指令都要执行对应的monitorexit 指令。为了保证在方法异常时,monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行 异常的monitorexit指令。而字节码中多出的monitorexit指令,就是异常结束时,被执行用来释放monitor的。