多线程作为Java中很重要的一个知识点,在此还是有必要学习一下的
线程与进程
进程 是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。
1. 创建线程的三种方式
1.1 继承 Thread 类,重写该类的run()方法
class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
测试:
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread myThread1 = new MyThread(); // 创建一个新的线程 myThread1 此线程进入新建状态
Thread myThread2 = new MyThread(); // 创建一个新的线程 myThread2 此线程进入新建状态
myThread1.start(); // 调用start()方法使得线程进入就绪状态
myThread2.start(); // 调用start()方法使得线程进入就绪状态
}
}
}
}
继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机
1.2 实现Runnable接口
class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
Thread thread2 = new Thread(myRunnable);
thread1.start(); // 调用start()方法使得线程进入就绪状态
thread2.start();
}
}
}
}
1.3 两种创建线程方式区别
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Runnable myRunnable = new MyRunnable();
Thread thread = new MyThread(myRunnable);
thread.start();
}
}
}
}
class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
System.out.println("in MyRunnable run");
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
class MyThread extends Thread {
private int i = 0;
public MyThread(Runnable runnable){
super(runnable);
}
@Override
public void run() {
System.out.println("in MyThread run");
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
同样的,与实现Runnable接口创建线程方式相似,不同的地方在于
Thread thread = new MyThread(myRunnable);
那么这种方式可以顺利创建出一个新的线程么?答案是肯定的。至于此时的线程执行体到底是MyRunnable接口中的run()方法还是MyThread类中的run()方法呢?通过输出我们知道线程执行体是MyThread类中的run()方法。其实原因很简单,因为Thread类本身也是实现了Runnable接口,而run()方法最先是在Runnable接口中定义的方法。
public interface Runnable {
public abstract void run();
}
我们看一下Thread类中对Runnable接口中run()方法的实现:
@Override
public void run() {
if (target != null) {
target.run();
}
}
也就是说,当执行到Thread类中的run()方法时,会首先判断target是否存在,存在则执行target中的run()方法,也就是实现了Runnable接口并重写了run()方法的类中的run()方法。但是上述给到的列子中,由于多态的存在,根本就没有执行到Thread类中的run()方法,而是直接先执行了运行时类型即MyThread类中的run()方法。
1.4 使用Callable和Future接口创建线程
具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
看着好像有点复杂,直接来看一个例子就清晰了:
public class ThreadTest {
public static void main(String[] args) {
Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
thread.start(); //线程进入到就绪状态
}
}
System.out.println("主线程for循环执行完毕..");
try {
int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<Integer> {
private int i = 0;
// 与run()方法不同的是,call()方法具有返回值
@Override
public Integer call() {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:
public class FutureTask<V> implements RunnableFuture<V> {
//....
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕…”则很可能是在子线程循环中间输出。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕…”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?
原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值
上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法
2. 线程常见方法
2.1 sleep()——线程休眠
方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
- sleep(时间):指定当前进程阻塞的毫秒数
- sleep时间到达之后,线程进入就绪状态(等待CPU调度之后再执行)
- sleep()可以模拟网络延时、倒计时等等
- 每一个对象都有一个锁,sleep不会释放锁
测试:
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyThreadTest {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM-dd :hh:mm:ss");
System.out.println(Thread.currentThread().getName() + " 线程开始时间:" + dateFormat.format(new Date()));
try {
Thread.sleep(10000); //会出现异常 中断异常
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程结束时间:" + dateFormat.format(new Date()));
}
}
运行结果:
Connected to the target VM, address: '127.0.0.1:51269', transport: 'socket'
Thread-0 线程开始时间:2020-10-21 :12:31:44
Thread-0 线程结束时间:2020-10-21 :12:31:54
Disconnected from the target VM, address: '127.0.0.1:51269', transport: 'socket'
2.2 yield()——线程礼让
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
- 线程礼让是让当前正在执行的线程暂停,但不阻塞;
- 将线程从运行状态转为就绪状态,同其他都属于就绪状态的线程一起再次接受CPU的调度;
- 礼让后让CPU重新调度,但礼让不一定都能礼让成功(即让其他线程先执行),而是由CPU决定
测试:
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"A ").start();
new Thread(myYield,"B ").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"is running......");
Thread.yield(); //线程礼让
System.out.println(Thread.currentThread().getName()+"is stopped......");
}
}
运行结果:
Connected to the target VM, address: '127.0.0.1:51352', transport: 'socket'
B is running......
A is running......
B is stopped......
A is stopped......
Disconnected from the target VM, address: '127.0.0.1:51352', transport: 'socket'
2.3 join()——等待 线程执行完毕
在很多情况下,主线程创建并启动了线程,如果子线程中药进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁
public class JoinThread extends Thread{
public JoinThread(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws InterruptedException {
// 启动子进程
new JoinThread("new thread").start();
for (int i = 0; i < 10; i++) {
if (i == 5) {
JoinThread th = new JoinThread("joined thread");
th.start();
th.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
运行结果:
Connected to the target VM, address: '127.0.0.1:51538', transport: 'socket'
main 0
main 1
main 2
main 3
main 4
new thread 0
new thread 1
joined thread 0
joined thread 1
joined thread 2
joined thread 3
joined thread 4
new thread 2
new thread 3
new thread 4
main 5
main 6
main 7
main 8
main 9
Disconnected from the target VM, address: '127.0.0.1:51538', transport: 'socket'
由上可以看出main主线程等待joined thread线程先执行完了才结束的。如果把th.join()这行注释掉,运行结果如下:
Connected to the target VM, address: '127.0.0.1:51565', transport: 'socket'
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
new thread 0
new thread 1
new thread 2
new thread 3
new thread 4
joined thread 0
joined thread 1
joined thread 2
joined thread 3
joined thread 4
Disconnected from the target VM, address: '127.0.0.1:51565', transport: 'socket'
3. 线程的五种状态
-
新建状态:即单纯地创建一个线程,创建线程有三种方式,在我的博客:线程的创建,可以自行查看!
-
就绪状态:在创建了线程之后,调用Thread类的start()方法来启动一个线程,即表示线程进入就绪状态!
-
运行状态:当线程获得CPU时间,线程才从就绪状态进入到运行状态!
-
阻塞状态:线程进入运行状态后,可能由于多种原因让线程进入阻塞状态,如:调用sleep()方法让线程睡眠,调用wait()方法让线程等待,调用join()方法、suspend()方法(它现已被弃用!)以及阻塞式IO方法
-
死亡状态:run()方法的正常退出就让线程进入到死亡状态,还有当一个异常未被捕获而终止了run()方法的执行也将进入到死亡状态!
这里的线程状态我在jvm里面写过,有兴趣的可以以看一下:
深入理解Java虚拟机—Java内存模型与线程