文章目录
一、认识线程
1.1概念
进程:是系统分配资源的最小单位,通过程序运行,表现在操作中动态执行的,被操作系统所管理的,简单来说一个程序的执行就是一个进程。
线程:是操作系统能进行运算调度的最小单位,也就是执行程序的最小单位,一个进程里默认有一个线程:即主线程。
1.2进程与线程的区别
- 作用
进程是系统分配资源的最小单位;
线程是执行程序调度的最小单位;
- 速度
进程有自己的独立地址空间;
线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小得多,同时创建一个线程的开销也比进程要小得多;
- 关系
进程中第一个线程是主线程,主线程创建其他线程,其他线程也可以创建线程,线程之间是平等的;
进程有父进程、子进程,独立的内存空间,唯一的进程标识符,pid;
- 创建
创建新的线程很容易,直接使用创建方法即可;
创建新的进程需要对父进程做一次复制;
- 操作
一个线程可以操作同一进程的其他线程,也就是同一进程中,线程与线程是平等的;
进程有父进程和子进程,父进程只能操作其子进程,而不能操作其父进程和其他进程;
- 通信
由于同一进程的各线程间共享内存和文件资源,所以线程间可以不通过内核进行直接通信;
进程之间不能进行直接通信;
二、多线程的使用
2.1多线程的作用
提高程序的运行效率,增加运行速度。
多线程也存在并行、并发。
2.2多线程的特性
原子性
原子性是指一个操作或者一系列操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
换句话说,如果有多个线程执行相同一段代码时,而你又能够预见到这多个线程相互之间会影响对方的执行结果,那么这段代码是不满足原子性的。
例如,n++操作,其实是三步操作:
- 将数据从内存读入CPU;
- 对数据进行++操作;
- 把进行++操作后的数据写回内存。
如果把n++操作用多线程的方式执行,就可能会导致结果不正确。例如线程一已结对n进行了++操作,而在把++后的数据写回内存之前,另外一个线程并发执行到了++操作,就会导致最后得到的数据进行了多次++操作,得到的结果并不是想要的,导致线程不安全,所以类似的自增、自减操作都不是原子性的。
可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,有可能共享变量的值在线程一的工作内存中已将改变了,但是没有及时写回主内存,导致线程二操作的时候可能还是主内存中之前的变量值,并没有看到线程一对共享变量所做的改变,所以最后得到的结果不正确,就是因为共享变量不可见引起的。
有序性
有序性的本义是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。但是在Java内存模型中,线程内代码是JVM、CPU都进行过重排序的,给我们的感觉就是线程内的代码是有序的,是因为重排序优化方案会保证线程内代码执行的依赖关系,即线程内看自己的代码运行都是有序的,但是看其他线程代码运行都是无序的。也就是说,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。也就是说在多线程中代码的执行顺序,不一定会与你直观上看到的代码编写的逻辑顺序一致。
2.3多线程的使用场景
- 提高效率:任务量比较多(单个任务比较耗时)的时候,或者任务数量也比较多的时候。
- 并发:阻塞式代码会导致后面的代码无法执行,可以使用多线程。
2.4创建线程的方法
- 继承Thread类:
(1)创建步骤:
- 自定义一个类继承Thread类;
- 重写run()方法,run()方法的方法体内写该线程需要完成的事情;
- 创建Thread子类的实例;
- 调用线程对象的start()方法启动线程。
(2)代码示例:
public class MainThread {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
- 实现Runnable接口:
(1)创建步骤:
- 自定义一个类实现Runnable接口,重写run()方法;
- 创建Runnable实现类的实例,并以此作为Thread的参数来创建对象;
- 调用Thread类对象的start()方法启动线程。
(2)代码示例:
public class MainThread {
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
}
}
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
- callable+Future:
(1)创建步骤:
- 实现callable接口,并重写call()方法;
- 使用FutureTask包装实现callable的对象,并以此作为Thead对象的参数,创建线程;
- 调用start()方法启动线程。
(2)代码实现:
import java.util.concurrent.*;
/**
* Callable创建线程:
* Future/FutureTask
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("call");
return 123;
}
};
//Thread使用Calllable
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();;
System.out.println("main");
Integer r = task.get();//当前线程阻塞等待,直到线程执行完毕(join效果差不多),但可以获取线程的返回值
System.out.println(r);
}
}
2.5 多线程的一般方法
2.5.1 Thread的常见构造方法
方法说明:
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
代码示例:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.5.2 Thread的常见属性
方法说明:”
属性 | 方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
是否被中断 | isInterrupted() |
- ID是线程的唯一标识,不同线程不会重复
- 名称也是线程的一种标识,可以通过此方法观察线程的运行顺序,在调试中用到
- 状态表示当前线程所处的一个情况
- 是否被中断是用来查看是否被其他线程通过中断方式将该线程中断
2.5.3 启动一个线程-start()方法
**线程启动:**让线程从创建到可以运行。
创建线程时重写的run()方法只是匿名内部类里一个方法的定义,不对线程造成影响,如果调用run()方法会把线程从就绪态转变为运行态,让线程直接运行起来。而调用线程的start()方法则是将线程从创建态即阻塞态转变为就绪态,才是真正把线程启动起来,所以启动一个线程必须调用start()来启动。
方法说明:
方法 | 说明 |
---|---|
void start() | 导致此线程开始执行; Java虚拟机调用此线程的run方法。 |
代码示例:
new Thread(new Runnable(){}).start();
2.5.4 等待一个线程-join()
线程等待:指的是让当前线程进行等待,直到调用join方法的线程执行完毕或一定时间再继续往下执行。意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run
方法先执行完毕之后在开始执行主线程。
方法说明:
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
代码示例:
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
});
t.start();
//当前线程:代码行执行的时候所在的线程
//t线程:线程引用对象
//当前线程进行阻塞(运行态--》阻塞态)等待(满足一定条件),t线程(不做仍和处理,让t执行运行)
//让线程进入等待的一定条件是:以下两个条件哪个先执行完都满足(等待到两个条件最短的时间点,然后往下执行)
// 1、传入的时间(时间值+时间单位毫秒)
//2、线程引用对象执行完毕
t.join();//先将t这个线程执行完毕,再往下执行
//t.join(2000);//传入时间单位
System.out.println(Thread.currentThread().getName());
}
}
2.5.5 休眠一个线程-sleep()
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
方法说明:
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
代码示例:
Thread t = new Thread(new Runnable(){});
t.start();
t.sleep(1000);
2.5.6 获取当前线程的引用-currentThread()
**当前线程引用:**表示当前正在执行的一个线程。
方法说明:
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
代码示例:
Thread t = new Thread(new Runnable(){
public void run(){
System.out.println(t.currentThread().getName());
}
});
t.start();
2.5.7 中断一个线程
线程中断:改变当前线程的中断标志位,并不是直接停止当前线程。
方法说明:
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
原理:通过中断标志位来实现
线程对象.interrupt():修改线程对象的中断标志位为true
线程对象.isInterrupted():获取线程对象的中断标志位
Thread.interrupted():获取当前线程的中断标志位并重置
线程启动以后,中断标志位=false。
线程因调用wait()/join()/sleep()处于阻塞态时,将线程中断,会导致:
- 在这三个阻塞方法所在的代码行,直接抛出InterruptedException异常;
- 抛出异常之后,重置线程的中断标志位=true。
代码示例:
public class InterruptThread{
public static void main(String[] args){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//阻塞状态时,通过捕获及处理异常,来处理线程的中断逻辑
try {
System.out.println(Thread.currentThread().isInterrupted());//true
Thread.sleep(3000);
//线程处于调动wait()/join()/sleep()阻塞时,如果把当前线程中断掉,会直接抛出一个异常
//抛出异常以后,线程中断标志位会进行重置
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().isInterrupted());//false
}
}
});
t.start();//t线程中的中断标志位=false
t.interrupt();//t线程的中断标志位=true
}
}
2.5.8 线程让步-yield()
线程让步: 暂停当前正在执行的线程对象,并执行其他线程。
意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意: 调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间。
方法说明:
方法 | 说明 |
---|---|
public static void yield() | 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。 |
代码示例:
public class ThreadYield {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
//等待new Thread所有线程执行完毕,否则一直等待
while(Thread.activeCount()>1){//使用调试的方式运行
Thread.yield();//将当前线程由运行态--》就绪态(线程让步)
}
System.out.println(Thread.currentThread().getName());
}
}