上节课内容回顾
进程, 进程调度, PCB结构,进程的虚拟空间地址,进程间的通信
进程和线程之间的区别和联系:
- 进程包含线程!一个进程里面可以有一个线程也可以有多个线程
- 进程和线程都能解决并发编程问题的场景,但是进程在频繁创建和销毁中,开销更高,线程开销更低(线程比进程更轻量级)
- 进程是系统分配资源(内存,文件资源…)基本单位,线程是系统调度执行的基本单位(CPU)
- 进程之间是相互独立的,各自有各自的虚拟空间地址.同一个进程的内部的多个线程之间共用一个内存空间以及文件资源.一个进程挂了其他进程一般都没事,但是一个线程挂了可能把整个进程搞崩
多线程仍然是最主流最常见的一种并发编程的方式
Java中如何进行多线程编程;
在Java标准库中,就提供了一个Thread类来表示操作线程
Thread可以视为Java标准库提供的API
创建好的Thread实例,其实和操作系统的线程是一一对应的关系,操作系统提供了一组关于线程的API(C语言风格)Java对于这组API进一步封装,就成了Thread类
Thread类的基本用法
第一种
通过Thread类创建线程,写法有多种,其中最简单的就是创建子类继承Thread,并且重写run方法
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello thread!");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
run
描述了每个线程内部要执行那些代码,每个线程都是并发执行的(各自执行各自的代码),因此就需要告诉这个线程你要执行什么代码
start
需要调用这个start方法,才是真正的在系统上创建了线程,才是真正的执行上面的run操作,在调佣出start方法之前,系统是没有创建出线程的
线程之间是并发执行的,如果一个循环中不加任何的限制,这个循环转的速度非常快,导致打印的东西太多了,根本看不过来,就可以加上一个sleep操作,来强制让这个线程休眠一段时间
在一个进程中至少有一个线程,在一个Java进程中也是至少有一个调用main方法的线程,自己创建的线程和自动创建的main线程就是并发执行的关系(宏观看起来是同时执行)
使用场景:
class MyThread extends Thread {
@Override
public void run() {
while(true) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
while(true) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
现在两个线程,都是先打印一条就休眠1s,当1s时间到了之后,系统先唤醒谁那?(看起来这个顺序不是完全确定的)随机的,对于操作系统来说,内部对于线程之间的调度顺序,在宏观上认为是随机的(抢占式的)
第二种
创建一个类,实现Runable接口,再创建Runable实例传给Thread实例
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("hello thread!");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
第三种
就是上面俩种写法的翻版,使用了匿名内部类
public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("hello thread!");
}
};
thread.start();
}
}
创建了一个匿名内部类,继承自Thread类,同时重写run方法,同时new这个匿名内部类的实例
第四种
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello thread!");
}
});
thread.start();
}
}
new Runable针对这个创建的匿名内部类,同时new出的Runable实例传给Thread 的构造方法
通常认为Runable这种写法更好一点,能够能够做到让线程和线程执行的任务更好的解耦,写代码一般希望高内聚,低耦合
第五种
相当于第四种写法的延伸,使用lambda表达式,是使用了lambda代替了Runable而已
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("hello thread!");
});
thread.start();
}
}
以上五种写法都很常见大家都应该掌握
多线程能够提高任务完成的效率,
具体使用:
有两个整数变量,分别对着两个整数变量自增10亿次,分别使用一个线程和两个线程来观察时间的变化
package Thread;
import java.util.Scanner;
public class Demo7 {
private static final long count = 10_0000_0000;
public static void serial() {
long beg = System.currentTimeMillis();
long a = 0;
long b = 0;
for(int i = 0; i < count; i ++) {
a ++;
}
for(int i = 0; i < count; i ++) {
b ++;
}
long end = System.currentTimeMillis();
System.out.println((end - beg) + "ms");
}
public static void concurrency() throws InterruptedException{
long beg = System.currentTimeMillis();
Thread thread1 = new Thread(() -> {
long a = 0;
for(int i = 0; i < count; i ++) {
a ++;
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
long b = 0;
for(int i = 0; i < count; i ++) {
b ++;
}
});
thread2.start();
thread1.join();
thread2.join();
long end = System.currentTimeMillis();
System.out.println((end - beg) + "ms");
}
public static void main(String[] args) throws InterruptedException {
serial();
concurrency();
}
}
Thread类的其它属性和方法
Thread(String name)
,这个东西是给线程起名字,起一个什么样子的名字不影响线程本身的执行,仅仅影响到程序员的调试,可以借助一些工具可以看到每个线程的名字,很容易在调试的时候对线程做出区分,可以用jconsole
来观察线程的名字
中断线程
线程停下来的关键是让对应的run方法执行完(还有一个特殊的是main这个线程,对于main来说,得是main方法执行完,线程就完了)
- 可以手动的设置一个标志位(自己创建的变量,boolean),来控制线程是否要执行结束
代码实现:
public class Main {
private static boolean isQuit = false;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while(! isQuit) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuit = true;
}
}
这种实现的方法并不严谨,假如有很多线程都要用这个标志位怎么办?
- 更好的方法是用一个Thread中内置的一个标志位来判定,可以通过两种方法来实现
Thread.interrupted()
,这是一个静态的方法
Thread.currentThread().isInterrupt()
,这是一个实例的方法,其中的currentThread
就是获取当前线程的实例
使用样例:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while(! Thread.currentThread().isInterrupted()) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
这个代码绝大部分都是在休眠状态,此处的中断是希望立即产生效果的,如果线程已经是阻塞状态下,此时设置标志位就不能起到及时唤醒的效果
调佣
t.interrupt()
,这个方法可能产生两种情况
- 如果线程是出于就绪状态,就是设置线程的标志位为true
- 如果线程出于阻塞状态,就会触发一个异常,应该在异常的时候直接退出即可
线程等待
多个线程之间调度顺序是不确定的,线程之间的执行是按照调度器来来安排的,这个过程可视为是“无序,随机”,这样不太好,有些时候我们需要控制线程之间的顺序,线程等待就是一种控制线程执行顺序的手段,此处的线程等待,主要是控制线程结束的先后顺序
join
,调佣join的时候,那个线程调用的join,那个线程就会阻塞等待,知道对应的线程执行完毕为止(对应的run方法执行完)
主要有:
1. 线程的创建
- 线程的中断
- 线程的等待
- 线程获取引用
- 线程的休眠