无论用哪种方式创建线程,线程的启动一律使用Thread.start(),run()不能由用户直接调用,而是由JVM调用,run()如果由用户调用,则和调用普通方法没有区别。start()->start0()[本地方法]->进入到JVM中调用操作系统写好的方法来执行。
一个线程的start()只能调用一次,否则会抛出IllegalThreadStartException异常。
高并发:访问线程量非常高(CPU占用率达99.99%)
JVM的启动是多线程吗?是,因为JVM启动时,至少启动了垃圾回收线程和主方法线程,所以是多线程的。
一、进程和线程的概念
1.进程
进程:操作系统中一个程序的执行周期,是资源分配的最小单位。
操作系统是一组能有效地组织和管理计算机软硬件资源,合理地对各类任务进行调度 ,以及方便用户使用的程序的集合。
操作系统中是这样解释进程的:进程指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
DOS系统:单进程。
Windows系统:多进程,可以同时执行多个进程。
进程在哪?每当使用java命令去解释程序的时候,就表示启动了一个新的JVM进程。
线程的例子:当在QQ中聊天时,发送消息和接收消息都是同步的。
2.线程
线程:一个程序同时执行多个任务。每一个任务称为一个线程,进程中的一个子任务,任务分配的基本单位。
3.进程和线程的区别与关系
没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。
每个进程拥有自己的一整套变量,而线程则共享数据。多进程中,进程间没有任何关系,两个进程通信用到网络中的TCP/IP协议。多线程间可以共享数据,使得线程之间的通信比进程之间更有效,更方便。
1)调度的基本单位
在传统的操作系统中,进程是作为独立调度和分派的基本单位,因此进程是能够独立运行的基本单位。在引入线程的操作系统中,已经把线程作为调度和分派的基本单位,因此线程是能独立运行的基本单位。
2)并发性
在引入线程的操作系统中,不仅进程间可以并发执行,而且在一个进程中的多个线程间也可以并发执行,甚至一个进程中的所有线程都可以并发执行。从而更加有效地提高资源利用率和系统吞吐量。
3)拥有资源
进程可以拥有资源并作为系统中拥有资源的一个基本单位。然而,线程本身并不拥有系统资源,而是仅有一点必不可少的、能够支持其独立运行的资源。
4)独立性
在同一进程中的不同线程之间显然比不同进程之间的独立性低得多。
5)系统开销
在创建或撤销进程时,操作系统为此付出的开销明显大于线程创建或撤销时所付出的开销。
6)支持多处理机系统
在多处理机系统中,对于传统的进程,不管有多少处理机,该进程只能运行在一个处理机上。但对于多线进程,就可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行,加快了进程的完成。
主要区别:
创建或撤销的消耗:创建或销毁一个进程要比创建或销毁一个线程消耗得多。
通信的方便性:线程间通信要比进程间通信方便得多。
5.高并发
含义:访问的线程量非常高。(多用户访问)
高并发引起的问题:服务器内存不够用,程序资源竞争,无法处理新的请求。
二、线程的状态
线程的状态共有:创建、就绪、运行、阻塞、终止。(或者新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead))
系统调用通过控制CPU分配时间片来运行线程。
导致阻塞的事件如:没有资源。在阻塞状态中,操作系统收回CPU,执行其它工作。
(1)新建状态:用new语句创建的线程对象处于新建状态(New),此时它和其它对象一样,仅仅在堆区中分配了内存。
(2)就绪状态:当一个线程对象创建好后,其它线程调用该线程对象的start(),该线程就进入了就绪状态(Runnable)
(3)运行状态:处于运行状态的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只有一个线程处于运行状态,如果有多个CPU,那么同一时刻可以让几个线程占用不同的CPU,使它们都处于运行状态。只有处于就绪状态的线程才有机会转到运行状态。
(4)阻塞状态:线程出于某些原因放弃CPU,暂时停止运行。
出现阻塞的情况大体分为如下5种:
1. 线程调用 sleep方法,主动放弃占用的处理器资源。
2. 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4. 线程等待某个通知。
5. 程序调用了 suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。
(5)死亡状态:当线程退出run()后,就进入死亡状态(Dead),该线程结束生命周期。线程有可能是正常执行完run()退出,也有可能是遇到异常退出的。
三、线程创建的方式
方式一、继承Thread类,覆写run().
方式二、通过实例化Runnable接口/Callable接口的类的对象,覆写run().或者call()
具体:2.1 无返回值:1)先创建实现Runnable接口的类(类中覆写run())。
2) 然后创建该类对象,通过传对象给Thread对象的构造方法,用Thread对象来调用start()。
2.2 有返回值:1)创建实现Callable接口的类Mythread,覆写无参的call(),
2)在主函数中用FutureTask<> task=new FutureTask<>(Mythread对象)
3)再用new Thread(task).start()启动线程,用task.get()接收返回结果。
说明:1.多线程的启动永远都是Thread类的start()方法。
2.此时的Runnable对象可以采用匿名内部类的方法或者lambda表达式定义。
方式三、线程池
具体方法请见线程池
调用run()还是start()?
调用run()不会启动线程,只是方法的普通调用,与多线程无关。而调用start(),启动多线程,最终仍然会执行run(),这个方法只会执行一次,但是每个线程的执行时间不一定,体现了多线程的特点。因此应该调用start()。
调用run():
package com.xunpu.a;
/**
* 线程创建
* 方式一:直接继承Thread类,覆写该类中的run()。
*/
class MyThread extends Thread{
private String title;
public MyThread(String title){
this.title=title;
}
@Override
public void run(){
for(int i=0;i<3;i++){
System.out.println(this.title+",i="+i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread myThread1=new MyThread("thread1");
MyThread myThread2=new MyThread("thread2");
MyThread myThread3=new MyThread("thread3");
myThread1.run();
myThread2.run();
myThread3.run();
}
}
调用start():
package com.xunpu.a;
/**
* 线程创建
* 方式一:直接继承Thread类,覆写该类中的run()。
*/
class MyThread extends Thread{
private String title;
public MyThread(String title){
this.title=title;
}
@Override
public void run(){
for(int i=0;i<3;i++){
System.out.println(this.title+",i="+i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread myThread1=new MyThread("thread1");
MyThread myThread2=new MyThread("thread2");
MyThread myThread3=new MyThread("thread3");
myThread1.start();
myThread2.start();
myThread3.start();
}
}
每次执行的结果都可能不同:
方式二:定义一个实现Runnable接口(克服了单继承的缺陷)的类,实例化该类,通过Thread类的构造方法实现创建。
package com.xunpu.a;
/**
* 创建线程方式二:
* 定义一个实现Runnable接口(克服了单继承的缺陷)的类,实例化该类,通过Thread类的构造方法实现创建。
*/
class Mythread implements Runnable{
private String title;
public Mythread(String title){
this.title=title;
}
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(this.title+",i="+i);
}
}
}
public class Demo2 {
public static void main(String[] args) {
Mythread mythread1=new Mythread("thread1");
Mythread mythread2=new Mythread("thread2");
Mythread mythread3=new Mythread("thread3");
new Thread(mythread1).start();
new Thread(mythread2).start();
new Thread(mythread3).start();
}
}
上面的几种方式都是没有返回值的写法,下面是有返回值的创建线程方法。
具体做法:实现Callable接口,覆写无参的call(),传对象给FutureTask<>对象,再传FutureTask对象给Thread对象,调用start(),用task.get()接收返回结果。
package com.xunpu.a;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 继承Callable类,有返回值。
*/
class Mythread3 implements Callable<String> {
private int ticket=5;
//覆写无参的call方法
public String call(){
while(this.ticket>0){
System.out.println("剩余票数:"+this.ticket--);
}
return "票卖完了";
}
}
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread3 mythread=new Mythread3();
FutureTask<String> task=new FutureTask<String>(mythread);
new Thread(task).start();
new Thread(task).start();
System.out.println(task.get());
}
}
四、和线程有关的方法
1.public Thread(Runnable runnable,String name)--构造方法,创建线程的时候设置名称。
2.public final synchronized void setName(String name)--设置线程名称
3.public final String getName()--取得线程名称
说明:1)如果线程没有设置名称,那么系统会自动分配一个名字,为Thread-i(i从0开始)。
2)主线程的名称为:main
3) 线程名字如果要设置应避免重复,同时中间不能修改。
package com.xunpu.a;
/**
* 和线程有关方法的使用
*/
public class Demo3{
public static void main(String[] args) {
Mythread2 mt=new Mythread2();
Thread thread=new Thread(mt,"ThreadA");//用构造方法设置线程名称。
thread.start();
}
}
class Mythread2 implements Runnable {
public void run() {
Thread.currentThread().setName("ThreadB");//通过setName()修改最终的线程名。
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);//使用Thread.currentThread().getName()获取线程名
}
}
}