一、线程&进程
- 进程: 运行中的程序,即指应用程序在内存中分配的空间。
- 线程: 进程中负责程序运行的执行单元,也叫执行路径。
- 线程任务: 线程执行的代码内容。
- 并行: 多个cpu实例或者多台机器同时执行一段处理逻辑,实现真正的同时。
- 并发: 通过cpu的调度算法,快速的切换线程执行线程任务,并不是真正意义上的同时。
- 线程安全: 同一段代码在多线程使用下,不会因为线程的调度顺序而影响任何结果。在出现共享资源的情况下容易出现线程安全问题。确保线程的安全要优先于性能。
- 同步: 同步是指通过代码进行调度和控制,确保共享资源不会因为多线程的使用造成线程安全问题。
多线程技术解决了多部分代码同时执行的需求,能够更好的利用cpu的资源。
java程序中(JVM)至少有两个线程,一个负责自定义代码运行(主线程),一个负责垃圾回收(垃圾回收线程)。其中主线程运行的任务都定义在main
方法中,垃圾回收线程的任务都定义在finalize
方法中。
二、线程的状态
线程主要可以归纳为五种状态:New、Runnable、Running、Blocked、Dead
如上图所示,我们对这五种状态进行解析
1.New
线程新建状态,当创建了线程对象之后,该线程即进入了线程新建状态。
2.Runnable
可运行状态,当调用了线程对象的start()
或join()
中断、sleep()
结束、I/O完成、yield()
等,该线程就进入了可运行状态,等待获取CPU的使用权,注意,这个时候并没有开始执行线程任务。
3.Running
运行状态,当CPU开始调度该线程时,该线程进入运行状态,开始执行线程任务
4.Blocked
阻塞状态,这个状态比较复杂,具体还可以细分为三种不同的阻塞状态
- 等待阻塞
当运行中的线程调用wait()
方法时,该线程会释放同步锁并进入到等待池当中。
- 同步阻塞
线程在获取同步锁失败时或当等待阻塞状态中的线程被notify()/notifyAll()
唤醒,会进入到锁定池中。
- 其他阻塞
调用线程的sleep()
或join()
或发出了I/O请求时,线程也会进入到阻塞状态,注意,sleep()
不释放同步锁。
5.Dead
死亡状态,当线程执行完了或者因异常退出了run()
方法,该线程结束生命周期。
三、多线程的实现
main
函数也是一个线程,在java中所有线程都是同时启动的,然后根据CPU的调用算法来运行线程。
因此每次运行java程序至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程。
多线程的实现主要有三种方法:
- 继承
Thread
类,重写run()
方法 - 实现
Runnable
接口,重写run()
方法,在将该实现类对象作为Thread
类的构造器参数 - 使用
Callable
和Future
接口创建线程
1.继承Thread类
继承Thread
类并复写其run
函数是比较常见的一种多线程实现方式,但较实现Runnable
接口的方式而言有一些劣势,这点在下文中解释。
public class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 0; i<10; i++) {
System.out.println(currentThread().getName() + "-------" + name);
}
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
MyThread one = new MyThread("ONE");
MyThread two = new MyThread("TWO");
one.start();
two.start();
}
}
2.实现Runnable接口(推荐)
实现Runnable
接口的方式来实现多线程有以下优点:
- 降低了线程对象和线程任务的耦合性。
- 避免了Thread类单继承。
- 将线程任务单独封装,更符合面向对象的思想。
- 适合多个相同的程序代码的线程去处理同一个资源。
具体实现如下
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
public void run() {
for(int i = 0; i<10; i++) {
System.out.println(Thread.currentThread().getName() + "-------" + name);
}
try {
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
MyRunnable one = new MyRunnable("A");
MyRunnable two = new MyRunnable("B");
Thread threadOne = new Thread(one);
Thread threadTwo = new Thread(two);
threadOne.start();
threadTwo.start();
}
}
我们发现,Runnable
接口和Thread
类中都有run
方法,那么在调用Thread
对象的start
方法时,究竟调用的是谁的run
方法呢?
public class Thread implements Runnable {
......
@Override
public void run() {
if (target != null) {
target.run();
}
}
.....
查看源码可以发现Thread
类其实也是实现了Runnable
接口,当调用Thread
对象的start
方法时,先调用了Thread
对象中的run
方法,判断在创建Thread
对象时候有没有传入Runnable
,若有则直接调用Runnable
的run
方法。
3.使用Callable接口和FutureTask类创建线程
这个方式实际上并不常用,具体流程是实现Callable
接口并实现其call
函数,然后通过FutureTask
类对Callable
实现类对象进行封装,之后再将该FutureTask
对象作为Thread
构造器的参数。
public class MyCallbale implements Callable<Integer> {
private String name;
public MyCallbale(String name){
this.name = name;
}
public Integer call() throws Exception {
int count = 0;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "......" + name);
count = i;
Thread.sleep(20);
}
return count;
}
}
public class Main {
public static void main(String[] args) {
MyCallbale one = new MyCallbale("A");
MyCallbale two = new MyCallbale("B");
FutureTask<Integer> futureTaskOne = new FutureTask<Integer>(one);
FutureTask<Integer> futureTasktwo = new FutureTask<Integer>(two);
Thread threadOne = new Thread(futureTaskOne);
Thread threadTwo = new Thread(futureTasktwo);
threadOne.start();
threadTwo.start();
}
}
四、线程常用函数
Thread
类中提供了一些列线程函数,常用的有如下函数:
1.join([long millis])
调用join()
会在线程启动时候起效,使其他线程等待该线程终止后再执行
2.sleep(long millis)
使当前线程进入等待阻塞状态,当设定的时间结束后,该线程进入可运行状态
3.yield()
yield()
方法也被称为"退让",调用yield()
方法会将当前线程转入可运行状态,将CPU使用权让给同等或更高优先级的其他可运行状态线程,因此在没有同等或更高优先级的其他可运行状态线程情况下,有可能在"退让"之后立即又被CPU执行。这也是yield()
与wait()
和sleep()
的不同之处,后两者后两者在调用后线程在指定时间内或被唤醒之前是肯定不会在被执行的。
4.setPriority(int priority)
设置线程优先级可直接使用1~10默认为5
5.interrupt()
使当前线程的中断状态设置为true,有几点需要注意的地方
- 若线程处于阻塞状态,将中断状态设为ture时,中断状态会被清除并抛出
InterruptedException
异常。例如:当前线程处于wait的等待阻塞状态,此时调用interrupt
,中断状态会被设置为true,但是因为处于阻塞状态,所以中断状态会立即被清除为false并抛出异常。 - 同理,若线程中断状态为ture,使线程进入阻塞状态则会清除中断状态并抛出
InterruptedException
异常。
6.interrupted()
使当前线程的中断状态设置为false
7.interrupted()
判断当前线程是否处于中断状态
8.currentThread()
获取当前线程对象
9.setDaemon(boolean)
将当前线程设置为守护线程。JAVA中有用户线程和守护线程两种线程,守护线程即是运行在后台的线程,其作用是为了给前台运行的用户线程提供便利服务的,例如java的垃圾回收线程。因此,当所有的用户线程运行结束,没有可以守护的线程时,JVM会退出,结束所有守护线程。在运用守护线程时有几点需要注意的:
setDaemon()
函数必须在线程启动前调用,否则会抛出IllegalThreadStateException
异常。- 在守护线程中产生的新线程也是守护线程。
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
10.isDaemon()
判断当前线程是否为守护线程