Java多线程基础汇总(上)

目录

一. 概念

 二.线程的创建

三. Thread类的常见方法

1.启动一个线程

2.终止一个线程

3.等待一个线程

四. 线程安全问题

1.导致线程安全的原因:

 2.如何解决线程安全问题

2.1  synchronized关键字

2.2  volatile关键字

3. wait 和 notify

4.wait 和 sleep的区别(面试题)



一. 概念

     Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

     线程:一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码。通俗点说就是一个应用程序中执行着不同的功能,比如我们用微信聊天,微信运行起来相当于一个进程,而我们使用微信聊天,发朋友圈等等就相当于不同的线程。

     进程:是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,通俗点说相当于一个跑起来的程序。

     举个例子:就拿吃饭这件事来说,我们在吃饭的同时,可以有玩手机,看电视,聊天等等一系列的行为,我们把吃饭这件事就可以看做是一个进程,而玩手机,看电视,聊天这样的行为可以看做是一个独立的线程,当这些行为一起进行的时候,我们就可以看做是一个多线程任务


 二.线程的创建

转载我之前的blog:Java线程的创建_Bc_小徐的博客-CSDN博客


三. Thread类的常见方法

1.启动一个线程

如果要启动一个线程,我们调用Thread类的start方法来解决

public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("线程1");
        });
        //启动线程
        thread.start();
        System.out.println("线程2");
    }

当我们调用start方法后,这个才真正的在操作系统的底层创建出了一个线程,这个线程就进入就绪状态了,等待cpu的调度;

2.终止一个线程

在多线程中,我们通常通过设置一个标志位来终结一个线程的运行

class Mythread implements Runnable{
    //设置一个标志位,通过改变标志位的值,来终止线程的进行
    public boolean flag = true;
    @Override
    public void run() {
        while (flag){
            for (int i = 0; i < 999; i++) {
                System.out.println(i);
            }
        }
    }
}
public class Demo3 {
    public static void main(String[] args) {
      Mythread mythread = new Mythread();
      Thread thread = new Thread(mythread);
        thread.start();
        for (int i = 0; i < 999; i++) {
            System.out.println(i);
            if(i == 666){
              //改变标志位的值
                mythread.flag = false;
                System.out.println("该线程该停止了");
            }
        }
    }
}

3.等待一个线程

在线程里面,有的时候我们需要等待一个线程先执行完,再进行下一个线程的进行,此时,Thread标准库里提供了一个join方法,让我们去实现:

public class Demo4 {
    //等待一个线程
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("线程1");

        });
        //启动线程
        thread.start();
        //在main函数里调用join,意思是让main线程等待thread线程执行完,main线程才执行
        thread.join();
        System.out.println("主线程");
    }
}

 

从执行结果我们可以看到,这里在main函数里调用了join方法,意思在让thread线程先执行,执行完主线程才开始执行, 那如果thread线程一直没有执行完,那么主线程会处于一个叫阻塞等待的状态,也不会参与CPU的调度,直到thread线程执行完,阻塞才会解除,继续执行;


四. 线程安全问题

1.导致线程安全的原因:

关于线程安全的问题我认为在Java中一直是个重要的问题,我们写程序一直要确保万无一失,关于线程安全可以举个例子来看:

class Counter {
    public int count = 0;

    public void add() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.add();
            }

        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.add();
            }

        });
        //启动线程
        thread1.start();
        thread2.start();

        //等待线程
        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());
    }

上述代码中,我们利用线程来对count 进行++的操作,并且让thread1和thread2线程各循环1000次,如果按照正常的逻辑执行的话,我们最后输出的一个结果是2000,但是真正输出的结果并不是这样的,如下图可以看到,我们分别打印了3次,而每次的结果都是不同的,所以

 

 


这里就涉及到线程安全的问题了,出现这种Bug的原因实际上主要和线程调度的随机性有关,针对这个count++的操作,站在CPU的角度分析,是相当于三条指令来进行完成的:

第一步:Load操作:把内存中的数据读取到CPU寄存器中

第二步:Add:把寄存器的值进行+1的运算

第三步:Save:把寄存器的值写回到内存中

这三个步骤在两个线程中并发也是随机进行的,相当于线程的调度是随机的,它是一个大的范围,而对于++这个操作,这三条指令也是随机执行的,它在大的范围之下的小的范围,这就导致了为什么最终的结果和我们预期的不一样;

上述的每一个系列号都是一种情况:

   拿第二个图来说,thread1 的 load,add,save先执行,当thread1的三条指令全部执行完,此时就进行了一个加1的操作,thread1执行完,再执行thread2 的三条指令;

   拿第三个图来说,thread1 的 load 指令先执行,再执行 thread2 的loaf add save 的操作,此时 thread1 的加1操作并没有执行完,所以不会加1,当执行完 thread2 的三条操作后,再执行thread1的剩下两条指令(add,save),当这两条指令执行完之后,才是一个完整的加1操作;

这也是导致线程安全的主要原因:它不是按顺序执行的,而是随机调度的;


 2.如何解决线程安全问题

2.1  synchronized关键字

    上述的线程不安全问题,主要就是三条指令随机调度导致的,为了解决这一问题,我们可以对其加锁操作,将这三条指令的操作封装起来,意思就是要执行就一起执行,不能分散的执行,这样也是确保了操作的原子性,使得即使2个线程并发执行,但是要执行的操作是原子的,所谓原子,就是不可分割的;

Java里对于加锁的操作是使用synchronized关键字:

在被synchronzied修饰的代码块中,会触发加锁,出了synchronized代码块,就会解锁;

public void add() {
        synchronized (this){
            count++;
        }
    }

 关于synchronized的写法:

如果对于普通方法:

第一种写法,锁住的对象就是当前的操作
public void add() {
        synchronized (this){
            count++;
        }

    }
第二种写法,直接修饰这个方法

synchronized public void add(){
        count++;
    }

如果对于静态成员方法:

当修饰静态方法的时候
第一种写法,synchronized后面锁住的对象就是当前类对象
public static void add(){
        synchronized (Counter.class){

        }
    }
第二种写法,直接修饰这个静态方法    
synchronized public static void add(){
        
    }

2.2  volatile关键字

导致线程安全的原因有很多,不止是原子性这一方面,例如下面一段代码:

public class Demo6 {
    private static int ret = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (ret == 0) {

            }
            System.out.println("线程结束");
        });
        Thread thread2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            ret = scan.nextInt();
        });
        thread1.start();
        thread2.start();
    }
}

上述代码中,我们通过输入ret的值来结束线程的进行,当我们输入一个非零的数,按照代码的逻辑,就可以结束线程1,打印线程结束了,但是预期的结果和我们实际输出的结果并不一样,当我们运行程序可以看到:

线程始终没有结束,并且也没有打印线程结束,导致这一原因就是编译器优化做的决策,

 所以这里相当于无论你怎么输入,它始终与第一次拿到的值进行比较,为了解决这一问题,我们就要使用volatile关键字来解除编译器优化,以确保拿到的值是你下一次输入的值,而不是和第一次拿到的值进行比较了;

加上volatile关键字之后,输出的结果就和我们预期的一样了,被volatile修饰的变量就禁止编译器优化,保证每次都是从内存中重新读取数据;

注:但是volatile 不能保证原子性,它使用的场景是一个线程读,一个线程写;


3. wait 和 notify

     由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序,此时wait和notify的作用就来了,我们可以通过这两个关键字来控制线程的先后进行:

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread thread1 = new Thread(() -> {

            try {
                System.out.println("线程1中的wait开始");
                synchronized (locker) {
                    locker.wait();
                }
                System.out.println("线程1中的wait结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });
        //启动线程
        thread1.start();
        //线程休眠
        Thread.sleep(1000);

        Thread thread2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("线程2中的notify开始");
                locker.notify();
                System.out.println("线程2中的notify结束");
            }
        });
        //启动线程
        thread2.start();
    }
}

上述代码中我们就使用了wait和notify这两个关键字,让线程1先启动,再使用wait让线程1进入阻塞状态,再执行线程2,等线程2执行完,notify会起到一个唤醒的作用,唤醒线程1,让线程1执行完,这样就灵活的控制了线程执行的顺序;


wait主要做的事有:

1.解锁(所以在使用wait之前,要对其进行加锁,不然怎么解锁)

2.进入阻塞状态

3.等待被唤醒,重新拿到锁

notify的作用就是唤醒等待的方法

4.wait 和 sleep的区别(面试题)

1.wait需要搭配 synchronized 使用,而 sleep 不需要

2.wait 是 object 的方法,sleep 是 Thread的方法

猜你喜欢

转载自blog.csdn.net/m0_63635730/article/details/129783434