文章目录
多线程基础小结
1.概念
1.1什么是线程
每个线程都是一个执行流,每个执行流都单独处理自己的工作,多个执行流是并发执行的。
1.2进程和线程的区别
进程包含线程,一个进程中可以有多个线程,但至少有一个线程,即要有一个主线程。
每一个进程都相当于一个单独的房间,进程之间不共享内存空间。同一个进程内的线程共享同一个内存空间。(进程和线程就像房间和房客的关系,一个房间可以有多个房客,房间与房间直接互不影响,同一个房间内的房客共享房间的面积)
1.3多进程和多线程
(1)多进程
例如一个公司任务数量为100,它可以将任务分配给2个只有一套流水线的工厂去完成,每个工厂完成50的任务。
如下图这里的一个工厂就是一个进程,两个进程同时执行互不影响提高了工作效率,但是这种方式开支比较大,因为多建造了一个工厂。
(2)多线程
还是例如一个公司任务数量为100,但是公司将任务交给一个有2个流水线的工厂去完成,每条流水线完成50任务数量。
如下图,工厂就相当于一个进程,每个流水线都相当于一个线程,这样也提高了工作效率,但是开支就比较小。
2.使用java操作多线程
2.1创建线程
2.1.1继承Thread类
(1)创建一个线程类MyThread继承Thread
class MyThread extends Thread {
@Override
public void run() {
while (true){
System.out.println("hello thread");
}
}
}
(2)在main方法中实例化MyThread并且调用start()方法启动线程
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while (true){
System.out.println("hello main");
}
}
}
以上代码中MyThread线程类中run()方法只是表述了线程要做的工作,此时线程仅仅被创建了出来,直到main()方法中t调用start()方法线程才被创建出来,直到run()方法执行完毕线程才会销毁,上述代码为死循环所以线程会一直进行。
运行结果如下:可以看到控制台在交替打印线程类MyThread和main方法中的信息。
2.1.2实现Runnable接口
(1)创建一个类MyRunnable实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello thread");
}
}
(2)在main方法中实例化MyRunnable类,创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为参数,然后调用start()方法开始线程。
public class ThreadDemo2 {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
}
}
这里run()方法只打印了一次信息,打印结束后线程销毁,程序终止。
2.1.3匿名内部类创建 Thread 子类对象
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello");
}
};
t.start();
}
}
2.1.4匿名内部类创建 Runnable 子类对象
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
}
});
t.start();
}
}
2.1.5lambda 表达式创建 Runnable 子类对象
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.start();
}
}
以上方法中运行结果均相同,其中lambda表达式方法是最常用的线程创建方法。
2.2启动线程start()
在上述五种线程创建的方法中,我们已经看到如何覆写run()方法来创建一个线程,但是线程被创建出来并不意味着线程开始运行了,只有调用start()方法时线程才开始运行。
实际上覆写run()方法后只是将线程的任务给创建了出来,只有在调用start方法时操作系统底层才真正将线程创建出来。
如下图:run()方法就像一个加了密码的保险柜,而start()方法就是密码,只有知道了密码才能拿出里面的东西。
2.3线程的中断
(1)使用自定义的变量来作为标志.,来控制线程中断
public class ThreadDemo7 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (flag){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
flag = false;
}
}
通过修改flag的值改变while循环的判断条件达到中断线程的目的
(2)使用 thread 对象的 interrupted() 方法通知线程结束.
public class ThreadDemo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
interrupted()是Thread内置的一个标志位,可以更方便的实现线程的中断操作。
interrupt方法的作用
1设置标志位为true。
2如果线程处于阻塞状态(比如正在执行sleep)此时会把阻塞状态唤醒通过抛异常的方式让sleep立即结束。
2.4等待一个线程
线程之间是并发执行的,各个线程抢占式执行,所以操作系统对线程的调度是随机的,我们无法判断那个线程先开始哪个线程先结束。
有时候我们需要等待一个线程完成后才能继续接下来的工作,这时候我们就需要一个方法来明确等待一个线程结束。
public class ThreadDemo9 {
public static void main(String[] args) {
Thread t = new Thread(()->{
try {
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
System.out.println("join之前");
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join 之后");
}
}
由代码和运行结果可以明确看出,程序运行后main方法和Thread分头行动,在遇到join之后main方法被阻塞直到t线程完全结束main方法才恢复,这里main方法便是等待了t线程的结束。
3.线程的状态
3.1线程的所有状态
public class ThreadDemo10 {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了.
3.2观察线程的状态和转移
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这个循环体啥都不干,也不sleep
}
});
//启动之前获取T的状态也就是NEW状态
System.out.println("start之前的状态" + t.getState());
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("t执行中的状态" + t.getState());
}
t.join();
//线程执行完毕之后就是TERMINATED状态
System.out.println("t结束之后" + t.getState());
}
}
上述代码和运行结果可以清晰的看出线程执行过程中的状态转移。
4单线程与多线程速度对比
(1)单线程方法
public static void serial(){
//currenttimemillis获取当前ms级时间戳
long beg = System.currentTimeMillis();
long a =0;
for (long i = 0; i < 100_0000_0000L; i++) {
a++;
}
long b = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
b++;
}
long end = System.currentTimeMillis();
System.out.println("单线程执行时间:" + (end - beg) + "ms");
}
currentTimeMillis为获取当前ms级的时间戳,分别在线程开始和结束时获取一次,再相减得到线程的执行时间。
(2)多线程方法
public static void concurrency() {
//使用两个线程分别完成自增
Thread t1 = new Thread(()->{
long a = 0;
for (long i = 0; i <100_0000_0000L ; i++) {
a++;
}
});
Thread t2 = new Thread(()->{
long b = 0;
for (long i = 0; i <100_0000_0000L ; i++) {
b++;
}
});
long beg = System.currentTimeMillis();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("多线程执行时间:" + (end - beg) + "ms");
}
这里同样获取线程开始和结束时的时间戳,来得到执行时间,但是这里要注意必须执行 t1.start()和 t2.start()等待t1线程和t2线程都完全结束后再获取结束时间戳,否则程序会在线程未完全结束时的到end时间戳,影响最终结果。
(3)在main方法中先后调用两个方法进行对比
public static void main(String[] args) throws InterruptedException {
serial();
concurrency();
}
由上图可看出多线程的执行时间要远高于单线程,并且在执行任务越多时,结果越明显。