多线程(上)
传送门:多线程(下)
1. 基本概念
-
什么是进程?什么是线程?
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列。进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位。一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
-
什么是并发?什么是并行?
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
-
对java程序来说,会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护、回收垃圾。最起码,java程序中至少有连个线程并发。
-
进程与进程之间资源内存独立不共享,线程和线程间堆内存和方法区内存共享,但栈内存独立,即一个线程一个栈,互不干扰,这就是多线程并发,目的是为了提高程序的处理效率。
-
使用了多线程机制之后,main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还再压栈弹栈。
2. 实现线程的方式
(1)第一种方式
编写一个类,直接继承java.lang.Thread,重写run方法
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args) throws Exception {
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start();
//这里的代码运行在主线程中
for(int i = 0; i < 1000; i++){
System.out.println("主线程-->" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)
for(int i = 0; i < 1000; i++){
System.out.println("分支线程-->" + i);
}
}
}
执行结果(交替):
- start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了,start()方法结束,线程就启动成功了
- 启动成功的线程会自动调用run方法,不需要手动调用,由JVM线程调度机制来运作的,并且run方法在分支栈的栈底部(压栈)
- run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main平级
- 如果在main方法中不调用start(),而是直接调用run(),则不会启动新的线程
(2)第二种实现方式
编写一个类实现java.lang.Runnable接口
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args) throws Exception {
//创建一个可运行的对象
MyRunnable myRunnable = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread myThread = new Thread(myRunnable);
//Thread myThread = new Thread(new MyRunnable);
//启动线程
myThread.start();
//这里的代码运行在主线程中
for(int i = 0; i < 1000; i++){
System.out.println("主线程-->" + i);
}
}
}
//这不是一个线程类,是一个可运行的类。它还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)
for(int i = 0; i < 1000; i++){
System.out.println("分支线程-->" + i);
}
}
}
注:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。否则用第一种继承Thread,再想继承其他类就无法继承了。
(3)第二种的匿名内部类实现
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args) throws Exception {
//创建线程对象,采用匿名内部类的方式
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 1000; i++){
System.out.println("分支线程-->" + i);
}
}
});
//启动线程
myThread.start();
//这里的代码运行在主线程中
for(int i = 0; i < 1000; i++){
System.out.println("主线程-->" + i);
}
}
}
(4)第三种方式
实现Callable接口(JDK 8新特性)
这种方式实现的线程可以获取线程的返回值,前两种方式是无法获取线程返回值的,因为run方法返回void
-
使用场景
系统委派一个线程区执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们就会需要用这种方式去拿到这个执行结果
public class Test {
public static void main(String[] args) {
//创建一个“未来任务类”对象
//匿名内部类
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method end");
int a = 1;
int b = 2;
return a+b;
}
});
//需要把未来任务类对象传进去
Thread t = new Thread(task);
t.setName("t");
t.start();
Object obj = null;
try {
//在主线程中获取t线程的返回值
//get方法需要处理异常
obj = task.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println(obj);
}
}
main方法想要执行必须等待get()方法的结果,而get()方法可能需要等待很久,因为get()方法是为了拿到另一个线程的结果,另一个线程是需要时间的。
- 总结
- 优点:可以获取到线程的执行结果
- 缺点:效率比较低,在获取t线程执行结果的时候,当前线程收到阻塞,效率较低
3. 线程的生命周期
4. 获取当前线程对象
-
获取线程对象的名字
String name = 线程对象.getName();
-
修改线程对象的名字
线程对象.setName("线程名字");
-
若当前线程没有设置名字的时候,默认的名字:Thread-0、Thread-1、Thread-2、Thread-3
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args) throws Exception {
//创建线程对象
MyThread myThread = new MyThread();
//设置线程的名字
myThread.setName("ttt");
//获取线程的名字
String threadName=myThread.getName();//ttt
System.out.println(threadName);
}
}
class MyThread extends Thread{
@Override
public void run() {
}
}
-
获取当前对象
static Thread currentThread(); //静态方法! 返回一个Thread对象
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args) throws Exception {
Thread currentThread = Thread.currentThread();
//这个代码出现在main方法当中,所以当前线程就是主线程
System.out.println(currentThread.getName());//main
}
}
示例:
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args) throws Exception {
MyThread t1 = new MyThread();
t1.setName("t1");
t1.start();
MyThread t2 = new MyThread();
t2.setName("t2");
t2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
//currentThread就是当前线程对象,现在对象是谁?
//当t1线程执行run()方法时,当前线程就是t1
//当t2线程执行run()方法时,当前线程就是t2
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()+"--->"+i);
}
}
}
输出结果:
5. 线程睡眠sleep
static void sleep(long millsis)
-
静态方法:Thread.sleep(1000);
-
参数是毫秒
-
要解决异常
-
作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让其他线程使用
这行代码出现在A线程中,A线程会休眠。出现在B线程,B线程休眠
public class Demo {
//main方法的代码属于主线程
public static void main(String[] args){
try {
Thread.sleep(1000*5); //休眠5秒, 过5秒钟输出hello world
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world");
}
}
使用Thread.sleep()方法,可以做到效果:间隔特定的时间,去执行一段特定的代码,每隔多久执行一次
常见问题:
public class Demo {
public static void main(String[] args){
Thread t = new MyThread();
t.start();
try {
t.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world");
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
由于sleep为静态方法,在执行时会被转成Thread.sleep(1000*5),让当前线程进入休眠,也就是main线程进入休眠。这行代码出现在main,main线程休眠
所以该程序的运行结果为:循环马上直接完毕,过5s后输出hello world
6. 终止线程睡眠
线程对象.interrupt()
public class Demo {
public static void main(String[] args) {
Thread t = new Thread(new MyThread());
t.setName("t");
t.start();
//希望5秒后,t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t线程的睡眠
t.interrupt();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ "---> begin");
try {
//这里的异常只能try..catch,不能抛出
//因为run方法在父类中没有抛出任何异常,子类不能父类抛出更多的异常
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "---> end");
}
}
输出结果:
显然,这种终止线程睡眠的方式是依靠了java的异常处理机制,因为它打印出来了异常信息。如果不想异常信息打印出来的话,可以将run方法中的异常信息注释掉。
7. 线程的终止
在java中对线程进行终止,我们常用两种方法,一种是调用线程对象的stop方法,另一种是添加布尔标记。更常用的是第二种。
-
第一种:stop方法
public class Demo { public static void main(String[] args) { Thread t = new Thread(new MyThread()); t.setName("t"); t.start(); try { Thread.sleep(1000*5); } catch (InterruptedException e) { e.printStackTrace(); } //5秒后线程t停止 t.stop(); } } class MyThread implements Runnable{ @Override public void run() { for(int i=1;i<10;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
输出结果:
这种方式存在很大的缺点:容易丢失数据。因为这种方式直接将线程杀死了,线程没有保存的数据将丢失,不建议使用
-
第二种:布尔标记
public class Demo { public static void main(String[] args) { MyThread m = new MyThread(); Thread t = new Thread(m); t.setName("t"); t.start(); try { Thread.sleep(1000*5); } catch (InterruptedException e) { e.printStackTrace(); } m.setRun(false); } } class MyThread implements Runnable{ private boolean run = true; public boolean isRun() { return run; } public void setRun(boolean run) { this.run = run; } @Override public void run() { for(int i=1;i<10;i++){ if(run){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
8. 线程的调度
-
常见的线程调度模型
- 抢占式调度模型:哪个线程的优先级比较高,抢到的CPU时间片的概率就多一些。java采用的就是抢占式调度模型
- 均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样。
-
java中提供的与线程调度有关的方法
实例方法:
-
void setPriority(int newPriority)设置线程的优先级
-
int getPriority()获取线程优先级
最低优先级是1,默认优先级是5,最高优先级是10
优先级比较高的获取CPU时间片可能会多一些(大概率)
-
void join() 合并线程
静态方法:
-
static void yield() 线程让位,暂停当前正在执行的线程对象,并执行其他线程
该方法不是阻塞方法。让当前线程让位,给其他线程使用。
该方法执行会让当前线程从“运行状态”回到“就绪状态”,但回到就绪之后,有可能会再次抢到
-
9. 线程的优先级
public class Demo {
public static void main(String[] args) {
System.out.println("最高优先级"+ Thread.MAX_PRIORITY); //10
System.out.println("最低优先级"+ Thread.MIN_PRIORITY); //1
System.out.println("默认优先级"+ Thread.NORM_PRIORITY); //5
}
}
-
如何获取当前线程的优先级
public class Demo { public static void main(String[] args) { Thread currentThread = Thread.currentThread(); System.out.println(currentThread.getName()+"线程的默认优先级是"+currentThread.getPriority()); //5 } }
-
如何设置当前线程的优先级
public class Demo { public static void main(String[] args) { Thread currentThread = Thread.currentThread(); currentThread.setPriority(2); System.out.println(currentThread.getName()+"线程的优先级是"+currentThread.getPriority());//2 } }
优先级较高的,会抢到的CPU时间片相对多一些
10. 线程让位和合并
-
让位
静态方法:Thread.yield();
让位:当前线程暂停,回到就绪状态,让给其他线程
-
合并
实例方法:线程对象.join() 该方法需要进行异常处理
将某个线程合并到当前线程中,当前线程受阻
public class Demo { public static void main(String[] args) { System.out.println("main begin"); Thread t = new Thread(new MyThread()); t.setName("t"); t.start(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main over"); } } class MyThread implements Runnable{ @Override public void run() { for(int i=1;i<10;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
没采用合并之前的输出结果:
采用合并之后的输出结果:注:合并线程不代表着另一个栈消失!