创建线程的方式
方式1:直接使用或继承于Thread类
大致步骤
1、创建一个继承于Thread类的子类
2、在该子类中重写Thread类的run()方法 (通过ctrl+o(override)输入run查找run方法)
3、创建该Thread子类的对象
4、通过此子类对象调用start()方法后此线程便可开启
示例代码
public static void main(String[] args) {
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
System.out.println("创建了一个线程: "+this.getName());
}
};
t1.start();
}
Thread实现任务的局限性
1、任务逻辑写在Thread类的run方法中,有单继承的局限性
2、创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享
Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为 线程体
- 通过Thread对象的start()方法来启动这个线程,而非直接调用run()
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。想要启动多线程,必须调用start方法。
- run()方法应该由JVM调用,至于什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以下的异常“IllegalThreadStateException”(不合法的线程状态)
start与run方法的区别
线程的启动:通常应该通过调用线程对象的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,这也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM会通过调用线程对象的run()方法来完成试机的操作,当run()方法结束之后,此线程就会终止。
如果直接调用线程对象的run()方法,它就会被当做一个普通的函数调用,程序中仍然只有主线程main这一个线程。也就是说,start()方法可以异步地调用run()方法,但是直接调用run()方法却是同步的,不能达到多线程的目的,由此我们可以知道,只有通过调用线程类的start()方法才能真正达到多线程的目的
结论:
若调用start方法,则先执行主线程的,后执行子线程的;若调用run方法,相当于在主函数中进行run函数的调用,按照程序的顺序执行,不能达到多线程目的;
一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。
方式2:实现Runnable接口方式
把【线程】和【任务】(要执行的代码)分开
Thread 代表线程; Runnable 可运行的任务(线程要执行的代码)
大致步骤
1.创建一个实现了Runable接口的类
2.用该实现类去实现Runnable中的抽象方法:run()
3.创建该实现类的对象
4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
代码示例
public static void main(String[] args) {
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
System.out.println("线程启动....");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
}
//========================//Java 8 以后可以使用 lambda 精简代码//==============
// 创建任务对象
Runnable task2 = () -> System.out.println("线程启动....");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
Runnable解决了Thread的任务单继承的局限性
开发中,优先选择实现Runable接口的方式
原因:
1:实现的方式没有类的单继承性的局限性
2:实现Runnable的方式更适合用来处理多个线程有共享数据的情况
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
但是Runbale相比Callable有以下的局限性
任务没有返回值
任务无法抛异常给调用方法
注意:
- 用这个方法创建的线程,一个实现了Runnable接口的实现类创建的多个对象一般共用这个实现类的资源
public class CreateDemo {
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
Thread t1=new Thread(myRunnable,"t1");
Thread t2=new Thread(myRunnable,"t2");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
private int i=0;
@Override
public void run() {
i++;
System.out.println("=====实现了Runnable中的RUN方法===="+i);
}
}
上面的代码中,两个线程t1,t2传入同一个任务myRunnable,且MyRunnable中i的变量不是静态的,结果输出结果如下,说明两个线程t1、t2共享了这个变量
- 直接用Thread类来创建的线程,尽管只有一个类继承了Thread类,但是这个类的每一个对象都是单独地拥有一份类的资源,除非资源被关键字static修饰。
方式3、使用FutureTask 配合 Thread 实现callable接口方式
创建步骤:
- 1.创建一个实现callable的实现类
- 2.实现call方法,将此线程需要执行的操作声明在call()中
- 3.创建callable实现类的对象
- 4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
- 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
示例代码
public class CreateDemo {
public static void main(String[] args) {
MyCallable myCallable=new MyCallable();
FutureTask futureTask=new FutureTask(myCallable);
Thread t1=new Thread(futureTask,"t1");
Thread t2=new Thread(futureTask,"t2");
t1.start();
t2.start();
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
System.out.println("实现Callable接口.....");
return "call";
}
}
与实现runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常、支持泛型的返回值,可以借助FutureTask类,获取返回结果
但是为什么上面的例子中明明开启了两个线程却只是启动一个???
线程运行的大概原理
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈) ,我们都知道 JVM 内存模型由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
线程常见方法
Thread类的每一个实例代表一个jvm中的线程 , 除此之外的任何东西都不代表线程 , 故Runnable/Callable都不是线程 ,而是任务。
Thread线程类构造方法:
Thread() :创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name) :创建新的Thread对象
普通方法
1、void start():
start()方法用来启动一个线程,当调用start方法后,会执行对象的run()方法。系统才会开启一个新的线程来执行用户定义的子任务(就绪变为运行),在这个过程中,会为相应的线程分配需要的资源。
start()方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的。start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException。
2、run():
该方法是用来写线程在被调度时执行的操作,run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
注意:在run方法中的异常不能throws,只能try catch。因为run方法在父类中没有抛出任何异常,子类就不能比父类抛出更大的异常
3、String getName():
返回线程的名称
4、void setName(String name):
5、static Thread currentThread():
返回当前正在执行的线程的引用【可以返回main线程的引用】。在Thread子类中就是this代表当前线程,通常用于主线程和Runnable实现类
6、static void yield():
yield() :线程让步。调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的优先级更高的线程。
它跟sleep方法类似,同样不会释放锁。
但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
但是调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配,这一点是和sleep方法不一样的。
暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
若队列中没有同优先级的线程,忽略此方法
7、join() :
当某个程序执行指令流中调用其他线程的 join() 方法时,调用该方法的线程将被阻塞,直等待调用join方法的线程对象运行结束。
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间,等待某线程执行完成后恢复运行。
如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。
使用这个方法使得低优先级的线程也可以获得执行权
8、static void sleep(long millis) :
(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
抛出InterruptedException异常
sleep方法相当于让当前线程睡眠,进入阻塞状态,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
当休眠时间结束后,重新争抢cpu的时间片继续运行。
面试题注意:有两个Thread对象t1,t2线程,那么在main主方法中调用t1.sleep()方法,t1线程会进行休眠么?
答案是不会,因为sleep方法是静态方法,在main方法中执行到t1.sleep时,会转化为Thread.sleep()执行,而sleep方法出现在那个线程,那个线程就会进行休眠,所以t1不会休眠,会休眠的是main主线程。这也是静态方法的特色。
如果sleep调用之后睡眠太久了,如何让它在睡到中途的时候醒来?(不是中断线程的执行而是中断线程的睡眠)
答案是调用睡眠线程对象的interrupt方法,这种中断的方式利用了Java的异常处理机制,interrupt方法执行后会让调用的线程对象的睡眠出异常,因为在run方法中调用sleep方法必须使用try catch的方式抓取异常, interrupt调用后报出的异常就会被抓取从而中断了该线程的睡眠。
9、stop():
强制线程生命期结束,不推荐使用。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。
10、boolean isAlive():
返回boolean,判断线程是否还活着
11、wait():
wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。?、?
Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。
从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。
相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
12、interrupt():
其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true
*不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!???
示例代码:
13、interrupted()
作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。(除非在第二次调用之前再次被中断)
14、isInterrupted()方法
作用是只测试此线程是否被中断 ,不清除中断状态。
注意:
interrupted()作用于当前线程,interrupt()和isInterrupted()作用于此线程,即代码中调用此方法的实例所代表的线程。
其他方法:
获取线程属性的几个方法:
1)getId
用来得到线程ID
2)getName和setName
用来得到或者设置线程名称。
3)getPriority和setPriority
用来获取和设置线程优先级。
4)setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:
守护线程是为其他线程提供服务,依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
sleep()和yield()的区别
yield跟sleep方法类似,同样不会释放锁。
sleep()和yield()的区别):sleep()使当前线程进入停滞阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行的就绪状态,**所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。**<br /> sleep 方法使当前运行中的线程睡眠一段时间,进入不可运行状态,这段时间的长短是由程序设定的,**yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。**实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,**否则,继续运行原来的线程。**所以yield()方法称为“退让”,**它把运行机会让给了同等优先级的其他线程**<br /> 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态**,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。**
在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。