java多线程

本文介绍了Java中线程的两种创建方式:继承Thread类和实现Runnable接口,并对比了两者的优缺点。接着讨论了线程的生命周期、线程安全问题,特别是通过synchronized关键字实现的同步机制,以及同步代码块和同步方法的使用。此外,文章还提到了线程的优先级、线程的死锁问题和Lock接口的使用,特别是ReentrantLock的公平锁特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 线程的创建和使用

文档参考【尚硅谷_java基础】九、多线程菜鸟教程,视频参考:尚硅谷Java入门视频教程,宋红康java基础视频

1.1 创建线程的方式

JDK1.5之前创建新执行线程有两种方法:

  • 继承Thread类的方式
  • 实现Runnable接口的方式

1.1.1 方式一:继承Thread类的方式

  1. 创建一个继承于Thread的子类
  2. 重写Thread类run()–>将此线程执行的操作声明在run()中
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start()必须通过调用star()方法来启动线程,如果直接调用run()方法,不会创建一个新的线程,就只是普通的方法调用)

示例:创建MyThread线程遍历1-100的偶数,main函数遍历1-100
注:其中Thread.currentThread().getName()为获取当前线程的名字

//1.创建一个继承于Thread的子类
class MyThread extends Thread{
    //2.重写Thread类run()
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
public class demo1 {
    public static void main(String[] args) {
        //3.创建Thread类的子类的对象
        MyThread t1=new MyThread();

        //4.通过此对象调用start():a.启动当前线程 b.调用当前线程的run()
        t1.start();

        MyThread t2 = new MyThread();
        t2.start();

        for (int i = 0; i < 100 ;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }

}

输出为:
在这里插入图片描述
从上面的代码可以看出继承Thread类创建线程的缺陷,由于java单继承的特点,继承了Thread类就无法继承其他的类了,因此还有一种实现Runnable接口来创建线程的方式

1.1.2 方式二:实现Runnable接口

  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法:run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()

可以使用同一个Runnable实现类的对象来创建多个相同的线程

Thread 类的构造器:

  • Thread():创建新的Thread对象
  • Thread(String threadname):创建线程并指定线程实例名
  • Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
  • Thread(Runnable target, String name):创建新的Thread对象
//1.创建一个实现了Runnable接口的类
class MyThread implements Runnable{
    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
public class demo1 {
    public static void main(String[] args) {
        //3.创建实现类的对象
        MyThread myThread=new MyThread();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);
        //4.通过此对象调用start():a.启动当前线程 b.调用当前线程的run()
        thread1.start();
        thread2.start();
        
        for (int i = 0; i < 100 ;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

输出为:
在这里插入图片描述

1.1.3 继承方式和实现方式的联系与区别

区别

  • 继承Thread:线程代码存放在Thread子类run方法中
  • 实现Runnable:线程代码存放在接口的子类的run方法。

实现方式的好处

  • 避免了单继承的局限性
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

1.2 Thread类的常用方法

  • start():启动当前线程,执行当前线程的run()
  • run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  • currentThread(): 静态方法,返回当前代码执行的线程
  • getName():获取当前线程的名字
  • setName():设置当前线程的名字
  • yield():释放当前CPU的执行权
  • join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  • stop():已过时。当执行此方法时,强制结束当前线程。
  • sleep(long millitime):让当前线程“睡眠”指定时间的millitime毫秒)。在指定的millitime毫秒时间内,当前线程是阻塞状态的。
  • isAlive():返回boolean,判断线程是否还活着

注意:

  • 当一个线程yield()释放当前CPU的执行权后,所有线程同时去拿执行权,可能他又能抢到了执行权,导致结果看起来就像他没有yield()一样
  • stop()方法已经过时了,不建议使用,参考文章:为什么不要用stop方法停止线程

1.3 线程的优先级

  • 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序
  • Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) ~ 10(Thread.MAX_PRIORITY )
  • 默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)
  • 具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
  • 涉及的方法
    getPriority() :返回线程优先值
    setPriority(int newPriority):改变线程的优先级
  • 说明
    线程创建时继承父线程的优先级
    低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
    `

1.4 线程的分类

Java中的线程分为两类:

  • 守护线程:
    • 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程
    • Java垃圾回收就是一个典型的守护线程。 若JVM中都是守护线程,当前JVM将退出
  • 用户线程
    • 普通创建的线程

它们几乎在每个方面都是相同的,唯一的区别是判断JVM何时离开。
若JVM中都是守护线程,当前JVM将退出。

实例:

class MyThread implements Runnable{
    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }

    }
}
public class demo1 {
    public static void main(String[] args) {
        //3.创建实现类的对象
        MyThread myThread=new MyThread();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread thread1 = new Thread(myThread);

        //将thread1设置为守护线程
        thread1.setDaemon(true);

        //4.通过此对象调用start():a.启动当前线程 b.调用当前线程的run()
        thread1.start();

        System.out.println("main要结束啦......");

    }
}

输出结果:

  • 第一次输出
    在这里插入图片描述

  • 第二次输出
    在这里插入图片描述

  • 第三次输出
    在这里插入图片描述

结果分析:

  1. thread1设置为守护线程,必须在star()之前设置,守护线程和用户线程一样,但是它是为用户线程服务的,所以当所有用户的用户线程结束他也会结束
  2. 此程序中只有main一个线程,thread1是守护线程,所以当main结束时thread1也会结束,因此每次main能运行多久thread1就运行多久,所以thread1每次输出的次数都不一样,有时候甚至thread1来不及输出main就结束了,看起来就像thread1没启动一样

加上下面这段for循环,延迟main的结束时间,给thread1足够的时间进行输出,thread1就能全部输出

  for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName()+i);
        }

在这里插入图片描述

JVM 中的垃圾回收线程就是典型的守护线程,如果 JVM 中没有一个正在运行的非守护线程,这个时候,JVM 会退出,程序结束。换句话说,守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

参考文章:面试官: 谈谈什么是守护线程以及作用 ?

2. 线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期。
在这里插入图片描述
在这里插入图片描述

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start() 这个线程。

  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

3. 线程的同步

3.1 提出问题

示例:

模拟火车站售票程序,开启三个窗口售票模拟火车站售票程序,开启三个窗口售票

package JUC.RunnableTest;

class Ticket implements Runnable{
    private int tick=10;
    @Override
    public  void run(){
        while (true){
            if(tick>0){
                System.out.println(Thread.currentThread().getName()+"售出车票,tick号为:"+tick--);
            }else break;
        }
    }
}

public class demo2 {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1");
        t2.setName("t2");
        t3.setName("t3");
        t1.start();
        t2.start();
        t3.start();
    }
}

多次运行,某一次结果输出为:
在这里插入图片描述
问题:

在Ticket类的代码中if(tick>0)才会输出,但是我们发现最后一次程序输出了tick号为:0,这是为什么呢?

问题的原因:

多线程出现了安全问题,当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

在这个程序中3个线程共享tick变量,当tick=1时,线程t1进行if(tick>0)判断,此时判断通过,应该输出tick号为:1,同时t-1,但是在他输出前,线程t2也运行到了这,抢先输出tick号为:1,同时t-1,因此t1输出的就会是0
在这里插入图片描述

解决办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

3.2 Synchronized的使用方法–同步代码块和同步方法

Java对于多线程的安全问题提供了专业的解决方式:同步机制

方式一:同步代码块

synchronized(同步监视器){
      //需要被同步的代码
 }

说明:

  1. 操作共享数据的代码,即为需要被同步的代码 —>不能包含代码多了,也不能包含代码少了。
  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以来充当锁。

要求:

  • 多个线程必须要共用同一把锁。

补充:

  • 在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器。
  • 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监听器

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的

public synchronized void show (String name){.
}

好处:

  • 同步的方式,解决了线程的安全问题。

缺点:

  • 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

3.2.1 同步代码块处理实现Runnable接口的线程安全问题

class Windows implements Runnable{
    private int ticket=100;
    Object obj=new Object();
    @Override
    public void run() {
        while(true){

            synchronized (obj) {//synchronized(this),this指代Windows这个类
                if (ticket > 0) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class SRunnable {
    public static void main(String[] args) {
        Windows w = new Windows();
        Thread t1=new Thread(w);
        Thread t2=new Thread(w);
        Thread t3=new Thread(w);

        t1.start();
        t2.start();
        t3.start();
    }
}

3.2.2 同步方法处理实现Runnable的线程安全问题

class Windows implements Runnable{
    private int ticket=100;
    Object obj=new Object();
    @Override
    public void run() {
        while(true){
            show();
        }
    }
    private synchronized void show(){//同步监视器就是this,this指代Windows这个类
        if (ticket > 0) {
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class SRunnable {
    public static void main(String[] args) {
        Windows w = new Windows();
        Thread t1=new Thread(w);
        Thread t2=new Thread(w);
        Thread t3=new Thread(w);

        t1.start();
        t2.start();
        t3.start();
    }
}

3.2.3 同步代码块处理继承Thread类的线程安全问题

class ThreadTest extends Thread{

    private static int ticket=100;
    private static Object obj=new Object();

    @Override
    public void run() {
        while(true){
            synchronized(obj) {
            //synchronized(duoxiancheng.ThreadTest.class){
            //synchronized(this){//错误,因为此时this表示的是t1,t2,t3三个对象
                if (ticket > 0) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class SThread {
    public static void main(String[] args) {
        ThreadTest t1 = new ThreadTest();
        ThreadTest t2 = new ThreadTest();
        ThreadTest t3 = new ThreadTest();

        t1.start();
        t2.start();
        t3.start();
    }
}

3.2.4 同步方法处理继承Thread类的线程安全问题

class ThreadTest extends Thread{

    private static int ticket=100;

    @Override
    public void run() {
        while(true){
            show();
        }
    }
    private static synchronized void show(){//同步监视器:this
    //private synchronized void show(){//同步监视器:t1,t2,t3
        if (ticket > 0) {
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }

    }
}

public class SThread {
    public static void main(String[] args) {
        ThreadTest t1 = new ThreadTest();
        ThreadTest t2 = new ThreadTest();
        ThreadTest t3 = new ThreadTest();

        t1.start();
        t2.start();
        t3.start();
    }
}

关于同步方法的总结:

  • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身

3.3 线程安全的单例模式之懒汉式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

1、懒汉式,线程不安全

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

2、懒汉式,线程安全

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

详细参考:
菜鸟教程-单例模式

3.4 同步机制中的锁

synchronized的锁是什么?

  • 1.任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
  • 2.同步方法的锁:静态方法(类名.class)、非静态方法(this)
  • 3.同步代码块:自己指定,很多时候也是指定为this或类名.class

注意

  • 1.必须确保使用同一个资源的多个线程共用一把锁,否则就无法保证共享资源的安全。
  • 2.一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块指定需谨慎

3.5 同步的范围

如何找问题,即代码是否存在线程安全?(非常重要)

  • (1)明确哪些代码是多线程运行的代码
  • (2)明确多个线程是否有共享数据
  • (3)明确多线程运行代码中是否有多条语句操作共享数据

如何解决?(非常重要)

  • 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的这些语句都要放在同步范围中

切记:

  • 范围太小:没锁住所有有安全问题的代码
  • 范围太大:没发挥多线程的功能。

3.6 释放和不会释放锁的操作

释放锁的操作

  • 1.当前线程的同步方法、同步代码块执行结束。
  • 2.当前线程在同步代码块、同步方法中遇到breakreturn终止了该代码块、该方法的继续执行。
  • 3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 4.当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作

  • 1.线程执行同步代码块或同步方法时,程序调Thread.sleep()Thread.yield()方法暂停当前线程的执行
  • 2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()resume()来控制线程

3.7 线程的死锁问题

死锁

  • 1.不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 2.出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

解决方法

  • 1.专门的算法、原则
  • 2.尽量减少同步资源的定义
  • 3.尽量避免嵌套同步

死锁示例:

package JUC.lock;

public class DeadlockTest {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized(s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized(s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).run();
    }
}

3.8 Lock(锁)

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了Lock ,它拥有与 synchronized
    相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
    在这里插入图片描述
    代码示例:
class window implements Runnable{
    private int t=100;
    
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    
    @Override
    public void run() {
    
       while(true){
       
            //调用锁定方法:lock()
            lock.lock();
            
            try{
                if(tick > 0){

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "售出车票,tick号为:" + tick--);
                }else{
                    break;
                }
            }finally {
            
                //3.调用解锁方法:unlock()
                lock.unlock();
                
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        window w = new window();

        Thread t1=new Thread(w);
        Thread t2=new Thread(w);
        Thread t3=new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

3.9 synchronized 与 Lock 的对比

  • 1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  • 2.Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)

3.10 ReentrantLock实现公平锁

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

ReentrantLock lock = new ReentrantLock(true);

详细参考:ReentrantLock(重入锁)功能详解和应用演示

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值