多线程(初阶一)

学习线程,我们大致要了解这样一些东西

线程概念

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
就像是去银行取钱,如果只有一个窗口张三在办业务,所有人都在等这一个窗口,这样就有点麻烦,但是,张三又叫了李四,王五两个同事又开了两个窗口来干一样的事,都是在帮银行办事,这样的流水线就叫多线程,(张三喊的李四和王五,我们一般把张三成为主线程).

进程线程区别

首先,我们要知道,为啥要有进程?因为我们的系统是支持多任务的,程序员也就需要"并发编程".
什么是并发编程?(首先我们谈谈并行和并发:并行:微观上,两个cpu核心,同时执行两个任务代码.并发:微观上,一个cpu核心,先执行一会任务一,在执行一会任务二,在任务三…在任务一…这样他只要切换的够快,从宏观上来看,他就像是在同时执行多个任务)所以,并发和并行这两件事,只在微观上区分,宏观上不区分,所以我们常常用并发来指代并行+并发.)

这里谈到进程和线程区别:
1:
进程包含线程,一个进程里可以有一个或多个线程
2:
进程和线程都是为了处理并发编程这样的场景.但进程频繁创建销毁时候效率低,相比之下,线程更轻量,效率高(因为少了申请释放资源的过程)
3:
操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位.操作系统创建线程,是要在cpu上调度执行,线程是操作系统调度执行的基本单位
4:
进程具有独立性,一个进程挂了不影响其他进程,同一个进程中的多个线程,公用一个内存空间,一个线程挂了,可能影响其他线程,甚至让整个进程崩溃.

(举个例子):(如果把进程比喻成一个工厂,现在要生产1w部手机:
两个方案:1:搞两个工厂,一人5k
2:一个工厂:搞2个生产线,在分别生产5k
可能时间差不多,但里面的成本可想而知)

java创建线程

在Java中使用 Thread 类这个类的对象来表示操作系统中的线程
五种方法:
1:继承 Thread, 重写 run

class MyThred extends Thread{
    
    
        @Override
    public void run(){
    
    
            System.out.println("hello thread");
        }
}

public class Demo1{
    
    
    public static void main(String[] args) {
    
    
        Thread thread = new MyThred();
        thread.start();
    }
}

第二种:实现 Runnable, 重写 run

class MyRunnable implements Runnable{
    
    
public void run(){
    
    
            System.out.println("hello");
        }
}

public class Demo3
public static void main(String[] args) {
    
    
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

第三种:继承 Thread, 重写 run, 使用匿名内部类

public class Demo4{
    
    
public static void main(String[] args) {
    
    
        Thread thread = new Thread(){
    
    
            public void run(){
    
    
                System.out.println("hello world");
            }
        };
        thread.start();
    }
}

第四种:实现 Runnable, 重写 run, 使用匿名内部类

public class Demo5{
    
    
public static void main(String[] args) {
    
    
        Thread thread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("hello thread");
            }
        });
        thread.start();
    }
}

第五种:使用 lambda 表达式

public class Demo6{
    
    
    public static void main(String[] args) {
    
    
        Thread thread = new Thread(() ->{
    
    
            System.out.println("hello thread");
        });
        thread.start();
    }
}

Thread的几个常见属性

属性 获取方法
ID getId()
名称 getName()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的一个情况,下面我们会进一步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进一步说明

中断线程

自己定义一个变量来

public class Demo10 {
    
    
    private static boolean isQuit = false;

    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            while (!isQuit) {
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // 只要把这个 isQuit 设为 true, 此时这个循环就退出了, 进一步的 run 就执行完了, 再进一步就是线程执行结束了.
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        isQuit = true;
        System.out.println("终止 t 线程!");
    }
}

在这里插入图片描述
但是这个做法并不严谨,更好的做法是:
使用Thread中内置的一个标志位,来进行判定,可以通过:
Thread.interrupted() 这个是静态方法
Thread.currentThread().isInterrupted() 这个是实例方法

public class Demo11 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            while (!Thread.currentThread().isInterrupted()) {
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                    // 当触发异常之后, 立即就退出循环~
                    System.out.println("这是收尾工作");
                    break;
                }
            }
        });
        t.start();

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

        // 在主线程中, 调用 interrupt 方法, 来中断这个线程.
        // t.interrupt 的意思就是让 t 线程被中断!!
        t.interrupt();
    }
}

在这里插入图片描述

线程等待

线程和线程之间,调度顺序是完全不确定的…如果想要线程的顺序可控,线程等待就是一种方法.
在这里插入图片描述

public class Demo12 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);//1秒
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // 在主线程中就可以使用一个等待操作. 来等待 t 线程执行结束.
        try {
    
    
            t.join(10000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

获取当前线程的引用

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用
public class Demo13 {
    
    
    public static void main(String[] args) {
    
    
//        Thread t = new Thread() {
    
    
//            @Override
//            public void run() {
    
    
//                // System.out.println(Thread.currentThread().getName());
//                System.out.println(this.getName());
//            }
//        };
//        t.start();

        Thread t = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                // System.out.println(this.getName());
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();

        // 这个操作是在 main 线程中调用的. 因此拿到的就是 main 这个线程的实例
        System.out.println(Thread.currentThread().getName());
    }
}

在这里插入图片描述

线程休眠

休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException 可以更高精度的休眠

这个Sleep这个方法,本质上就是把线程PCB从就绪队列,移动到阻塞队列.

注: 当线程调用 sleep / join / wait / 等待锁 … 就会把 PCB放到另一个队列(阻塞队列)

public class TestDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

在这里插入图片描述

线程状态

NEW: 安排了工作, 还未开始行动
把Thread对象创建好了,但是没有调用start~
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
就绪状态:
处于这个状态的线程,就是在就绪队列中,随时可以调度到CPU上~
如果代码没有进行sleep,也没有进行其他的可能导致阻塞的操作,代码大概率是处在Runnable状态~

BLOCKED: 这几个都表示排队等着其他事情
当前线程在等待锁,导致了阻塞(阻塞状态之一) synchronized~~
WAITING: 这几个都表示排队等着其他事情
当前线程正在等待唤醒,导致了阻塞(阻塞状态之一) wait~~
TIMED_WAITING: 这几个都表示排队等着其他事情
代码中调用了sleep,就会进入到TIMED_WAITING
join(超时)
意思就是当前的线程在一定时间=内,是阻塞状态

public class Demo14 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                // 这里啥都不能有!!!

                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

在这里插入图片描述一定时间之后,阻塞状态解除,这种情况就是TIMED_WAITING…也是属于阻塞的状态之一

TERMINATED: 工作完成了.
操作系统中的线程已经执行完毕,销毁了,但是Thread对象还在,获取到的状态

在这里插入图片描述

线程安全

看下面这段代码:

class Counter {
    
    
    // 这个 变量 就是两个线程要去自增的变量
    volatile public int count;

    public void increase() {
    
    
        count++;
    }

    synchronized public static void func() {
    
    
    }
}

public class Demo15 {
    
    
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        // 必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();

        // 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
        System.out.println(counter.count);
    }
}

在这里插入图片描述我们想得到的是10w,我运行了四次,结果每次结果都不一样
为啥会这样,下面来分析一下:
在这里插入图片描述
那怎样做到线程安全,来举个例子:
在这里插入图片描述就像这样,总不能一窝蜂的去一起上同一个厕所吧,像这样把门锁上,一个一个来,不就安全了吗!

我们给某一个线程上锁,另一个线程就等着,结束在解锁

import java.util.Scanner;

public class Demo16 {
    
    
    private static int isQuit = 0;

    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            while (isQuit == 0) {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println("循环结束! t 线程退出!");
        });
        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值: ");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕!");
    }
}

在这里插入图片描述在这里插入图片描述

简单总结一下线程不安全的原因

1:
线程都是抢占式执行,线程间的调度充满随机性.[万恶之源,无可奈何]
2:
多个线程对同一个变量进行修改操作~~
3:
针对变量的操作不是原子(计算机中说原子都是说不能再分的了)
针对这些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的
通过加锁操作,也就是把好几个指令打包成一个原子的了~
加锁操作,就是把这里的操作打包成一个原子操作
4:
内存可见性,也会影响线程安全
在这里插入图片描述5:
指令重排序,也会影响到线程安全问题~~

synchronized

特性

1:互斥: synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

2:刷新内存
synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁
    所以 synchronized 也能保证内存可见性.
    3:可重入
    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
    (啥叫重入锁,直观的讲,同一个线程针对同一把锁,连续加锁两次,如果出现了死锁,就是不可重入锁,如果不会,就是可重入锁)(死锁:简单点举个例子:我喜欢蘸酱油吃饺子,我姐喜欢蘸醋吃饺子,一天,我拿着一瓶醋想蘸酱油,我姐拿着一瓶酱油想蘸醋,我说:“你给我酱油我就给你醋”,我姐说:“你给我醋我就给你酱油”.大家都这样僵着了,谁也不让谁!)
    死锁的四个必要条件:
    1:互斥使用:一个锁被一个线程占用之后,其他线程占用不了(锁的本质,保证原子性)
    2:不可抢占:一个锁被一个线程占用以后,其他线程不能把这个锁抢走(解释一下锁竞争(所对象):一个女神有俩人在追,其中一个追到手了,另一个就只有等分手了才能去重新追)
    3:请求和保持:当一个线程占据了多把锁之后,除非显示的释放锁,否者这些锁都是被线程持有的~
    4:环路等待:等待关系,成环了~~A等B,B等C,C等A;(参考哲学家问题)

volatile 关键字

volatile 能保证内存可见性

代码在写入 volatile 修饰的变量的时候,
改变线程工作内存中volatile变量副本的值
将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

看一个代码:

public class Demo1 {
    
    

    static class Counter {
    
    
        volatile public int count = 0;
        void increase() {
    
    
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在这里插入图片描述
在这里插入图片描述此时可以看到, 最终 count 的值仍然无法保证是 100000
通俗点说:计算机想要执行一些计算,就需要把内存的数据读到cpu寄存器中,然后再里面计算再返回到内存
然而cpu访问寄存器的速度,比访问内存快太多了,当他多次访问内存发现,都一样?~~那就偷个懒吧~从而漏加了一些.

wait 和notify

等待和通知~~
wait 和 notify都是Object对象的方法
调用wait方法的线程就会阻塞,要有其他线程通过notify方法来通知

public class Demo18 {
    
    
    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            // 进行 wait
            synchronized (locker) {
    
    
                System.out.println("wait 之前");
                try {
    
    
                    locker.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        });
        t1.start();

        Thread.sleep(3000);

        Thread t2 = new Thread(() -> {
    
    
            // 进行 notify
            synchronized (locker) {
    
    
                System.out.println("notify 之前");
                locker.notify();
                System.out.println("notify 之后");
            }
        });
        t2.start();
    }
}

在这里插入图片描述
wait内部会做三件事~
1:先释放锁
2:等待其他线程通知
3:收到通知后,重新获取锁,并继续向下执行

所以,要使用wait/notify就得搭配synchronized
wait哪个对象,就得针对哪个对象加锁

猜你喜欢

转载自blog.csdn.net/chenbaifan/article/details/123903205