【Java学习总结】多线程

一、线程简介

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()会让优先级同级或优先级更高的有更高的执行机会。

猜你喜欢

转载自blog.csdn.net/qq_43647936/article/details/120802262