今天记录一下线程吧。
========== 【什么是线程】==========
Java中在遇到并发时,常常会用多线程来解决,首先我们先来了解一下什么是线程,面试中最常被问到的就是进程和线程的区别是什么。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
线程和进程的区别
1. 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
2.操作系统会为进程分配资源,而不会为线程分配资源,线程想要使用资源只能从其所属进程获取,一个进程中的各个线程共享这个进程的所有资源。
3.线程是进程的,一个进程可以有多个线程,一个线程只能属于一个进程,线程不能脱离进程单独存在。
4.一个进程至少要有一条进程。
就这四条吧,这只是最基本的,往深了挖应该还有好多,不过以我目前的水平,我觉得还是算了,不过先码住。
========== 【线程的基本状态】==========
新建:线程刚被创建出来的状态。
可执行:线程被创建出来后,调用start()方,线程等待被调度算法选中,处于可执行状态。
执行:线程被调度算法选中,获取cpu,处于执行状态。
阻塞:由于某事件的发生,导致线程无法继续执行,暂时停止运行,处于阻塞状态。
死亡:线程执行结束,或者因某事件发生而抛出异常,线程就处于死亡状态,死亡线程不可再生。
========== 【线程的三种实现方式】==========
继承Thead类
定义一个类继承Thread类,这个类就变成了线程类,然后重写Thread类中的run方法,当调用该线程类的start方法时就会执行run方法中的代码。
public class Test1 {
public static void main(String[] args) {
T1 t1 = new T1();
T1 t2 = new T1();
t1.start();
t2.start();
System.out.println("main");
}
static class T1 extends Thread {
@Override
public void run() {
//获得线程名
String n = getName();
//1到1000
for (int i = 1; i <= 1000; i++) {
System.out.println(n+" - "+i);
}
}
}
}
实现Runnable接口
准备一个类实现Runnable接口,该类就变成了一个线程类,然后,实现Runnable接口中的run方法,当调用该线程类中的start()方法时,会执行run方法中的代码。
public class Test2 {
public static void main(String[] args) {
R1 r1 = new R1();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
//线程启动后,自动执行 r1.run()
t1.start();
t2.start();
}
static class R1 implements Runnable {
@Override
public void run() {
//获得正在执行这行代码的线程(当前线程)
Thread t = Thread.currentThread();
//获得线程名
String n = t.getName();
//1到1000
for (int i = 1; i <= 1000; i++) {
System.out.println(n+" - "+i);
}
}
}
}
实现Callable接口FutureTask包装器来创建线程
实现Callable接口需要实现它的call()方法,这种方式可以创建一个带有返回值的线程,实现Callable接口的目的,主要是FutureTask这个类引用了Callable接口,所以它的构造函数需要传入一个Callable接口的实现类。FutureTask间接实现了Runnable,接口和Future接口。Runnable接口中提供了创建线程的方法,Future接口中提供了设置任务超时时间的,判断任务状态等一系列方法。两者结合使用可以创建一个带有返回值的线程。
public class Animal implements Callable<String>{
@Override
public String call() throws Exception {
return null;
}
}
class test{
public static void main(String[] args) {
//创建Callable接口的实现类对象
Callable<String> AnimalCallable = new Animal();
//通过FutureTask来构建任务未来任务
FutureTask<String> oneTask = new FutureTask<String>(AnimalCallable);
//将任务传给Thread类构建线程对象
Thread oneThread = new Thread(oneTask);
//启动线程
oneThread.start();
}
}
总结:三种实现方式的区别
1.继承Thread类的方式,由于java单继承的特点,会占用继承位置,有些浪费。实现Runnable接口和Callable接口的方式,避免了这种问题。
2.继承Thread类和实现Runnable接口的方式,创建的线程没有返回值,而实现Callable接口创建的线程有返回值。
3.常用的还是Runnable接口的方法。具体看业务需求。
========== 【线程的常用方法】==========
方法 | 作用 |
---|---|
currentThread() | 获取当前运行线程对象的引用 |
yield() | 线程让步,表示当前线程愿意让出cpu给其他线程 |
sleep() | 让线程休眠一段时间 |
start() | 启动线程 |
setName() | 设置线程名称 |
getName() | 获取线程名称 |
getName() | 获取线程名称 |
interrupt() | 中断当前线程 |
setDaemon() | 设置当前线程是否为守护线程,当运行的唯一线程都是守护进程线程时,Java虚拟机将退出 |
join() | 将当前线程加入主线程,当前线程执行完之后,主线程才能继续执行 |
setPriority() | 设置线程优先级:默认为5,优先级别1-10 |
========== 【线程安全和数据访问冲突】==========
问题:
当时用多个线程共同访问同一个资源时,非常容易出现线程安全的问题,例如当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失。而多个同时访问一个资源,其中一个线程修改了资源,就会出现数据访问冲突的问题,就会导致其他线程访问到的数据不完整。
解决方法:线程同步
1.synchronized关键字实现线程同步
同步方法:即将目标方法用synchronized关键字修饰之后,目标方法就会变成同步方法,在Java中,每个对象都有一个内置的对象所锁,当线程访问被synchronized关键字修饰的方法,即同步方法是,首先会获得这个对象锁,然后再去执行相应代码,等到执行结束之后,再释放锁,在此期间,此方法同一时间只允许被一个线程访问。这就保证了,多线程访问同一方法时,线程需要排队执行。避免了出现线程不安全和数据访问冲突问题。但是这样会大大降低程序运行的效率。
同步代码块:即将目标方法可能只有部分代码会引起线程安全问题,可以用synchronized关键字修饰该部分代码,该部分代码就变成同步代码块,该代码块同一时间只允许被一个线程访问。
2.使用Lock锁
可以使用Lock锁机制来实现线程同步,Lock锁机制提供比synchronized关键字更加灵活的体验,Lock可以灵活的获取释放锁。使用Lock也可以解决线程不安全和数据访问冲突的问题。
- 使用Lock的步骤:
- 1.创建Lock实现类的对象
- 2.使用Lock对象的lock方法加锁
- 3.使用Lock对象的unlock方法解锁
- 注意:可把unlock方法的调用放在finally代码块中,保证一定能解锁。