一、线程简介
1.什么是进程:
进程是程序的运行过程,是系统进行资源分配和调度的一个独立单位。通俗来讲,进程就是在操作系统中运行的程序,例如:电脑中运行的微信、eclipse、idea等。
2.什么是线程
线程是操作系统能够进行运算调度的最小单位。一个进程至少有一个线程,一个程序可以有多个线程。
3.多线程
如果一个程序可以同时运行多个线程,那么这个程序就是多线程的。例如看视频时程序同时进行加载图像、获取字幕和获取弹幕等。
二、线程的创建
1.继承Thread类
步骤:
(1)创建线程类,并继承Thread类
(2)重写run()方法,编写线程执行体
(3)创建线程对象,调用start()方法启动线程
示例代码如下:
public class TestThread extends Thread{
@Override
public void run() {
//重写run方法线程执行体
for (int i = 0; i < 100; i++) {
System.out.println("testThread线程第"+i+"次执行");
}
}
public static void main(String[] args) {
//创建线程对象
TestThread testThread = new TestThread();
//调用start()方法开启线程
testThread.start();//子线程开启,和主线程交替执行
//main线程执行一个for循换
for (int i = 0; i < 100; i++) {
System.out.println("main线程第"+i+"次执行");
}
}
}
执行结果如下图所示,testThread和main两个线程会交替执行100次。截取了部分输出结果。
2.实现Runnable接口
步骤:
(1)定义MyRunnable类实现Runnable接口
(2)实现run()方法,编写线程执行体
(3)创建Runnable接口的实现类对象
(4)创建线程对象,调用start()方法启动线程
示例代码如下:
public class TestRunnable implements Runnable{
@Override
public void run() {
//重写run方法线程体
for (int i = 0; i < 100; i++) {
System.out.println("testRunnable线程第"+i+"次执行");
}
}
public static void main(String[] args) {
//创建Runnable接口的实现类对象
TestRunnable testRunnable = new TestRunnable();
//创建线程对象,通过线程对象来开启我们的线程,代理
Thread thread = new Thread(testRunnable);
//调用start方法,启动线程
thread.start();//子线程开启,和主线程交替执行
//main线程执行for循换
for (int i = 0; i < 100; i++) {
System.out.println("main线程第"+i+"次执行");
}
}
}
执行结果testThread和main两个线程会交替执行100次,和上一段程序一样。
3.实现Callable接口
步骤
(1)实现Callable接口,需要返回值类型
(2)重写call方法,需要抛出异常
(3)创建目标对象
(4)创建执行服务
(5)提交执行
(6)获取结果
(7)关闭服务
public class TestCallable implements Callable<Boolean> {
//重写call方法
@Override
public Boolean call() {
for (int i = 0; i < 100; i++) {
System.out.println("testCallable线程执行了"+i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable testCallable = new TestCallable();
//创建执行服务
ExecutorService service = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> future = service.submit(testCallable);
//获取结果
Boolean result = future.get();
//关闭服务
service.shutdownNow();
}
}
三、线程状态
1.线程的五种状态
(1)新建状态(New): 线程对象被创建后,就进入了新建状态。执行完
(2)就绪状态(Runnable):调用线程对象的start()方法后,该线程进入就绪状态。此时线程进入等待队列,随时可能被CPU调度执行。
(3)运行状态(Running): run方法被执行时,线程获取CPU权限进行执行,进入了运行状态。
(4)阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。 当程序运行时候,遇到了 sleep、wait、join等方法或获取同步锁失败时会进入阻塞状态。
(5)死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2.线程的状态关系图:
3.线程的停止
- 当一个线程的run()方法执行完毕后,线程会自然终止。
- JDK提供stop()、destory()方法(不推荐使用)。
- 建议定义一个布尔类型的变量flag作为标志位,当flag=false时,终止线程。
public class TestStop implements Runnable{
//1.设置一个标志位
private boolean flag = true;
@Override
public void run() {
while (flag){
System.out.println("run...Thread");
}
}
//2.设置一个公开的方法停止线程,转移标志位
public void stop(){
this.flag = false;
}
}
4.线程的阻塞与等待
(1)线程的休眠
- 调用Thread类中的sleep()方法可实现线程的休眠。
- sleep方法的重载:sleep(long millis) 中可传入休眠的时间,单位为毫秒。即让当前线程休眠millis毫秒。sleep(long millis, int nanos)方法,让当前线程休眠millis毫秒+nanos纳秒。
- sleep时间达到前线程进入阻塞状态(Blocked)。
- sleep时间达到后线程进入就绪状态(Runnable)。
- sleep可以模拟网络延时,倒计时等。
- 线程休眠时,不会释放锁。
(2)线程的礼让
- 调用Thread类中的yield()方法即可实现线程的礼让。
- 礼让线程,让当前正在执行的线程暂停,但不进入阻塞状态(Blocked)。在执行yield方法后, 依然还是就绪状态(Runnable)。
- 礼让线程实际上是放弃本次执行的机会,让CPU重新调度。当然,CPU的下次调度可能是执行了其他线程,也可能有调度了此线程。下一次调度执行哪个线程是随机的。
(3)线程的合并
- 调用Thread类中的join()方法实现线程的合并。
- 合并线程,将多条线程合并为一条。就是先执行调用join()方法的线程,待此线程执行完毕后,再执行其他线程。
- 在此线程先执行时,其他线程进入阻塞状态(Blocked)。
5.线程状态的观测
Thread类中的getState()方法会观测线程的状态并提供一个返回值来获取线程的状态。
四、线程同步
1.概述
(1)什么是并发?
并发:同一个对象同时被多个线程操作。
(2)为什么要进行线程同步?
在日常生活中经常会遇见并发现象。同一个对象同时被很多线程操作修改,例如:抢票软件在进行抢票时,成千上万个用户同时进行买票操作。如果这个时候不进行线程同步处理,就会导致对对象的操作极其混乱。
生活中这种情况最好的解决办法是让大家排好队一个一个进行操作,同样的在我们的线程中也可以引入线程同步 . 线程同步其实就是一种等待机制 ,多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用。
2.同步方法与同步块
- synchronized关键字即加锁。当一个线程操作对象时,它会拿到锁,之后如果有其他的线程要来操作该对象时,会进入阻塞状态(Blocked)。直到上一个线程操作完该对象并释放锁后,后续的线程才可以拿到锁并对该对象进行操作。
- 举例:在买票时,一个人先来买了票,在买票过程中其他人无法对票进行操作,必须进入排队状态。等待前一个人买完票后,后面的人才能开始买票。这种操作可以避免两个人买到重复的票。
- synchronized关键字包括两种用法:synchronized方法和synchronized块。
(1)synchronized方法
在方法中加上synchronized修饰符即可实现同步方法。
弊端:方法里面需要修改的内容才需要锁,此方法锁的太多 , 浪费资源。
private synchronized void buy() throws InterruptedException {
/*
方法体
*/
}
(2)synchronized块
synchronized (obj){
/*
方法体
*/
}
其中obj是需要加锁的对象。要对哪个对象进行修改操作,对哪个对象加锁即可。
3.线程的死锁
(1)死锁的概念
例如:A线程占有test1资源,B线程占有test2资源。如果此时,A线程必须要调用B占有的test2资源才能继续执行,那么A就需要进入阻塞状态,等待B线程结束后释放test2的锁才能继续执行。假如恰巧B线程此时也需要A线程占有的test1资源,那么A、B两个线程就会同时进入阻塞状态,都需要等待对方执行结束,程序就出现了卡死的现象。这种情况就成为线程的死锁。
(2)死锁的避免方法
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。
五、小结
1.run()方法和start()方法的区别:
- 调用start()方法开启线程; 调用run方法,属于普通的方法调用,不会开启新线程。
- 调用start()方法多条执行路径,主线程和子线程交替执行;调用run()方法只有主线程一条执行路径。
2.yield与sleep区别:
- sleep()是先进入阻塞状态,结束后在进入就绪状态。yield()则是直接进入就绪状态。
- yield()会让优先级同级或优先级更高的有更高的执行机会。