创建线程的方式:以下创建线程的方式,本质上都是相同的,都要借助Thread类,在内核中创建出新的PCB,加入到内核中的双向链表中,只不过是描述任务的主体不一样;

1)Thread类的基本用法:我们可以通过Thread类创建线程,最简单的方法就是说创建一个类继承于Thread类,并且重写里面的run方法,
我们本质上是在创建继承于Thread类的这样的一个实例

1.1)注意:Thread类是在Java.lang包底下的,而我们的TimeUnit是在juc包底下的,咱们的jconsole是JAVA中自带的一个调试工具

1.2)jconsole可以列举出你系统上面的JAVA进程,但是其他进程不行,但是他是JDK的调试工具

前两个进程表示的是JConsole进程,下来一个是Main进程,下来是Idea进程

咱们的java进程一但进行启动,那么不仅仅是你自己代码中的线程,还有一些其他的线程

咱们的Java进程一旦进行启动,不仅仅有一些我们自己代码中的线程,还有一些其他的线程(有的进行网络连接,有的进行垃圾回收,有的进行日志打印)
Thread t1=new Thread(){ @Override public void run() { System.out.println(1); }
}; t1.start(); package com; class MyThread extends Thread{ public void run(){
System.out.println("执行run方法"); } } public class Solution{ public static void
main(String[] args) { MyThread thread=new MyThread(); thread.start(); } }

1)run方法里面描述了这个线程内部要执行那些代码,每一个线程都是并发执行的,每一个线程都有每一个线程的代码,是一个完全并发的关系,你就需要告诉线程你要执行的代码是什么,
我们的run方法的逻辑就是在新创建的线程中,要执行的代码

2)在这里我们说并不是我们定义这个Thread类的子类,一重写run方法,线程就被创建出来了,相当于是老板把活安排好了,工人们还没有开始干呢

3)我们需要进行创建Thread子类的实例并且调用start方法,我们才真正的进行开始执行上面的run方法,
所以说在我们进行调用start方法之前,我们系统中是没有进行创建出线程的

4)因为两个进程之间是不会相互干扰的,所以我们通过Thread类创建的线程,都是在同一个JAVA进程里面的

1)如果说我们在循环中不加任何限制,那么循环就会转得非常快,就导致我们打印的东西太多了,根本看不过来
import java.sql.Time; import java.util.concurrent.TimeUnit; class MyThread
extends Thread{ public void run(){ try { TimeUnit.SECONDS.sleep(1); } catch
(InterruptedException e) { e.printStackTrace(); }
System.out.println("我是线程1里面的方法"); } } class TestThread extends Thread{ public
void run(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {
e.printStackTrace(); } System.out.println("我是线程2里面的方法"); } } public class
Solution{ public static void main(String[] args) { MyThread t1=new MyThread();
t1.start(); TestThread t2=new TestThread(); t2.start(); } }
2)所以我们可以加上一个sleep操作,让这个线程强行进行休眠一段时间,
这个休眠操作就是让线程强制进入到阻塞状态,单位是ms,意思就是说在指定的毫秒之内,这个线程不会到CPU上面执行

3)InterruptedException就是说线程被打断的异常

4)在我们的一个进程里面,至少会有一个线程,在我们的一个JAVA进程里面,也是说会至少会有一个调用main方法的线程,
这个线程不是你自己写的,而是你的系统自动搞出来的,我们自己创建的线程和main线程就是在并发执行的,宏观上面看起来同时执行,这里面的并发就是指并发+并行,
我们在宏观上面是无法进行区分并发和并行,这完全取决于操作系统的调度执行

在我们的上述代码中,现在有两个线程,都是打印个一条,那么就直接休眠1s,但是当1s时间到了以后,会先执行谁呢?结论就是不知道,这个顺序并不能完全进行确定,
所以说操作系统内部对于线程之间的调度顺序,在宏观上面可以认为是随机的,我们把这个线程以内的随机调度,称之为抢占式执行,线程之间在抢,就存在了太多的不确定因素

2)通过显示创建一个类,实现Runnable接口,然后再把这个继承runnable的实例化对象关联到Thread的实例上;也可以通过匿名内部类的方式来进行创建;
static class myrunnable implements Runnable { public void run() {
System.out.println("我是线程"); } } public static void main(String[] args) throws
InterruptedException{ Thread t1=new Thread(new myrunnable()); t1.start(); }

3)通过runnable匿名内部类的方式创建一个线程,重写run方法,在直接把这个Runnable类创建的对象关联到Thread里面,这种写法和单独创建一个类,再继承Thread没有任何区别;

我们直接通过Runnable来进行描述一个线程执行的具体任务,进一步的在把描述好的任务交给Thread实例
1)Runnable myrunnable=new Runnable(){ public void run() {
System.out.println("我是一个线程"); } }; Thread t1=new Thread(myrunnable);
t1.start(); }}
我们在这里面主要是说我们new出来的Runnable接口,我们创建继承于runnable接口的类的实例,同时我们将new出来的Runnable实例传给Thread类的构造方法
2)Thread thread =new Thread(new Runnable() { @Override public void run() {
System.out.println("我是一个线程"); } }); thread.start();

4)通过lamda的表达式的方式创建一个线程,类似于通过匿名内部类的方式来进行创建,只是通过lamda表达式来代替Runnable接口
public class Hello { public static void main(String[] args) throws
InterruptedException{ Thread t1=new
Thread(()->{System.out.println("我是一个线程");}); t1.start(); 函数式接口 }} new
Comparator<Integer>(){ @Override public int compare(Integer o1, Integer o2) {
return o1-o2; } };
1)咱们的匿名内部类,其中的Comparable接口,Comparator接口,都是可以写成匿名内部类的方式

2)咱们上面的这个匿名内部类的写法就是说
我们进行创建了一个匿名内部类,实现了Comparator接口或者继承于Thread类,同时我们进行重写了run方法,同时还new出了这个匿名内部类的实例

3)我们还是说认为Runnable这一种写法要更好一些,因为我们可以让线程和线程执行的任务可以更好地进行解耦,所以说在我们写代码的时候,要高内聚,低耦合,
同类的功能的代码放在一起,不同的功能模块之间尽量不要有太多的关联关系,Runable只是单纯的描述了一个任务,至于这段代码是有一个线程来进行执行,进程,线程池,协程来进行执行,Runnable本身并不关心,Runnable本身的代码也不会进行关心,以后的代码改动更小,所以说这种写法要更好一些

Thread类中的run与start方法中的区别

run只是一个普通的方法,描述了任务的内容,start是一个特殊的方法,会在系统中创建线程

1)当我们点击程序运行的时候,首先系统会创建出一个进程,这个线程执行的代码,就是main线程,系统中原来就有很多PCB,执行代码,就会创建出一个PCB,通过链表与其他的PCB进行连接,此时这个PCB代表的就是main方法;

2)但是此时执行了t.start(),就会再次创建出一个PCB,执行t.start()的时候,就会把这个PCB挂到链表上面(这两个PID是相同的,此时在这个PCB里面就会自动调用咱们的run()方法;

3)t.run(),不会创建出现的线程,但是此时也会在系统中创建出一个PCB,这个PCB是代表main线程,在这个PCB中会调用这个main方法,这个过程并不会新创建线程

1)调用start方法可以直接启动线程,并使线程进入就绪,当run方法执行完了,线程,也就结束了。但是如果直接执行run方法,会当作普通方法来调用,还是在main方法进行的,不会创建一个新线程;

2)当我们执行run方法的时候,其实本质上是调用main方法来进行执行方法体的,但是咱们的start方法是真的进行开启一个新线程来进行执行任务

3)run方法也叫作线程体,它里面包含了具体要执行的业务代码,当我们进行调用run方法的时候,会立即执行run方法的代码,但是当我们调用start方法的时候,
本质上是启动了一个线程并将这个线程的状态设置为就绪状态,也就是说调用start()方法,程序不会立即执行

4)run方法是普通方法,普通方法是可以被调用多次,但是start方法是创建新线程执行任务,而start方法只能调用一次,否则就会出现IllegalThreadStateException非法线程状态

为什么Start方法不可以重复的进行调用呢? 

1)原因是当你的start代码实现的第一行,会先进行判断当前的状态是不是0,也就是说是否是新建状态,如果不是新建状态NEW,那么就会抛出IllegalThreadStateException非法线程状态异常

2)当线程调用了第一个start方法之后,线程的状态就会由新建状态NEW变成RUNNABLE状态,此时再次调用start方法,JVM就会判断当前线程已经不等于新建状态了,从而会抛出IllegalThreadStateException异常,所以线程状态是不可逆的;

下面我们来看一下单线程和多线程的执行差别,多线程可以提高任务完成的效率

1.我们先看单线程的执行效果,就是一个串行执行的效果

public static void main(String[] args){ long beg1=System.currentTimeMillis();
int a=0; for(long i=0;i<1000000000;i++) { a++; } int b=0; for(int
j=0;j<1000000000;j++) { b++; } long beg2=System.currentTimeMillis();
System.out.println("执行时间为"); System.out.println(beg2-beg1); } }
 2)再看多线程的执行效果
class Hello{ public static void main(String[] args) throws
InterruptedException{ long beg1=System.currentTimeMillis(); Thread t1=new
Thread(){ public void run(){ long a=0; for(long i=0;i<1000000000;i++) { a++; }
} }; Thread t2=new Thread(){ public void run(){ long b=0; for(long
j=0;j<1000000000;j++) { b++; } } }; t1.start(); t2.start(); t1.join();
t2.join(); long beg2=System.currentTimeMillis(); System.out.println("执行时间为");
System.out.println(beg2-beg1);
1)此时我们记录时间是在main线程里面来进行记录的,也是在主线程里面执行的,main线程和t1线程和t2线程是一种并发执行的关系,我们此处就认为t1和t2还没有执行完成呢,main线程就进行记录时间,这显然是不准确的
2)此时我们的正确做法应该是让我们的main线程等待t1线程和t2线程全部执行完成了,再来进行执行我们的main线程 } }
 上面的join效果就是t1线程和t2线程执行完成之后,再来执行我们的main线程

由此我们可知:多线程会比单线程效率更高

1)咱们的join的效果就是等待对应线程结束:t1.join就是让main线程等待t1线程结束,t2.join()就是说让main线程等待t2线程结束,
我们上述的这两个线程在我们的底层到底是在并行执行还是在并发执行,这是不确定的,
只有真正的两个线程在并行执行的时候,效率才会有显著的提升,但是肯定要比单线程执行的更快

2)如果说你进行计算的count值太小,那么此时你创建线程本身也是有开销的呀,你的主要的时间就花在创建线程上面了,光你创建两个线程就用了50ms,但是你计算值的过程就使用了10ms,此时肯定是得不偿失的,
只有你的任务量太大的时候,多线程才有优势,只有说我们进行创建的任务量的总时间大于线程创建的时间,我们才说多线程可以提高效率
t1.start(); t1.join(); t2.start(); t2.join(); 在这种情况下:t1,t2是串行执行的

1)主线程还是一直向下走,但是新线程会执行run方法,对于新线程来说,run方法执行完了,新线程就结束了,对于主线程来说,main方法执行完,主线程就结束了

2)线程之间,是并发执行的关系,谁先执行,谁后执行,谁执行到哪里让出CPU,都是不确定的,作为程序员是无法感知的,全权有操作系统的内核负责。例如当创建一个新线程的时候,接下来是主线程先执行,还是新线程,是不好保证的。

3)执行join方法的时候,该线程会一直阻塞,一直阻塞到对应线程结束后,才会继续执行,本质上来说是为了控制线程执行的先后顺序,而对于sleep来说,谁调用谁就会阻塞;

4)主线程把任务分成几份,每个线程计算自己的一份任务,当所有的任务被计算完毕后,主线程再来汇总(就必须保证主线程是最后执行完的线程)。

5)获得当前对象的引用 Thread.currentThread()

6)如果线程正在运行,执行计算其逻辑,此时就在就绪队列排序呢,调度器就会在就绪队列找出合适的PCB让他在CPU执行,如果某个线程调用Sleep就会让对应的PCB进入阻塞队列,无法上CPU;

7 对于sleep让其进入阻塞队列的时间是有限制的,时间到了之后,就会被系统把PCB那回到原来的就绪队列中了;

8)join被恢复的条件是对应的线程结束。

我们所说的共享资源主要指的是两个方面:

1)内存,线程一和线程二,都可以共享同一块内存(同一个变量)
2)文件,线程一打开的文件,线程二也可以去使用
本质上来说,变量就是内存,两个线程可以访问同一个变量,说明这两个线程在使用同一个内存,但是对于多进程来说,进程一就不可以访问进程二的变量

1)Thread类中的常见用法,Thread类是用于管理线程的一个类,换句话来说,每一个线程都有唯一的Thread类进行关联

2)Thread的常见构造方法:
Thread() 创建线程对象 Thread(Runnable target) 借助Runnable对象创建线程对象 Thread(String
name),创建线程对象,并命名; Thread(Runnable target,String name)通过runnable来进行创建线程对象,并且进行命名
有名字的构造方法就是为了方便调试
3)咱们给线程起一个名字,本质上是为了方便程序员来进行调试,我们起一个啥样的名字是不会影响线程本身的执行的,仅仅只是影响程序员来进行调试,
我们可以在工具中看到每一个线程以及名字,这样就很容易在调试中对线程进行区分,只是程序调试的小功能,并不会对代码本身的功能造成影响

4)我们在C:\Program
Files\Java\jdk1.8.0_301\bin中的jconsole.exe就可以罗列出我们系统上面的Java进程,jconsole.exe就是一个方便与程序员进行调试的工具
import java.util.concurrent.TimeUnit; class MyRunnableT1 implements Runnable{
public void run(){ while(true){ try { TimeUnit.SECONDS.sleep(1000);
System.out.println("我是一个任务"); } catch (InterruptedException e) {
e.printStackTrace(); } } } } class MyRunnableT2 implements Runnable { public
void run() { try { TimeUnit.SECONDS.sleep(1000); System.out.println("我也是一个任务");
} catch (InterruptedException e) { e.printStackTrace(); } } } public class Main
{ public static void main(String[] args) { Thread t1=new Thread(new
MyRunnableT1(),"ThreadT1"); Thread t2=new Thread(new
MyRunnableT2(),"ThreadT2"); t1.start(); t2.start(); } }

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信