多线程简单介绍

一、认识线程

⼀个线程就是⼀个"执行流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行着多份代码,(线程是轻量级进程)

线程存在的意义

单核CPU的发展遇到了瓶颈.要想提⾼算⼒,就需要多核CPU.而并发编程能更充分利用多核CPU 资源

有些任务场景需要"等待IO",为了让等待IO的时间能够去做⼀些其他的工作,也需要用到并发编程

其次,虽然多进程也能实现并发编程,但是线程比进程更轻量.

创建线程比创建进程更快.

销毁线程比销毁进程更快.

调度线程比调度进程更快.

进程和线程的区别

1.进程是包含线程的,每个进程至少有一个线程,即主线程

2.进程与进程之间不共享内存空间,同一个进程的线程之间共享一个内存空间

3.进程是系统分配资源的最小单位,线程是系统调度的最小单位

4.一个进程挂了一般不影响其他进程,但是一个线程挂了可能会把同进程内的其他线程全部带走

二、多线程

线程创建

继承Thread类

java 复制代码
package com.devilta.thread;

//继承Thread
class MyThread extends Thread {
    //重写run方法
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demon1 {
    public static void main(String[] args) throws InterruptedException {
        //调用MyThread
        Thread t = new MyThread();
        t.start();
        //main也调用
        while (true) {
            System.out.println("Hello mian");
            Thread.sleep(1000);
        }
    }
}

或者

java 复制代码
public class Demon1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("Hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        t.start();
        while(true){
            System.out.println("Hello main");
            Thread.sleep(1000);
        }
    }
}

或者

java 复制代码
public class Deon3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           while (true) {
               System.out.println("Hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        t.start();
        while (true) {
            System.out.println("Hello mian");
            Thread.sleep(1000);
        }
    }
}

实现Runnable接口

java 复制代码
package com.devilta.thread;

class MyRunnable implements Runnable{

    @Override
    public void run() {
        while(true){
            System.out.println("Hello Runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demon2 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();

        while(true){
            System.out.println("Hello main");
            Thread.sleep(1000);
        }
    }
}

或者

java 复制代码
public class Demon2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t = new Thread(runnable);
        t.start();
        while(true){
            System.out.println("Hello main");
            Thread.sleep(1000);
        }
    }
}

Thread核心属性

如果线程不能阻止进程结束,那么这个线程就是后台线程,这里补充一个方法可以把线程设置成后台线程,setDaemon()

如果线程能左右进程的结束,或者说线程没结束,进程就不能结束,那么这个线程就是前台线程

终止一个线程

java 复制代码
public class Demon4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
                    break;
                }
            }
            System.out.println("t 结束");
        });

        t.start();
        Thread.sleep(3000);
        System.out.println("尝试终止线程t");
        t.interrupt();
    }
}

等待一个线程

让线程的执行顺序按照程序员的意愿来执行

java 复制代码
public class Demon5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           for(int i = 0; i < 3; i++){
               try {
                   System.out.println("hello thread");
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println("t线程结束");
        });

        t.start();
        t.join();
        System.out.println("mian线程结束");
    }
}

join也可以设置一个超时时间

java 复制代码
public class Demon5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           for(int i = 0; i < 3; i++){
               try {
                   System.out.println("hello thread");
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println("t线程结束");
        });

        t.start();
        t.join(3000);
        System.out.println("mian线程结束");
    }
}

三、线程状态

**• NEW:**安排了工作,还未开始行动

**• RUNNABLE:**可工作的.又可以分成正在工作中和即将开始工作

**• BLOCKED:**这几个都表示排队等着其他事情

**• WAITING:**这几个都表示排队等着其他事情

**• TIMED_WAITING:**这几个都表示排队等着其他事情

**• TERMINATED:**工作完成了.

主要理解每个状态的意义

四、线程安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的,否则是线程不安全的

举个例子:

java 复制代码
public class Demon6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0;i < 50000;i++){
                count++;
            }
            System.out.println("t1结束");
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0;i < 50000;i++){
                count++;
            }
            System.out.println("t2结束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

得到的结果并不是预期结果100000,出现这种情况的原因是count++

实际上**count++**分为三步

1.load:把内存中count的值,加载到cpu的寄存器

2.add:把寄存器中的值加一

3.save:把寄存器中的内容保存到内存上

但由于操作系统对线程的调度是随机的,抢占式执行的,因此执行上面三步骤时可能并不是一次性全部执行完毕,可能是执行到某个步骤就被调度走了

总结一下线程安全产生的原因:

1.根本原因:操作系统对线程的调度是随机的,即抢占式执行

2.多个线程修改同一个变量

3.修改操作不是原子的,所谓的原子类似于数据库事务的原子性,当修改操作只对应到一个CPU指令,就说明该操作是原子的,即不会出现指令执行到一半就被调度走了的情况,上面的例子就不是原子的操作

4.内存可见性

5.指令重排序

五、解决线程安全问题

先看代码:

java 复制代码
public class Demon6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            for(int i = 0;i < 50000;i++){
                synchronized(lock){
                    count++;
                }
            }
            System.out.println("t1结束");
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0;i < 50000;i++){
                synchronized(lock){
                    count++;
                }
            }
            System.out.println("t2结束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这里的synchronized的作用相当于给count++加了个锁

synchronized的特性

互斥

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行 到同⼀个对象synchronized就会阻塞等待.

• 进⼊synchronized修饰的代码块,相当于加锁

• 退出synchronized修饰的代码块,相当于解锁

synchronized的参数是构成互斥的关键,比如上面的代码中参数都是lock,这两个锁构成互斥,必须一个锁解锁后另一个锁才能进行加锁。如果这两个锁的参数不一样,那这样的锁就失去了意义

可重入

该特性主要解决了死锁问题

java 复制代码
class Count{
    public int count = 0;
    public void add(){
        //加锁
        synchronized (this){
            count++;
        }
    }
    public int getCount(){
        return count;
    }
}
public class Demon1 {
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        Thread t1 = new Thread(() ->{
           for(int i = 0;i < 50000;i++){
               //加锁
               synchronized(count){
                   count.add();
               }
           }
        });
        t1.start();
        t1.join();
        System.out.println(count.getCount());
    }
}

这段代码简单演示了一个死锁,首先在线程t1中,针对count.add()进行了加锁,那么其它地方想加锁就必须等待这个锁里的逻辑执行完毕后解锁,但是调用的这个方法本身又加了一层锁,这会导致代码会在这里一直阻塞等待下去,所以代码会卡在这里

但是因为java的synchronized具有可重入性,实际上代码是可以得到预期结果的

可重入锁的原理实际上是让内存把锁对象保存下来,即记录当前是哪个线程持有锁,当后续有线程针对这个锁再次加锁,判断是否是同一个线程,是的话就直接放行(实际上相当于没加锁),后续解锁的话,则通过一个计数器(初始为0)实现,每次匹配到 { 的话就加一,匹配到 } 就减一,当计数器再次为0时,就解锁

其他类型的死锁

可重入读解决的死锁是一个线程连续加两次同一把锁,死锁还有其他类型

两个线程,两把锁,线程本身已经加锁的情况下尝试获取对方的锁

先看代码:

java 复制代码
import static java.lang.Thread.sleep;

public class Demon2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() ->{
            synchronized (lock1) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2) {
                    System.out.println("t1线程两个锁都拿到");
                }
            }
        });
        Thread t2 = new Thread(() ->{
            synchronized (lock2) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock1) {
                    System.out.println("t1线程两个锁都拿到");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

在上面的代码中每次获取对方锁前面都进行了等待,实际上这是为了成功演示死锁加的,如果不加的话可能不会死锁,由于执行速度的原因,可能t2还没开始,t1就已经拿到了两个锁

n个线程,m把锁

也就是哲学家就餐问题

  • 5 个哲学家围坐在一张圆桌旁。

  • 每个哲学家面前有一盘意大利面,桌子上的每个哲学家左右各有一根叉子 (有些版本是筷子)。

    注:总共只有 5 根叉子

  • 每个哲学家的行为只有两种:思考进食

  • 要进食,哲学家需要同时获得左右两边的叉子

  • 进食完毕后,他会放下两根叉子,继续思考。

如果每个哲学家都先拿起左边的叉子,然后等待右边的叉子,就会导致所有哲学家都拿着一根叉子,永远等不到第二根叉子 → 系统永久阻塞。

避免死锁

构成死锁的四个必要条件:

1.互斥,一个线程拿到锁后,另一个线程尝试获取锁,必须要阻塞等待

2.锁是不可抢占的

3.请求和保持,一个线程拿到锁1,不释放锁1的情况下获取锁2

4.循环等待,多个线程,多把锁之间等待对方释放锁

由于前两个必要条件是锁本身的特性,所以主要针对后两个条件进行处理

对于请求和保持,尽量不要嵌套请求锁,那么上面的示例可以这么改:

java 复制代码
import static java.lang.Thread.sleep;

public class Demon2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() ->{
            synchronized (lock1) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            synchronized (lock2) {
                System.out.println("t1线程两个锁都拿到");
            }
        });
        Thread t2 = new Thread(() ->{
            synchronized (lock2) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            synchronized (lock1) {
                System.out.println("t1线程两个锁都拿到");
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

这种做法打破了请求与保持,但有些场景下又必须进行嵌套加锁,因此需要针对循环等待进行处理

约定好加锁的顺序,比如先获取序号小的锁,再获取序号大的锁:

java 复制代码
import static java.lang.Thread.sleep;

public class Demon2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() ->{
            synchronized (lock1) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2) {
                    System.out.println("t1线程两个锁都拿到");
                }
            }
        });
        Thread t2 = new Thread(() ->{
            synchronized (lock1) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2) {
                    System.out.println("t2线程两个锁都拿到");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

内存可见性问题

先看代码:

java 复制代码
import java.util.Scanner;

public class Demon3 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {

            }
            System.out.println("t1结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner sc = new Scanner(System.in);
            System.out.println("输入flag的值:");
            flag = sc.nextInt();
        });
        t1.start();
        t2.start();
    }
}

两个线程,一个读取,一个写入,但是读的线程没有读到修改后的值,这就是内存可见性问题

为什么会产生这样的问题呢,因为编译器优化的原因,JVM会在程序员代码逻辑不变的基础上,对代码细节进行调整,是代码运行效率提高,但在多线程场景下,这种优化可能会导致代码逻辑前后出现偏差

在上面的示例里,有一个while循环,它的逻辑是flag为0的时候一直死循环,实际上是先把内存上的flag值load到寄存器里,然后执行类似于compare的指令,这两个指令的开销差距其实是比较大的,读内存的开销远远大于compare

在执行过程中,while循环的速度是相当快的,可能等到用户输入的时候已经执行了很多次循环,因此编译器会进行错误的优化,即将读取内存的操作变成读取寄存器,直接在寄存器上获取flag,所以当用户修改flag的值后,线程就感知不到了

怎么处理这种情况?

volatile关键字

java 复制代码
import java.util.Scanner;

public class Demon3 {
    private volatile static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {

            }
            System.out.println("t1结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner sc = new Scanner(System.in);
            System.out.println("输入flag的值:");
            flag = sc.nextInt();
        });
        t1.start();
        t2.start();
    }
}

使用volatile可以有效解决内存可见性问题,但不能解决原子性问题

wait和notify

线程饥饿 (Thread Starvation)是指一个或多个线程无法获得所需的资源 (如 CPU 时间、锁、内存等),导致它们无法继续执行,而其他线程却一直在执行的现象。通俗的讲,由于操作系统的随即调度机制,当一个线程释放锁时,它还是就绪态, 其他线程都是阻塞等待,所以大概率还是这个线程拿到锁

这个场景就是wait和notify的典型场景

先看wait方法:

java 复制代码
public class Demon4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
        }
    }
}

代码进入wait,就会先释放锁,然后阻塞等待,当其它线程做完了必要的工作,就会调用notify唤醒wait从而解除阻塞,重新获取到锁,继续执行

然后总体使用一下:

java 复制代码
import java.util.Scanner;

public class Demon5 {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(() ->{
            try{
                System.out.println("等待之前");
                synchronized (lock){
                    lock.wait();
                }
                System.out.println("等待之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(() ->{
            Scanner sc = new Scanner(System.in);
            System.out.println("输入内容以唤醒t1");
            sc.nextInt();
            synchronized (lock){
                lock.notify();
            }
        });
        t1.start();
        t2.start();
    }
}

这里注意几个点:

wait和notify的对象必须是同一个

一定要先有wait,才能notify

当有多个线程需要唤醒时,可以使用notifyAll

java 复制代码
import java.util.Scanner;

public class Demon5 {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(() ->{
            try{
                System.out.println("t1等待之前");
                synchronized (lock){
                    lock.wait();
                }
                System.out.println("t1等待之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t3 = new Thread(() ->{
            try{
                System.out.println("t3等待之前");
                synchronized (lock){
                    lock.wait();
                }
                System.out.println("t3等待之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(() ->{
            Scanner sc = new Scanner(System.in);
            System.out.println("输入内容以唤醒t1和t3");
            sc.nextInt();
            synchronized (lock){
                lock.notifyAll();
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

当然wait也可以添加超时时间

wait和sleep的区别

1.wait必须要搭配锁使用,先加锁才能用,而sleep不需要

2.如果都在synchronized中使用,wait会释放锁,sleep不会

最后给个综合例子来看看wait和notify联动的效果:

java 复制代码
public class Demon6 {
    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Object lock3 = new Object();
        Thread t1 = new Thread(() ->{
            try{
                for(int i=0; i<10; i++){
                    synchronized (lock1){
                        lock1.wait();
                    }
                    System.out.print("A");
                    synchronized (lock2){
                        lock2.notify();
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(() ->{
            try{
                for(int i=0; i<10; i++){
                    synchronized (lock2){
                        lock2.wait();
                    }
                    System.out.print("B");
                    synchronized (lock3){
                        lock3.notify();
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException();
            }
        });
        Thread t3 = new Thread(() ->{
            try{
                for(int i=0; i<10; i++){
                    synchronized (lock3){
                        lock3.wait();
                    }
                    System.out.println("C");
                    synchronized (lock1){
                        lock1.notify();
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException();
            }
        });
        t1.start();
        t2.start();
        t3.start();

        Thread.sleep(1000);
        synchronized (lock1){
            lock1.notify();
        }
    }
}
相关推荐
小比特_蓝光3 小时前
算法篇二----二分查找
java·数据结构·算法
QJtDK1R5a3 小时前
C# 14 中的新增功能
开发语言·c#
大黄说说3 小时前
Java 中 String 为何被设计为不可变?
开发语言
田梓燊3 小时前
leetcode 56
java·算法·leetcode
复园电子3 小时前
KVM与Hyper-V虚拟化环境:彻底解决USB外设映射掉线的底层架构优化
开发语言·架构·php
scan7243 小时前
龙虾读取session历史消息
java·前端·数据库
better_liang3 小时前
每日Java面试场景题知识点之-分布式事务
java·微服务·seata·分布式事务·一致性·saga·tcc
kvo7f2JTy3 小时前
JAVA 设计模式
java·开发语言·设计模式
仍然.3 小时前
多线程---阻塞队列收尾和线程池
java·开发语言·算法