作者:~小明学编程
文章专栏:JavaEE
格言:热爱编程的,终将被编程所厚爱。
目录
多线程
什么是线程
为什么需要多线程
进程和线程的区别(面试重点)
Java实现多线程
多线程带来的好处
认识Thread类
Thread 的常见构造方法
Thread 的属性
start和run的区别
中断线程
线程等待
获取当前的线程
休眠当前的线程
线程的状态
线程的状态分类
多线程
什么是线程
所谓的线程就相当于一个执行流,同时线程也是最小的执行单位,我们每一个线程都按照自己的代码去执行,多个线程的话就按照各自的代码去执行。
为什么需要多线程
前面我们谈到了多进程可以并发的解决问题,我们的多线程同样也能解决并发问题,相比于多进程我们多线程有它的优势:
1.创建线程比创建进程更快.
2.销毁线程比销毁进程更快.
3.调度线程比调度进程更快.
进程和线程的区别(面试重点)
1.进程是我们资源分配的最小单位,而线程是我们系统调度执行的最小单位也就是cpu调度的最小单位。
2.我们的进程可以有一个线程也可以有多个线程。
3.多进程和多线程都是为了解决我们的高并发的问题,但是我们的进程开辟需要分配相应的资源开辟的成本较大,而线程开辟的成本小比较的轻量。
4.我们进程之间是相互独立的,各自有各自的虚拟地址空间相互之间互不影响,而我们的多线程是同处在一个进程中的大家公用相同的资源,互相之间是有影响的,一个线程出现问题了很有可能会波及到整个进程。
如果把进程比作是一个工厂的话,多个进程就是多个工厂,那么我们的线程就可以比作我们的流水线,一个工程一般都会开辟多个流水线这样效率会比较高,同时我们建立流水线的成本肯定是要小于建立工厂的,但是我们一个工厂中的一个流水线发生问题,例如出现了火灾很可能会影响其它的流水线,乃至整个工厂都会报废。
Java实现多线程
第一种
//创建一个子类继承Thread父类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("haha");
}
}
public class Demo1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
我们的Thread是我们Java种的多线程的相关类,其中里面的run方法是我们该线程要完成的任务,我们可以自己定义一个类,然后去继承它,最后通过向上转型来实现,start()是我们开启线程的开关。
第二种
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("haha");
}
}
public class Demo3 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
我们可以实现一个Runnable的接口,重写里面的run方法,然后传给Thread,这时候会通过传参有一个向上转型,也同样完成了目标。
不过对比一个我们上面两种的写法,继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
而实现 Runnable 接口, this 表示的是 MyRunnable 的引用.如果想要指代当前线程的话需要使用 Thread.currentThread()
第三种
public static void main1(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("haha");
}
};
thread.start();
}
这是我们的第一种的变形,我们直接使用内部类,然后重写run方法。
第四种
public static void main2(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hehe");
}
});
thread.start();
}
这是我们的第二种写法的变形,我们这里重写了接口的里面的唯一抽象方法。
第五种
public static void main3(String[] args) {
Thread thread = new Thread(()->{
System.out.println("haha");
});
thread.start();
}
这是第四种的换了一个写法,我们的接口只有一个抽象方法的时候我们可以用lambda表达式来简化语句。
多线程带来的好处
我们接下来将用一个程序直观的给大家看一下用多线程和不用多线程的区别:
public class Demo5 {
private static final long count = 10_0000_0000L;
public static void test1() {
long a = 0;
long b = 0;
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
a++;
}
for (int i = 0; i < count; i++) {
b++;
}
long end = System.currentTimeMillis();
System.out.println("不用多线程:"+(end-start));
}
public static void test2() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread1 = new Thread() {
@Override
public void run() {
long a = 0;
for (int i = 0; i < count; i++) {
a++;
}
}
};
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
long b = 0;
for (int i = 0; i < count; i++) {
b++;
}
}
});
thread2.start();
th
该程序就是两个很大的循环一个一个单线程一个双线程。
可以看到不使用多线程的时间是使用了的差不多两倍。
认识Thread类
Thread 的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这 个目前我们了解即可 |
前面的三个构造方法我们都比较的熟悉了,我们重点讲一下最后一个,
Thread(Runnable target, String name)是给我们的线程命名了,方便我们后面的监视,例如下面这个程序
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
while (true) {
System.out.println("thread1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread1");
thread1.start();
Thread thread2 = new Thread(()->{
while (true) {
System.out.println("thread2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread2");
thread2.start();
}
我们开启了两个线程并且都给命名了,下面我们就利用jconsole来对其监视。
这里可以看到我们命名的线程,并且我们还可以进行跟踪。
Thread 的属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
这是我们Thread的几个属性,下面我们给大家做个简单的介绍。
首先就是ID这就相当于我们的身份证,每个线程都有属于自己的ID,然后就是名称线程的名字,状态就是告诉我们当前的线程处于什么样子的状态,是休眠还是使用,优先级的话类似于我们的进程,到底先执行哪一个看优先级,是否是后台线程我们简单说明一下:
是否后台线程 | isDaemon() |
如果是后台线程的话就不影响进程的退出,如果不是就影响进程的退出。
就像我们上面那一个代码,thread1,thread2默认是前台的线程也就是当我们的main线程执行完毕,整个进程不能退出得等到我们thread1,thread2都执行完毕才能退出,如果是后台线程的话那么main()线程执行完毕,整个进程就退出了。
是否存活 | isAlive() |
Thread thread 对象的生命周期和系统内核种线程的存在周期是不一样的,当我们start()之后就会开启相应的线程,但是当我们run完了之后虽然我们的线程会销毁但是我们的thread对象还在,isAlive()就是用来区分我们start()之后我们的run到底跑完了没有,跑完了就返回false,没跑完就返回true。
start和run的区别
这里我们详细的说一下我们的start()方法,我们前面开启线程的时候都会start(),我们用run ()的话也能打印相应的内容。
class MyThread extends Thread{
@Override
public void run() {
System.out.println("haha");
}
}
public class Demo1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.run();//haha
}
}
但是这两个方法却是有很大的区别,我们的run只是执行我们run()里面的内容并没有开启线程,而start()则是开启一个新的线程并且去执行run()。
中断线程
设置标志位中断:
public class Demo7 {
static boolean flag = true;
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
while (flag) {
System.out.println("run");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
//主线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("时间到");
flag = false;
}
}
2.使用自带的方法
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
//Thread.interrupted();//静态方法
while (!Thread.currentThread().isInterrupted()) {
System.out.println("run");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
//主线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("时间到");
thread.interrupt();//中断线程
}
我们看到抛出了异常,这是因为当我们想要中断的时候此时属于阻塞状态,然后就会抛出异常解决办法就是在catch里面加上一个break。
关于Thread.interrupted()和Thread.currentThread().isInterrupted()的区别
Thread.interrupted() :判断当前线程的中断标志被设置,清除中断标志(获取的是静态成员,一个程序只有一个标志位)。
Thread.currentThread().isInterrupted() :判断指定线程的中断标志被设置,不清除中断标志(获取的是普通的成员)。
一般情况下我们用的都是第二种。
线程等待
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作,这个时候就需要我们的线程等待了,也就是我们的join方法。
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
其使用方法是我们所在的线程等待我们指定的线程。
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
while (true) {
System.out.println("thread1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread1");
thread1.start();
thread1.join();
System.out.println("over!");
}
例如上述程序我们的over所在的main()线程永远在等待thread1的线程,如果没有join的话就直接输出了。
获取当前的线程
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
System.out.println("thread:"+Thread.currentThread().getName());
},"thread");
thread1.start();
System.out.println("main:"+Thread.currentThread().getName());
}
我们也可以有另外一种写法:
我们上面的this指代的是Thread这个类,而下面这种写法只是指代当前的接口。
所以只能写成这种形式
System.out.println(Thread.currentThread().getName());
休眠当前的线程
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
前面我们说到PCB,我们的每一个进程都会有若干个线程每一个线程都有PCB,PCB中有一个pgroundId也就是我们当前线程所属的进程,还有一个pid也就是当前的PCB的id,在linux中其底层是一个双向链表。
当我们休眠的时候就会被放到阻塞队列中去。
线程的状态
线程的状态分类
我们的线程有不同的一些状态,那么不同的状态分别代表什么含义呢?
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 当前线程在等待锁,正在阻塞
WAITING: 当前线程在等待唤醒,正在阻塞
TIMED_WAITING: 因为休眠在阻塞
TERMINATED: 工作完成了,线程已经销毁但是对象还在
下面我们用代码来演示一下我们的几种状态。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println(thread.getState());//NEW 安排了工作还没开始
thread.start();
System.out.println(thread.getState());//RUNNABLE 随叫随到,随时可以上cpu
Thread.sleep(500);
System.out.println(thread.getState());//TIMED_WAITING 处于阻塞状态,到了一定时间后阻塞状态解除
Thread.sleep(2000);
System.out.println(thread.getState());//TERMINATED 工作完毕,线程已经销毁但是对象还在
}
我们的getState()方法可以返回我们当前的线程所处的状态。
可以看到在我们还没有start()的时候,这个时候返回一个NEW,当我们start()以后就返回RUNNABLE了,表示当前是就绪状态随时可以上cpu执行,我们然main线程休眠了500ms之后此时我们的thread也是处于休眠状态,这个时候就返回TIMED_WAITING表示当前是阻塞的状态,之后我们又然main线程休眠了2000ms,此时我们的thread线程已经销毁就返回TERMINATED。
我们之所以分这么多的状态是为了在后面遇到线程问题的时候我们可以打印当前的状态,然后找到具体的原因,说到底就是为了方便排查错误。