线程基础
进程
进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程
- 线程是进程中的一个执行单元(执行路径),负责当前进程中程序的执行,一个进程中至少有一个线程
- 单线程程序:即若有多个任务只能依次执行,当上一个任务执行结束后,下一个任务开始执行
- 多线程程序:一个进程可以让多个线程同时执行。一个核心的CPU在多个线程之间进行着切换动作,由于切换时间很短(毫秒甚至是纳秒级别),导致我们感觉不出来
线程的运行模式
- 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
- 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度
java中的线程
主线程
JVM
启动后,必然有一个执行路径(线程)从main
方法开始的,一直执行到main
方法结束,这个线程在java
中称之为主线程。
多线程内存图解
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
Thread类
构造方法
Thread()
,分配新的Thread
对象Thread(String name)
,分配名为name
的Thread
对象Thread(Runnable target)
,使用Runnable
的实现类对象创建Thread
对象
常用方法
- void
run
(),线程实际要执行的任务 - void
start
(),启动线程 - void
setName
(String name),设定线程名字 - String
getName
(),获得线程的名字 - static Thread
currentThread
(),获得当前运行的的线程的引用 - static void
sleep
(long ms),让当前线程休眠指定毫秒数
创建线程
可以通过继承Thread
类或实现Runnable
接口创建运行新线程。
继承Thread类
- 定义一个类继承
Thread
- 重写
run
方法 - 创建子类对象,就是创建线程对象
- 调用
start
方法,开启线程并让线程执行,同时还会告诉JVM
去调用run
方法
实现Runnable接口
- 定义类实现
Runnable
接口 - 覆盖接口中的
run
方法 - 创建
Thread
类的对象 - 将
Runnable
接口的子类对象作为参数传递给Thread
类的构造函数 - 调用
Thread
类的start
方法开启线程
相对创建Thread
的继承类,避免了单继承的限制,可以多继承。
实现接口方式的好处
- 实现Runnable接口避免了单继承的局限性,所以较为常用。
- 降低了耦合性。更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务
- 继承Thread类,线程对象和线程任务耦合在一起;一旦创建Thread类的子类对象,既是线程对象,又有线程任务。
- 实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
匿名内部类实现多线程程序
实现接口Runnable
的匿名内部类
new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 50; i++) {
System.out.println("inner " + i);
}
}
}).start();
继承Thread
的匿名内部类
new Thread() {
public void run() {
for(int i = 0; i < 50; i++) {
System.out.println("inner: " + i);
}
}
}.start();
线程的状态图
线程池
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大;除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。
- 线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
创建线程池对象
线程池创建工厂类Executors
,使其它创建线程池对象:
- public static ExecutorService
newFixedThreadPool
(int nThreads),返回线程池对象(ExecutorService
实现类对象)
使用线程池对象
- Future<?>
submit
(Runnable task),传入Runnable
实现类对象,并执行 - <T> Future<T>
submit
(Callable<T> task),传入Callable
实现类对象,并执行
传入Runnable接口任务
步骤
- 创建线程池对象
- 创建
Runnable
接口子类对象 - 提交
Runnable
接口子类对象 - 关闭线程池
代码示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolUsingRunnable {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
Task task = new Task();
service.submit(task);
service.submit(task);
service.submit(task);
// 手动销毁线程池
// service.shutdown();
}
}
class Task implements Runnable{
@Override
public void run() {
System.out.println("Running task in Thread " + Thread.currentThread().getName());
}
}
/*==============
运行结果:
Running task in Thread pool-1-thread-2
Running task in Thread pool-1-thread-1
Running task in Thread pool-1-thread-2
===============*/
传入Callable接口任务
Callable
可以抛异常,也可以有返回值。
步骤
- 创建线程池对象
- 创建Callable接口子类对象
- 提交Callable接口子类对象
- 关闭线程池
代码示例
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolUsingCallable {
public static void main(String[] args) throws InterruptedException, ExecutionException{
ExecutorService service = Executors.newFixedThreadPool(5);
Future<Integer> result;
result = service.submit(new ComputeTask(1,1));
System.out.println(result.get());
result = service.submit(new ComputeTask(2,3));
System.out.println(result.get());
result = service.submit(new ComputeTask(3,5));
System.out.println(result.get());
service.shutdown();
}
}
class ComputeTask implements Callable<Integer>{
private int x;
private int y;
public ComputeTask() {}
public ComputeTask(int x, int y) {
this.x = x;
this.y = y;
}
public int getSum() {
return x + y;
}
public Integer call() throws Exception {
return getSum();
}
}
/*
* running result:
* 2
* 5
* 8
*/
线程安全
如果一个程序中有多个线程,这些线程可能共同操作某些数据,每个线程每次在操作数据时可能需要一系列的步骤,如果这些步骤由于多线程被分割成几次来执行,可能导致逻辑错误、数据被破坏。
解决方案:synchronized
代码块、synchronized
方法、Lock
接口。
使用synchronized
使用同步的方式可以解决以上的问题。
同步代码块
synchronized (/*锁对象*/) {
// 可能会产生线程安全问题的代码
}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
同步方法
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
- 同步的非静态方法中实际的锁对象是
本类对象
(this
) - 同步的静态方法中实际的锁是
本类.class
使用Lock接口
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能;可以获得比synchronized
广泛的锁定操作。
- void
lock
(),上锁 - void
unlock
(),解锁
使用示例:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
死锁
当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况需要避免掉。
线程1:A锁 > B锁
线程2:B锁 > A锁
// 若线程1拿到了A锁,同时线程2拿到了B锁,那么就出现了死锁
等待唤醒机制
- void
wait
(): 等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。 - void
notify
(): 唤醒,唤醒线程池中被wait
()的线程,一次唤醒一个,而且是任意的。 - void
notifyAll
(): 唤醒全部:可以将线程池中的所有wait
() 线程都唤醒。
这上面的方法都在继承自Object
,因为同步锁可以是任意对象;上面所说的线程池,实际是指在该对象监视器(object's monitor
)上等待的那些线程