当前位置: 首页 > news >正文

多线程(一)

目录

一、多线程基础

概述

1.进程与线程

1.1 什么是程序?

1.2 什么是进程?

1.3 什么是线程?

1.4 进程与线程的区别

2.线程基本概念

3.线程的创建与启动

4.线程的创建方式

4.1 方式一:继承java.lang.Thread类(线程子类)

4.2 方式二:实现java.lang.Runnable接口(线程执行类)

4.3 方式三:实现java.util.concurrent.Callable接口,允许子线程返回结果、抛出异常

总结:

4.4 方式四:线程池

5.线程的命名

6.获取线程名

7.线程的休眠(暂停)

8.线程的优先级

9.总结

二、线程的状态及常用方法

1.线程的状态

2.守护线程(Daemon Thread)

2.1 用户线程与守护线程的区别

2.2 设置守护线程

3. 线程的让出:yield()方法

3.1 yield()方法的作用

4.线程的中断:interrupt()方法

4.1 interrupt()方法的作用

4.2 interrupt()方法的原理

5.线程的插队:join()方法

5.1 join()方法的作用

5.2 join()方法的实现原理

4.总结

三、多线程同步

1.多线程的数据不一致

2.synchronized关键字

案例代码

1.多线程售票

四、Synchronized同步锁

1.什么是Synchronized同步锁?

2.什么是锁?

3.synchronized关键字的用法

synchronized修饰实例方法

synchronized修饰静态方法

synchronized修饰代码块

synchronized 关键字的补充


一、多线程基础

概述

现代操作系统(WindowsmacOSLinux)都可以执行多任务。多任务就是同时运行多个任务。例如:播放音乐的同时,浏览器可以进行文件下载,同时可以进行QQ消息的收发。

CPU执行代码都是一条一条顺序执行的,但是,即使是单核CPU,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。

操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒。在用户使用的体验看来,CPU就是在同时执行多个任务。

1.进程与线程

1.1 什么是程序?

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,可以理解为程序是包含静态代码的文件。例如:浏览器软件、音乐播放器软件等软件的安装目录和文件。

1.2 什么是进程?

进程是程序的一次执行过程,是系统运行程序的基本单位。在Windows系统中,每一个正在执行的exe文件或后台服务,都是一个进程,由操作系统统一管理并分配资源,因此进程是动态的。 例如:正在运行中的浏览器就是一个进程,正在运行中的音乐播放器是另一个进程,同理,正在运行中的QQWPS等都是进程。

操作系统运行一个程序,即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着, 同时,每个进程还占有某些系统资源如 CPU时间,内存空间,文件,输入输出设备的使用权等。

1.3 什么是线程?

某些进程内部还需要同时执行多个子任务。例如,我们在使用WPS时,WPS可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行自动保存和上传云文档,我们把子任务称为线程线程是进程划分成的更小的运行单位。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个主线程。

                        ┌──────────┐│Process   ││┌────────┐│┌──────────┐││ Thread ││┌──────────┐│Process   ││└────────┘││Process   ││┌────────┐││┌────────┐││┌────────┐│
┌──────────┐││ Thread ││││ Thread ││││ Thread ││
│Process   ││└────────┘││└────────┘││└────────┘│
│┌────────┐││┌────────┐││┌────────┐││┌────────┐│
││ Thread ││││ Thread ││││ Thread ││││ Thread ││
│└────────┘││└────────┘││└────────┘││└────────┘│
└──────────┘└──────────┘└──────────┘└──────────┘
┌──────────────────────────────────────────────┐
│               Operating System               │
└──────────────────────────────────────────────┘

例如:我们启动JVM运行一个Java程序,其实就是启动了一个 JVM 的进程。在JVM的进程中,又包含了main 主线程、Reference Handler 清理线程、Finalizer线程(用于调用对象 的finalize()方法)等线程。

线程是一个比进程更小的执行单位(CPU的最小执行单位)。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

1.4 进程与线程的区别

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

资源开销:每个进程都有独立的代码副本和数据空间,进程之间的切换,资源开销较大;线程可以看做轻量级的进程,每个线程都有自己独立的运行栈和程序计数器,线程之间切换,资源开销小。

包含关系:一个进程内包含有多个线程,在执行过程,线程的执行不是线性串行的,而是多条线程并行共同完成。

内存分配:同一进程内的所有线程共享本进程的内存空间和资源;进程之间的内存空间和资源相互独立。

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响;一个线程崩溃,会导致整个进程退出,所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口和程序出口。但是线程不能独立执行,必须依存在应用程序(进程)中,由应用程序提供多个线程执行控制。

2.线程基本概念

单线程:单线程就是进程中只有一个线程,单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。

public class SingleThread {public static void main(String[] args) {for (int i = 0; i < 10000; i++) {System.out.print(i + " ");}}
}

多线程:由一个以上的线程组成的程序称为多线程程序。Java中,一定是从主线程开始执行(main方法),然后在主线程的某个位置创建并启动新的线程。

public class MultiThread {public static void main(String[] args) {// 创建2个线程Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {System.out.println("线程1:" + i + " ");}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {System.out.println("线程2:" + i + " ");}}});// 启动2个线程t1.start();t2.start();}
}

3.线程的创建与启动

通过创建Thread实例,完成线程的创建。

        线程的内部实现可以通过继承Thread类、实现Runnable接口等方式进行封装。

通过调用Thread实例的start()方法启动新线程。

查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的本地方法,由JVM根据当前操作系统进行本地实现。

Step* 程序:程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,可以理解为程序是包含静态代码块的文件
* 多进程:进程是程序的一次执行过程,是系统运行程序的基本单位
* 多线程:线程是操作系统运行时能够调度的最小单位,它被包含在进程中,是进程在运行过程中的一个单位
*
* 多线程的应用场景:
* 软件中的耗时操作,拷贝和迁移文件,加载大量的资源的时候
* 所有的后台服务器
* 所有的聊天软件
public class Main {public static void main(String[] args) {// Step1: main主线程执行输出System.out.println("main start...");// Step2: main主线程,创建子线程sub,输出字母A-ZThread sub = new Thread() {// Step4:子线程被执行时,自动调用run()方法public void run() {for(char c='A';c<='Z';c++){System.out.println("子线程:" + c);}}};// Step3: main主线程,启动子线程subsub.start();// Step4: main主线程执行输出字母的ASCII码for(int c ='a';c<='z';c++){System.out.println("main线程:" + c);}System.out.println("main end...");}
}

Step1:main主线程执行输出

Step2: main主线程,创建子线程

Step3: main主线程,启动子线程

Step4: main主线程与子线程同时运行,由操作系统调度,程序本身无法确定线程的调度顺序

        同时,main主线程执行输出

        同时,子线程被执行时,自动调用run()方法

注意:直接调用Thread实例的run()方法是无效的,因为直接调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。

public class Main {public static void main(String[] args) {Thread t = new MyThread();t.run();System.out.println("再执行main主线程");}
}class MyThread extends Thread {public void run() {System.out.println("先执行子线程");}
}

运行结果:

先执行子线程

再执行main主线程

4.线程的创建方式

4.1 方式一:继承java.lang.Thread类(线程子类)

public class Demo01 {public static void main(String[] args) {//方式一//1.创建一个Thread的子类,重写run方法//2.创建子类对象//3.调用start方法启动MyTread t1=new MyTread();t1.start();for (int i = 0; i <100 ; i++) {System.out.println("main线程执行任务:"+i);}}
}
class MyTread extends Thread{@Overridepublic void run() {for (int i = 0; i <100 ; i++) {System.out.println(getName()+"执行任务:"+i);}}
}

4.2 方式二:实现java.lang.Runnable接口(线程执行类)

public class Demo02 {public static void main(String[] args) {//方式二:java.lang.Runnable 接口实现多线程//1.创建Runnable的实现类,重写run方法//2.创建Runnable的实现类对象r1//3.创建Thread类的对象,将r1作为构造方法的参数进行传递//4.调用start方法启动线程MyRun r1=new MyRun();Thread t1=new Thread(r1);t1.start();for (int i = 0; i <100 ; i++) {System.out.println("main线程执行任务:"+i);}}
}
class MyRun implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+"执行任务:"+i);}}
}

4.3 方式三:实现java.util.concurrent.Callable接口,允许子线程返回结果、抛出异常

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo03 {public static void main(String[] args) throws ExecutionException, InterruptedException {//方式三:Callable接口方式实现多线程//1.创建Callable的实现类,重写call方法//2.创建Callable的实现类对象//3.创建FutureTask对象,用来进行结果的管理操作//4.创建Thread类的对象,将步骤3的对象作为参数传递//5.启动线程MyCall c1=new MyCall(1,100);FutureTask<Integer> ft=new FutureTask<>(c1);Thread t1=new Thread(ft);t1.start();System.out.println("子线程c1的结果为:"+ft.get());MyCall c2=new MyCall(1,10);FutureTask<Integer> ft2=new FutureTask<>(c2);Thread t2=new Thread(ft2);t2.start();System.out.println("子线程c2的结果为:"+ft2.get());}
}
class MyCall implements Callable<Integer>{int begin,end;public MyCall(int begin,int end){this.begin=begin;this.end=end;}@Overridepublic Integer call() throws Exception {int number=0;for (int i = begin; i <=end; i++) {number+=i;}return number;}
}

总结:

* Thread     编程比较简单,可以直接使用 Thread 类中的方法 可拓展性差
* Runnable   扩展性强,实现该接口后还可以继承其他的类再实现其他的接口。
* Callable   扩展性强,实现该接口还可以继承其他类,可以有返回值

4.4 方式四:线程池

线程池,按照配置参数(核心线程数、最大线程数等)创建并管理若干线程对象。程序中如果需要使用线程,将一个执行任务传给线程池,线程池就会使用一个空闲状态的线程来执行这个任务。执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。使用线程池可以很好地提高性能。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo04 {public static void main(String[] args) {//使用线程池创建线程对象ExecutorService es= Executors.newCachedThreadPool();es.execute(new MyRun());es.execute(new MyRun());}
}

5.线程的命名

生产环境中,为了排查问题方便,建议在创建线程的时候指定一个合理的线程名字。

        调用父类的setName()方法或在构造方法中给线程名字赋值。

        如果没有为线程命名,系统会默认指定线程名,命名规则是Thread-N的形式。

6.获取线程名

调用父类的getName()方式获取线程名,如果我们没有给线程设置名字,线程默认名称是Thread-X,X使用从0开始。

7.线程的休眠(暂停)

在线程中,可以通过调用Thread.sleep(long millis),强迫当前线程按照指定毫秒值休眠。

public class Demo01 {public static void main(String[] args) throws InterruptedException {//给线程设置名字//setName()方式设置线程名//调用有参构造方法设置线程名//获取线程名//getName()方式获取线程名,如果我们没有给线程设置名字,线程默认名称是Thread-X,X使用从0开始//获取当前线程:Thread.currentThread()//让当前线程休眠:Thread.sleep(毫秒数)MyThread t1=new MyThread();t1.setName("土豆");
//        Thread.sleep(1000);t1.start();//使用构造方法设置线程名MyThread t2=new MyThread("洋芋");
//        Thread.sleep(1000);t2.start();//Runnable作为参数Runnable r1=new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"线程正在启动");try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+"线程运行结束");}};Thread t3=new Thread(r1,"马铃薯");t3.start();}
}
public class MyThread extends Thread{public MyThread() {}public MyThread(String name) {super(name);}@Overridepublic void run() {for (int i = 0; i <100 ; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("当前线程是:"+getName()+i);}}
}

8.线程的优先级

在线程中,通过setPriority(int n)设置线程优先级,范围是1-10,默认为5。

优先级高的线程被操作系统调度的优先级较高(操作系统对高优先级线程,调度更频繁)

注意:并不能代表,通过设置优先级来确保优先级高的线程一定会先执行。

public class Demo02 {public static void main(String[] args) {//创建线程1Thread t1=new Thread(new Runnable() {@Overridepublic void run() {for (char i = 'a'; i <'z' ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}},"线程1");t1.setPriority(1);//创建线程2Thread t2=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <26 ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}},"线程2");t2.setPriority(10);//优先级越高、抢占到资源的概率越大t1.start();t2.start();//获取线程优先级System.out.println(t1.getName()+"优先级为"+t1.getPriority());System.out.println(Thread.currentThread().getName()+"优先级为"+Thread.currentThread().getPriority());}
}

9.总结

(1)Java用Thread对象表示一个进程,通过调用start()启动一个新线程。

(2)一个线程对象只能调用一次start()方法。

(3)线程的执行代码写在run()方法中。

(4)线程调度由操作系统决定,程序本身无法决定调度顺序。

(5)Thread.sleep()可以把当前线程暂停一段时间。

二、线程的状态及常用方法

1.线程的状态

(1)在Java程序中,一个线程对象通过调用start()方法启动线程,并且在线程获取CPU时,自动执行run()方法。run()方法执行完毕,代表线程的生命周期结束。

(2)在整个线程的生命周期中,线程的状态有以下6种:

    New:新建状态,新创建的线程,此时尚未调用start()方法;

    Runnable:运行状态,运行中的线程,已经调用start()方法,线程正在或即将执行run()方法;

    Blocked:阻塞状态,运行中的线程,在等待竞争锁时,被阻塞,暂不执行;

    Waiting:等待状态,运行中的线程,因为join()等方法调用,进入等待;

    Timed Waiting:计时等待状态,运行中的线程,因为执行sleep(等待毫秒值)join(等待毫秒值)等方法,进入计时等待;

    Terminated:终止状态,线程已终止,因为run()方法执行完毕。

           当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

        线程正常终止:run()方法执行到return语句返回;

        线程意外终止:run()方法因为未捕获的异常导致线程终止;

        对某个线程的Thread实例调用stop()方法强制终止(宇宙超级无敌强烈不推荐);

2.守护线程(Daemon Thread)

2.1 用户线程与守护线程的区别

用户线程:我们平常创建的普通线程。

守护线程:用来服务于用户线程的线程,在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;而守护线程执行结束后,虚拟机不会自动退出。

2.2 设置守护线程

在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程。

public class Demo03 {public static void main(String[] args) {MyThread t1=new MyThread("女神"){@Overridepublic void run() {for (char i = 'a'; i <='z' ; i++) {System.out.println(getName()+":"+i);}}};MyThread t2=new MyThread("备胎"){@Overridepublic void run() {for (int i = 0; i <=100 ; i++) {System.out.println(getName()+":"+i);}}};//将t2设置为守护线程//细节:当其他的非守护线程执行完毕后,守护线程会陆陆续续结束t2.setDaemon(true);t1.start();t2.start();}
}

3. 线程的让出:yield()方法

3.1 yield()方法的作用

线程通过调用yield()方法告诉JVM的线程调度,当前线程愿意让出CPU给其他线程使用。

至于系统是否采纳,取决于JVM的线程调度模型:分时调度模型抢占式调度模型。

        分时调度模型:所有的线程轮流获得 cpu的使用权,并且平均分配每个线程占用的 CPU 时间片;

        抢占式调度模型:优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。(JVM虚拟机采用的是抢占式调度模型 )。

    /***yield()方法源代码* A hint to the scheduler that the current thread is willing to yield* its current use of a processor. The scheduler is free to ignore this* hint.* 向调度程序发出的提示,表示当前线程愿意让出处理器的使用权*/public static native void yield();

案例:通过yield()让出CPU。

public class Demo04 {public static void main(String[] args) {//创建线程1Thread t1=new Thread(new Runnable() {@Overridepublic void run() {for (char i = 'a'; i <'z' ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);//线程的让出Thread.yield();}}},"线程1");t1.setPriority(1);//创建线程2Thread t2=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <26 ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);Thread.yield();}}},"线程2");}
}

4.线程的中断:interrupt()方法

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

例如:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

4.1 interrupt()方法的作用

interrupt()方法的作用是设置该线程的中断状态为true,线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于中断状态。线程会不时地检测这个中断状态值,以判断线程是否应该被中断(中断状态值是否为true)。

4.2 interrupt()方法的原理

interrupt()方法只是改变中断状态,不会像stop()中断一个正在运行的线程。支持线程中断的方法(Thread.sleep()join()wait()等方法)就是在监视线程的中断状态,一旦发现线程的中断状态值被置为“true”,就会抛出线程中断的异常InterruptedException,给WAITING或者TIMED_WAITING等待状态的线程发出一个中断信号,线程检查中断标识,就会以退出WAITING或者TIMED_WAITING等待状态;

注意事项:

        线程被Object.wait(), Thread.join()Thread.sleep()三种方法阻塞或等待,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常,从而提前终结被阻塞状态。

        如果线程没有被阻塞或等待,调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()等方法进入阻塞或等待时,才会抛出 InterruptedException异常;

public class Demo06 {public static void main(String[] args) throws InterruptedException {System.out.println("main线程进入");Thread t1=new Thread(new Runnable() {@Overridepublic void run() {//获取当前的系统时间long startTime=System.currentTimeMillis();System.out.println("进入t1线程");try {Thread.sleep(1000*10);} catch (InterruptedException e) {System.out.println("中断t1线程,消耗时间为:"+(System.currentTimeMillis()-startTime));e.printStackTrace();return;}System.out.println("结束t1线程,消耗时间为:"+(System.currentTimeMillis()-startTime));}},"线程1");t1.start();//让主线程休眠Thread.sleep(1000*3);System.out.println("main线程结束");//main主线程修改t1线程的中断默认状态false变为true//t1线程检测到中断状态=true,就会抛出InterruptedException异常,子线程执行结束t1.interrupt();}
}

5.线程的插队:join()方法

5.1 join()方法的作用

​​​​​​​        t.join()方法会使当前线程( 主线程 或者调用t.join()的线程 )进入等待池,并等待 线程t 执行完毕后才会被唤醒。此时,并不影响同一时刻处在运行状态的其他线程。

//线程的插队
public class Demo05 {public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(){@Overridepublic void run() {System.out.println("进到"+Thread.currentThread().getName()+"中");for (int i = 0; i <=100 ; i++) {System.out.println(i);}System.out.println("结束"+Thread.currentThread().getName());}};t1.start();//如果想要线程1先执行,线程2后执行,可以使用线程插队//线程的插队,插入当前的线程的前面t1.join();//主线程执行的任务for (char i = 'a'; i <='z' ; i++) {System.out.println("main:"+i);}}
}

5.2 join()方法的实现原理

(1)join()方法的底层是利用wait()方法实现;

(2)join()方法是一个synchronized同步方法,当主线程调用 线程t.join( )方法时,主线程先获得了 线程t对象 的锁,随后进入join()方法,调用 线程t对象 wait()方法,使主线程进入了 线程t对象 的等待池;

(3)等到 线程t 执行完毕之后,线程在TERMINATED终止状态的时候会自动调用自身的notifyAll()方法,来唤醒所有处于等待状态的线程:这个机制在隐藏在native本地方法中,由一个C++实现的方法ensure_join()函数实现。在该函数的尾部,执行了lock.notify_all(thread);,相当于调用了notifyAll()方法。

//ensure_join( )函数源代码
static void ensure_join(JavaThread*thread) {Handle threadObj(thread, thread -> threadObj());ObjectLocker lock(threadObj, thread);thread -> clear_pending_exception();java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);java_lang_Thread::set_thread(threadObj(), NULL);//下行执行了notifyAll()操作lock.notify_all(thread);thread -> clear_pending_exception();
}

综上所述:join()方法实际上是通过调用wait()方法, 来实现同步的效果的。

例如:A线程中调用了B线程join()方法,则相当于A线程调用了B线程wait()方法,在调用了B线程wait()方法后,A线程就会进入WAITING或者TIMED_WAITING等待状态,因为它相当于放弃了CPU的使用权。

注意:join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕:即join(0)=join()

//join( )方法源代码
public class Thread implements Runnablepublic final void join() throws InterruptedException {join(0);}public final synchronized void join(long millis) throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {// 无限等待wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}// 计时等待wait(delay);now = System.currentTimeMillis() - base;}}}
}

4.总结

(1)线程的状态有以下6种:NewRunnableBlockedWaitingTimed WaitingTerminated

(2)join()方法用于实现线程插队,调用完毕后会释放锁。

(3)sleep()方法用于实现线程休眠,调用完毕后不会释放锁。

(4)interrupt()方法用于设置该线程的中断状态为true

(5)yield()方法用于让出CPU执行。

(6)JVM的线程调度模型:分时调度模型和抢占式调度模型。

三、多线程同步

1.多线程的数据不一致

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。

这个时候,一个在单线程模型下不存在的问题就会发生:如果多个线程同时读写共享变量,会出现数据不一致的问题。例如:

public class Main {public static void main(String[] args) throws Exception {Thread add = new AddThread();Thread dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);}
}class Counter {public static int count = 0;
}class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) { Counter.count += 1; }}
}class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) { Counter.count -= 1; }}
}

在上述代码案例中,两个线程同时对一个int变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。

这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作原子操作是指不能被中断的一个或一系列操作

例如:n = n + 1;看上去是一行语句,实际上对应了3条指令:

ILOAD
IADD
ISTORE

我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:

┌───────┐    ┌───────┐
│Thread1│    │Thread2│
└───┬───┘    └───┬───┘│            ││ILOAD (100) ││            │ILOAD (100)│            │IADD│            │ISTORE (101)│IADD        ││ISTORE (101)│▼            ▼

如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102

说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

┌───────┐     ┌───────┐
│Thread1│     │Thread2│
└───┬───┘     └───┬───┘│             ││-- lock --   ││ILOAD (100)  ││IADD         ││ISTORE (101) ││-- unlock -- ││             │-- lock --│             │ILOAD (101)│             │IADD│             │ISTORE (102)│             │-- unlock --▼             ▼

通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

2.synchronized关键字

综上所述,保证一段代码的原子性就是通过加锁解锁实现的。在Java的多线程模型中使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {n = n + 1;
}

synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下:

//多线程数据不一致
public class Main {public static void main(String[] args) throws Exception {Thread add = new AddThread();Thread dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);}
}class Counter {public static final Object lock = new Object();public static int count = 0;
}class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.count += 1;}}}
}class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.count -= 1;}}}
}

注意下列代码:它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0

synchronized(Counter.lock) { // 获取锁...
} // 释放锁

使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

概括总结一下如何使用synchronized

        1.找出修改共享变量的线程代码块;

        2.选择一个共享实例作为锁;

        3.使用synchronized(lockObject) { ... }

注意事项:

抛出异常:在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

public void add(int m) {synchronized (obj) {if (m < 0) {throw new RuntimeException();}this.value += m;} // 无论有无异常,都会在此释放锁
}

不同的lock:下列代码是一个错误使用synchronized的例子:

public class Main {public static void main(String[] args) throws Exception {Thread add = new AddThread();Thread dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);}
}class Counter {public static final Object lock1 = new Object();public static final Object lock2 = new Object();public static int count = 0;
}class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock1) {Counter.count += 1;}}}
}class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock2) {Counter.count -= 1;}}}
}

结果并不是0,这是因为两个线程各自的synchronized锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。

因此,使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。

案例代码

1.多线程售票

3个线程模拟3个售票窗口,卖出公共票池的20张票。

方式 1 : 使用 synchronized 实现实现方式中,包含有一个常见错误

//启动类
public class Main {public static void main(String[] args) {// 公共票池,默认门票数量为20张TicketPool publicTicketPool = new TicketPool(20);// 创建3个线程,用于模拟3个不同的售票窗口,共同卖出公共票池中的20张门票new Thread(publicTicketPool, "窗口1").start();new Thread(publicTicketPool, "窗口2").start();new Thread(publicTicketPool, "窗口3").start();}
}
//公共票池类
public class TicketPool implements Runnable {// 当前剩余门票数private int ticketNum;// 创建公共票池时,传入默认总门票数public TicketPool(int ticketNum) {this.ticketNum = ticketNum;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "准备开始卖票");// 线程竞争CPU执行权(this锁)synchronized (this) {while(true) {if (ticketNum <= 0) {System.out.println(Thread.currentThread().getName() + "没有票了");return;} else {System.out.println(Thread.currentThread().getName() + "卖出了一张门票,剩余" + --ticketNum + "张门票");try {// 当前线程休眠1000毫秒,模拟延迟// 注意:休眠过程中,当前线程不会让出持有的"this锁",此处为引发错误的原因Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}}public int getTicketNum() {return ticketNum;}public void setTicketNum(int ticketNum) {this.ticketNum = ticketNum;}
}

运行结果:错误仅有一个窗口(线程)参与售票,其它窗口(线程)闲置

窗口2准备开始卖票
窗口3准备开始卖票
窗口1准备开始卖票
窗口2卖出了一张票,剩余19张票
窗口2卖出了一张票,剩余18张票
窗口2卖出了一张票,剩余17张票
窗口2卖出了一张票,剩余16张票
窗口2卖出了一张票,剩余15张票
窗口2卖出了一张票,剩余14张票
... ...

代码修正

// 当前线程休眠1000毫秒,模拟延迟
// 注意:休眠过程中,当前线程不会让出持有的"this锁"
// Thread.sleep(1000);// 持有“this锁”的当前线程等待1000毫秒,模拟延迟
// 注意:等待过程中,当前线程让出持有的"this锁",允许其它线程参与竞争CPU执行权(this锁)
this.wait(1000);

方式 2 : 使用公共Object对象锁多个线程持有同一个Object对象锁

//启动类
public class Main {public static void main(String[] args) {// 公共对象锁Object lockObj = new Object();// 公共票池,默认门票数量为20张TicketPool publicTicketPool = new TicketPool(20,lockObj);// 创建3个线程,用于模拟3个不同的售票窗口,共同卖出公共票池中的20张门票new Thread(publicTicketPool, "窗口1").start();new Thread(publicTicketPool, "窗口2").start();new Thread(publicTicketPool, "窗口3").start();}
}
//公共票池类
public class TicketPool implements Runnable {// 当前剩余票数private int ticketNum;// 对象锁private Object lockObj;// 创建线程时,传入总票数 + 对象锁public TicketPool(int ticketNum,Object lockObj) {this.ticketNum = ticketNum;this.lockObj = lockObj;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "准备开始卖票");// 线程竞争CPU执行权(this锁)synchronized (lockObj) {while(true) {if (ticketNum <= 0) {System.out.println(Thread.currentThread().getName() + "没有票了");return;} else {System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余" + --ticketNum + "张票");try {// 持有“lockObj锁”的当前线程等待1000毫秒,模拟延迟// 注意:等待过程中,当前线程让出持有的"lockObj锁"lockObj.wait(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}}public int getTicketNum() {return ticketNum;}public void setTicketNum(int ticketNum) {this.ticketNum = ticketNum;}
}

运行结果:正常所有窗口均参与卖票,剩余票量正常

窗口1准备开始卖票
窗口3准备开始卖票
窗口2准备开始卖票
窗口1卖出了一张票,剩余19张票
窗口2卖出了一张票,剩余18张票
窗口3卖出了一张票,剩余17张票
窗口3卖出了一张票,剩余16张票
窗口2卖出了一张票,剩余15张票
窗口1卖出了一张票,剩余14张票
窗口1卖出了一张票,剩余13张票
窗口2卖出了一张票,剩余12张票
窗口3卖出了一张票,剩余11张票
窗口2卖出了一张票,剩余10张票
窗口3卖出了一张票,剩余9张票
窗口1卖出了一张票,剩余8张票
窗口3卖出了一张票,剩余7张票
窗口2卖出了一张票,剩余6张票
窗口1卖出了一张票,剩余5张票
窗口3卖出了一张票,剩余4张票
窗口2卖出了一张票,剩余3张票
窗口1卖出了一张票,剩余2张票
窗口3卖出了一张票,剩余1张票
窗口1卖出了一张票,剩余0张票
窗口2没有票了
窗口1没有票了
窗口3没有票了

四、Synchronized同步锁

1.什么是Synchronized同步锁?

Synchronized同步锁,简单来说,使用Synchronized关键字将一段代码逻辑,用一把锁给锁起来,只有获得了这把锁的线程才访问。并且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码,从而确保代码的线程安全。

2.什么是锁?

每个Java对象都可以充当一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。

synchronized(reference-to-lock) {//临界区
}

其中, 括号里面的reference-to-lock就是锁的引用, 任何一个Java对象都可以成为reference-to-lock。 你可以实例化一个Object对象,将它作为锁。如果直接使用this,代表使用当前对象作为锁。

3.synchronized关键字的用法

(1)修饰实例方法:synchronized修饰实例方法, 则用到的锁,默认为this当前方法调用对象;

(2)修饰静态方法:synchronized修饰静态方法, 则其所用的锁,默认为Class对象;

(3)修饰代码块:synchronized修饰代码块, 则其所用的锁,是某个指定Java对象;

synchronized修饰实例方法

        使用当前对象this充当锁完成对当前方法的锁定,只有获取this锁的线程才能访问当前方法;

        并发过程中,同一时刻,可以有N个线程请求执行方法,但只有一个线程可以持有this锁,才能执行;

        不同线程,持有的对象,必须相同;

当使用synchronized 修饰实例方法时, 以下两种写法作用和意义相同:

public class Foo {// 实例方法public synchronized void doSth1() {// 获取this锁,才能执行该方法}// 实例方法public void doSth2() {synchronized(this) {// 获取this锁,才能执行该代码块}}
}
public static void main(String[] args) {// 实例化一个对象Foo fa = new Foo();// 创建不同的线程1Thread thread01 = new Thread() {public void run() {// 使用相同的对象访问synchronized方法fa.doSth1();}};// 创建不同的线程2Thread thread02 = new Thread() {public void run() {// 使用相同的对象访问synchronized方法fa.doSth1();}};// 启动线程thread01.start();thread02.start();
}

synchronized修饰静态方法

        使用当前对象的Class对象充当锁,完成对当前方法的锁定,只有获取Class锁的线程才能访问当前方法;

        不同线程,持有的对象,可以不同,但必须相同class类型;

当使用synchronized 修饰静态方法时, 以下两种写法作用和意义相同:

public class Foo {// 静态方法public synchronized static void doSth1() {// 获取当前对象的Class对象锁,才能执行该方法}// 实例方法public static void doSth2() {synchronized(this.getClass()) {// 获取当前对象的Class对象锁,才能执行该代码块}}
}
public static void main(String[] args) {// 创建不同的对象(相同类型)Foo fa = new Foo();Foo fb = new Foo();// 创建不同线程1Thread thread01 = new Thread() {public void run() {// 使用不同的对象访问synchronized方法fa.doSth2();}};// 创建不同线程2Thread thread02 = new Thread() {public void run() {// 使用不同的对象访问synchronized方法fb.doSth2();}};// 启动线程thread01.start();thread02.start();
}

synchronized修饰代码块

synchronized(自定义对象) {//临界区
}

synchronized 关键字的补充

        当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。在没有加锁的情况下, 所有的线程都可以自由地访问对象中的代码, 而synchronized关键字只是限制了线程对于已经加锁的同步代码块的访问,并不会对其他代码做限制。所以,同步代码块应该越短小越好。

        父类中synchronized修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用synchronized修饰,则该方法不是线程安全的;

        在定义接口方法时,不能使用synchronized关键字;

        构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步;

        离开synchronized代码块后,该线程所持有的锁,会自动释放;

http://www.lryc.cn/news/610189.html

相关文章:

  • 【Spring AI快速上手 (二)】Advisor实现对话上下文管理
  • 【计算机网络 | 第2篇】计算机网络概述(下)
  • 如何使用 DBeaver 连接 MySQL 数据库
  • 移动端 WebView 视频无法播放怎么办 媒体控件错误排查与修复指南
  • SAP-ABAP:ABAP Open SQL 深度解析:核心特性、性能优化与实践指南
  • 深入剖析Java Stream API性能优化实践指南
  • Mybatis 简单练习,自定义sql关联查询
  • 卸油管链接检测误检率↓76%:陌讯多模态融合算法实战解析
  • Dbeaver数据库的安装和使用(保姆级别)
  • 基于FAISS和Ollama的法律智能对话系统开发实录-【大模型应用班-第5课 RAG技术与应用学习笔记】
  • Ubuntu系统VScode实现opencv(c++)图像一维直方图
  • 机器学习【六】readom forest
  • 微服务配置管理:Spring Cloud Alibaba Nacos 实践
  • 电子电气架构 ---智能电动汽车嵌入式软件开发过程中的block点
  • Nginx服务做负载均衡网关
  • 36.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--缓存Token
  • FPGA学习笔记——简单的乒乓缓存(RAM)
  • 飞算JavaAI需求转SpringBoot项目沉浸式体验
  • android内存作假通杀补丁(4GB作假8GB)
  • labview连接PLC的三种方式
  • 设计模式(一)——抽象工厂模式
  • ES集群规划与调优
  • 进程间通信:管道与共享内存
  • 移动前后端全栈项目
  • 读写分离有那些坑?
  • 16.8 华为昇腾CANN架构深度实战:3大核心引擎解析与性能优化216%秘籍
  • 手搓TCP服务器实现基础IO
  • falsk windows 服务器部署-解决服务器外无法访问
  • javacc学习笔记 02、JavaCC 语法描述文件的格式解析
  • Sklearn 机器学习 数据聚类 肘部法则选择聚类数目