Java 多线程

一、概念

在 Java 中,多线程是指在一个程序中同时运行多个执行线程,例如,在一个服务器程序中,可以使用多线程同时处理多个客户端的请求。


二、实现方式

多线程的实现方式主要有两种。

  • 继承 Thread
  • 实现 Runnable 接口

2.1 Thread 类

public class MyThread extends Thread {

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }


    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread("线程一");
        MyThread t2 = new MyThread("线程二");

        // 开启线程一
        t1.start();
        // 开启线程二
        t2.start();
    }
}

2.2 Runnable 接口

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "线程一");
        Thread t2 = new Thread(new MyRunnable(), "线程二");

        // 开启线程一
        t1.start();
        // 开启线程二
        t2.start();
    }
}

三、常用方法

3.1 基本方法

方法名含义
public final String getName()获取当前线程的名称
void setName(String name)设置线程的名称
static Thread currentThread()获取当前线程的对象
static native void sleep(long millis)让当前线程休眠,时间单位是毫秒
public class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            // 获取线程名称
            System.out.println(getName() + ":" + i);

            // 获取线程名称
            // System.out.println(Thread.currentThread().getName() + ":" + i);

            // 休眠一秒在打印
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        // 设置线程名字
        t1.setName("线程一");
        t2.setName("线程二");

        // 开启线程一
        t1.start();
        // 开启线程二
        t2.start();
    }
}

运行结果:

线程二:1
线程一:1
线程二:2
线程一:2
线程二:3
线程一:3
线程二:4
线程一:4
线程二:5
线程一:5

3.2 线程优先级

在 Java 中,线程的抢夺方式是 抢占式调度 的,这就意味着线程执行的频率是随机,那如果想要某一线程的代码执行频率相较于其他线程要高的话,就可以设置线程的优先级,另外,每个线程如果不设置优先级的话,默认等级是5,最高是10,最低是1。

方法名含义
public final void setPriority(int newPriority)设置线程的优先级
public final int getPriority()获取当前线程的优先级
public class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            // 获取线程名称
            System.out.println(getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        System.out.println("===== 默认线程的优先级 ======");
        // 获取线程的优先级
        System.out.println(t1.getName() + ":" + t1.getPriority());
        System.out.println(t2.getName() + ":" + t2.getPriority());


        // 设置线程名字
        t1.setName("线程一");
        t2.setName("线程二");

        // 设置线程的优先级
        t1.setPriority(1);
        t2.setPriority(10);

        // 开启线程一
        t1.start();
        // 开启线程二
        t2.start();
    }
}

运行结果:

===== 默认线程的优先级 ======
Thread-05
Thread-15
线程二:1
线程二:2
线程一:1
线程二:3
线程一:2
线程二:4
线程一:3
线程二:5
线程一:4
线程一:5

注:你会发现 线程二线程一 先执行完的几率要高一些,因为线程二的的优先级较高。

3.3 守护线程

守护线程的应用场景是当其他线程执行完毕后,有的线程就没有必要存在了,这时候就是将这些进程设置为守护线程,简单理解,守护线程,守护的就是其他线程,当其他线程都执行完,那么自己就没必要存在了。

方法名含义
public final void setDaemon(boolean on)设置线程为守护线程

创建线程类,在后面设置为守护线程

// 守护线程
public class DaemonThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

创建普通线程类,并测试守护线程

public class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            // 获取线程名称
            System.out.println(getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        // 创建普通线程
        MyThread Thread1 = new MyThread();
        // 创建普通线程
        DaemonThread daemonThread = new DaemonThread();

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


        // 设置线程名字
        Thread1.setName("线程");
        daemonThread.setName("守护线程");

        // 开启线程一
        Thread1.start();
        // 开启线程二
        daemonThread.start();
    }
}

运行结果:

守护线程:1
线程:3
守护线程:2
线程:4
守护线程:3
守护线程:4
线程:5
守护线程:5
守护线程:6
守护线程:7
守护线程:8
守护线程:9
守护线程:10
守护线程:11
守护线程:12
守护线程:13
守护线程:14
守护线程:15
守护线程:16
守护线程:17

注:守护线程不是立刻就会停止,而是逐渐停止。

3.4 礼让线程

看前面的多线程代码中的运行结果,会发现 线程一线程二 是交替执行的,也有 线程 一次执行多次代码的情况。而礼让线程就是让 线程 执行完后,重新去抢夺CPU执行权,可以让线程之间的执行频率对齐,但这不是一定的,有可能会出现当前 线程 再次抢到了执行权的情况。

方法名含义
public static native void yield()设置线程为礼让线程

创建礼让线程类

public class YieldThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            // 获取线程名称
            System.out.println(getName() + ":" + i);

            // 设置为礼让线程
            Thread.yield();
        }
    }
}

创建普通线程类,并测试礼让线程

public class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            // 获取线程名称
            System.out.println(getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        YieldThread t2 = new YieldThread();

        // 设置线程名字
        t1.setName("线程一");
        t2.setName("礼让线程");

        // 开启线程一
        t1.start();
        // 开启线程二
        t2.start();
    }
}

运行结果:

线程一:1
礼让线程:1
线程一:2
礼让线程:2
线程一:3
线程一:4
线程一:5
礼让线程:3
礼让线程:4
礼让线程:5

3.5 插队线程

插队线程的作用就是,让插入线程先于其他线程先执行完代码。

public class JoinThread extends Thread{

    @Override
    public void run() {
        try {
            // 让当前线程休眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(getName() + "线程执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {

        // 创建对象
        JoinThread t1 = new JoinThread();

        // 设置线程名称
        t1.setName("线程一");

        // 开启线程一
        t1.start();

        // 设置t1线程为插队线程,会优先于其他线程先执行完毕
        t1.join();

        System.out.println("当线程一执行完毕后,Main线程才会去执行");
    }
}

运行结果:

线程一线程执行完毕
当线程一执行完毕后,Main线程才会去执行

四、线程安全问题

在多线程编程中,需要注意线程安全问题。当多个线程同时访问和修改共享资源时,可能会导致数据不一致等问题。可以使用同步机制,如 synchronized 关键字、 等来解决线程安全问题。

需求:某电影院目前正在上映电影,电影票共有50张,分别在3个窗口售卖,请你用程序模拟这个场景。

public class MyThread extends Thread {
    // 电影票
    static int ticket = 0;

    // 具体逻辑
    @Override
    public void run() {
        while (true) {
            // 票没卖完
            if (ticket < 50) {
                ticket++;
                System.out.println(getName() + "在售卖第" + ticket + "张票");
            } else {
                // 票卖完了
                break;
            }

        }
    }

    public static void main(String[] args) {
        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        // 设置线程名
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

窗口一在售卖第1张票
窗口二在售卖第2张票
窗口三在售卖第3张票
窗口二在售卖第5张票
窗口一在售卖第4张票
窗口二在售卖第7张票
窗口三在售卖第6张票
窗口二在售卖第9张票
窗口一在售卖第8张票
窗口二在售卖第11张票
窗口三在售卖第10张票
窗口二在售卖第13张票
窗口一在售卖第12张票
窗口二在售卖第15张票
窗口三在售卖第14张票
窗口二在售卖第17张票
窗口一在售卖第16张票
窗口二在售卖第19张票
窗口三在售卖第18张票
窗口二在售卖第21张票
窗口一在售卖第20张票
窗口二在售卖第23张票
窗口三在售卖第22张票
窗口二在售卖第25张票
窗口一在售卖第24张票
窗口二在售卖第27张票
窗口三在售卖第26张票
窗口一在售卖第28张票
窗口二在售卖第29张票
窗口三在售卖第30张票
窗口二在售卖第32张票
窗口一在售卖第31张票
窗口二在售卖第34张票
窗口三在售卖第33张票
窗口二在售卖第36张票
窗口一在售卖第35张票
窗口二在售卖第38张票
窗口一在售卖第39张票
窗口三在售卖第37张票
窗口一在售卖第41张票
窗口二在售卖第40张票
窗口一在售卖第43张票
窗口三在售卖第42张票
窗口一在售卖第45张票
窗口二在售卖第44张票
窗口一在售卖第47张票
窗口三在售卖第46张票
窗口一在售卖第49张票
窗口二在售卖第48张票
窗口三在售卖第50张票

注意点:

  • 你会发现电影票会出现票的顺序不一致、少票、重复票、多票的情况,这些都是因为线程在执行的时候是随机的,打比方,如上述代码,窗口一 在执行完电影票++操作的时候,还没打印,窗口二窗口三就抢到了执行权,分别对车票进行++操作,然后所有窗口一起打印现在是多少张票,这就会造成重复票的安全问题,其他也是一样的逻辑。

4.1 同步代码块

上述问题,主要就是 窗口一 线程在执行方式时,窗口二 线程和 窗口三 线程也进来了,那么有没有一种方法可以让 窗口一 线程在执行方式时,其他线程不能进来,同步代码块就是来解决这个问题的,被同步代码块锁起来的代码,只用里面的代码全部执行完毕,锁才会释放,其他线程才能进来。

public class MyThread extends Thread {
    // 电影票
    static int ticket = 0;

    // 具体逻辑
    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                // 票没卖完
                if (ticket < 50) {
                    ticket++;
                    System.out.println(getName() + "在售卖第" + ticket + "张票");
                } else {
                    // 票卖完了
                    break;
                }
            }

        }
    }

    public static void main(String[] args) {
        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        // 设置线程名
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

窗口一在售卖第1张票
窗口一在售卖第2张票
窗口一在售卖第3张票
窗口一在售卖第4张票
窗口一在售卖第5张票
窗口一在售卖第6张票
窗口一在售卖第7张票
窗口一在售卖第8张票
窗口一在售卖第9张票
窗口一在售卖第10张票
窗口一在售卖第11张票
窗口一在售卖第12张票
窗口一在售卖第13张票
窗口一在售卖第14张票
窗口一在售卖第15张票
窗口一在售卖第16张票
窗口一在售卖第17张票
窗口一在售卖第18张票
窗口一在售卖第19张票
窗口一在售卖第20张票
窗口一在售卖第21张票
窗口一在售卖第22张票
窗口一在售卖第23张票
窗口一在售卖第24张票
窗口一在售卖第25张票
窗口一在售卖第26张票
窗口一在售卖第27张票
窗口一在售卖第28张票
窗口一在售卖第29张票
窗口一在售卖第30张票
窗口一在售卖第31张票
窗口一在售卖第32张票
窗口一在售卖第33张票
窗口一在售卖第34张票
窗口一在售卖第35张票
窗口一在售卖第36张票
窗口一在售卖第37张票
窗口一在售卖第38张票
窗口一在售卖第39张票
窗口一在售卖第40张票
窗口一在售卖第41张票
窗口一在售卖第42张票
窗口一在售卖第43张票
窗口一在售卖第44张票
窗口一在售卖第45张票
窗口一在售卖第46张票
窗口一在售卖第47张票
窗口一在售卖第48张票
窗口一在售卖第49张票
窗口一在售卖第50张票

注意点:

  • 第一synchronized() 它小括号传入的是锁对象,且必须是唯一的,不然还是会发生安全问题,synchronized() 就没意义了。

    • 可以传入this,例如 synchronized(this)
    • 可以创建自定义对象,专门表示唯一的锁对象,例如:Object lock = new Object();,然后 synchronized(lock)
    • 可以类的字节码对象,synchronized(类名.class)
  • 第二:注意 synchronized() 书写的位置,如果放在 while 循环的上面,那性质就变了。

4.2 同步方法

同步方法是用来保证方法内代码的安全问题的,锁对象不需要我们指定,如果方法是静态的它是用当前类的 字节码对象 当锁对象,如果是非静态的使用 this 来当锁对象的。

public class MyThread extends Thread {
    // 电影票
    static int ticket = 0;

    // 具体逻辑
    @Override
    public void run() {
        while (true) {
            if (extracted()) break;
        }
    }

    // 同步方法
    private static synchronized boolean extracted() {
        // 票没卖完
        if (ticket < 50) {
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在售卖第" + ticket + "张票");
        } else {
            // 票卖完了
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        // 设置线程名
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

窗口一在售卖第1张票
窗口一在售卖第2张票
窗口一在售卖第3张票
窗口一在售卖第4张票
窗口一在售卖第5张票
窗口一在售卖第6张票
窗口一在售卖第7张票
窗口一在售卖第8张票
窗口一在售卖第9张票
窗口一在售卖第10张票
窗口一在售卖第11张票
窗口一在售卖第12张票
窗口一在售卖第13张票
窗口一在售卖第14张票
窗口一在售卖第15张票
窗口一在售卖第16张票
窗口一在售卖第17张票
窗口一在售卖第18张票
窗口一在售卖第19张票
窗口一在售卖第20张票
窗口一在售卖第21张票
窗口一在售卖第22张票
窗口一在售卖第23张票
窗口一在售卖第24张票
窗口一在售卖第25张票
窗口一在售卖第26张票
窗口一在售卖第27张票
窗口一在售卖第28张票
窗口一在售卖第29张票
窗口一在售卖第30张票
窗口一在售卖第31张票
窗口一在售卖第32张票
窗口一在售卖第33张票
窗口一在售卖第34张票
窗口一在售卖第35张票
窗口一在售卖第36张票
窗口一在售卖第37张票
窗口一在售卖第38张票
窗口一在售卖第39张票
窗口一在售卖第40张票
窗口一在售卖第41张票
窗口一在售卖第42张票
窗口一在售卖第43张票
窗口一在售卖第44张票
窗口一在售卖第45张票
窗口一在售卖第46张票
窗口一在售卖第47张票
窗口一在售卖第48张票
窗口一在售卖第49张票
窗口一在售卖第50张票

4.3 lock()

方法名含义
void lock()上锁
void unlock()解锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread {
    // 电影票
    static int ticket = 0;

    // 创建锁对象
    static Lock lock = new ReentrantLock();

    // 具体逻辑
    @Override
    public void run() {
        while (true) {
            // 上锁
            lock.lock();
            try {
                // 票卖完了
                if (ticket >= 50) {
                    break;
                } else {
                    ticket++;
                    System.out.println(getName() + "在卖第" + ticket + "张票!!!");
                }
            } finally {
                // 解锁
                lock.unlock();
            }
            
        }
    }

    public static void main(String[] args) {
        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        // 设置线程名
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

窗口二在卖第1张票!!!
窗口二在卖第2张票!!!
窗口二在卖第3张票!!!
窗口二在卖第4张票!!!
窗口二在卖第5张票!!!
窗口二在卖第6张票!!!
窗口二在卖第7张票!!!
窗口二在卖第8张票!!!
窗口二在卖第9张票!!!
窗口二在卖第10张票!!!
窗口二在卖第11张票!!!
窗口一在卖第12张票!!!
窗口一在卖第13张票!!!
窗口一在卖第14张票!!!
窗口一在卖第15张票!!!
窗口一在卖第16张票!!!
窗口一在卖第17张票!!!
窗口一在卖第18张票!!!
窗口三在卖第19张票!!!
窗口三在卖第20张票!!!
窗口三在卖第21张票!!!
窗口三在卖第22张票!!!
窗口三在卖第23张票!!!
窗口三在卖第24张票!!!
窗口三在卖第25张票!!!
窗口三在卖第26张票!!!
窗口二在卖第27张票!!!
窗口二在卖第28张票!!!
窗口二在卖第29张票!!!
窗口二在卖第30张票!!!
窗口二在卖第31张票!!!
窗口二在卖第32张票!!!
窗口二在卖第33张票!!!
窗口二在卖第34张票!!!
窗口二在卖第35张票!!!
窗口一在卖第36张票!!!
窗口一在卖第37张票!!!
窗口一在卖第38张票!!!
窗口一在卖第39张票!!!
窗口一在卖第40张票!!!
窗口一在卖第41张票!!!
窗口一在卖第42张票!!!
窗口一在卖第43张票!!!
窗口一在卖第44张票!!!
窗口一在卖第45张票!!!
窗口一在卖第46张票!!!
窗口三在卖第47张票!!!
窗口三在卖第48张票!!!
窗口三在卖第49张票!!!
窗口三在卖第50张票!!!

注意点:

-Lock 它不像 synchronized ,它可以手动的上锁和解锁,比 synchronized 有更好的操作性;

  • Lock 是一个接口,可以通过 ReentrantLock 来创建对象调用方法。

五、线程生命周期

在这里插入图片描述


  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野生派蒙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值