线程简介
进程(Process)
进程,是正在运行的程序实例,是操作系统进行资源分配的最小单位。
每个进程都有它自己的地址空间和系统资源(比如CPU时间,内存空间,磁盘IO等)。
多个进程可以同时执行,每个进程在运行时都不会影响其他进程的运行,资源不共享。
程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程:
当打开一个软件程序时,计算机会为它分配一个资源区域,并为其分配一定数量的内存,这个程序就是一个进程。举个例子来说,比如打开了一个文字编辑器,这个编辑器就是一个进程。计算机会给它分配一定数量的内存空间,用于它的运行和存储。
通俗点说,进程是操作系统中运行的一个任务(一个应用程序运行在一个进程中)
电脑就是多进程的。 可以同时启动,视频播放器,音乐播放器,QQ、wechat等各种应用程序。
线程(Thread)
线程是进程的一部分,是CPU能够进行运算调度的最小单位。线程不能独立存在,必须依赖于进程。
线程是一个进程中的顺序执行流(执行单元)。一个进程中可以有一个线程,也可能有多个线程。每个线程都有自己的指令指针、堆栈和局部变量等,但它们共享进程的代码、数据和全局变量等资源。
多线程可以实现并发执行,提高程序的效率。
线程使用的场景:
-
当一个程序中需要同时完成多个任务的情况下,我们可以将每个任务定义成一个线程,使他们得以同时工作,
-
有些时候,虽然可以使用单一线程完成,但是使用多线程可以更快完成,如下载文件。
进程和线程的区别:
进程是操作系统运行的一个任务,线程是进程中运行的一个任务
进程是资源分配的最小单位(相互独立),线程是程序执行的最小单位(cpu调度的基本单元)。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。进程之间的通信需要以IPC进行
线程是轻量级的进程,同一个进程中可以包含多个线程。多线程共享进程中的数据,使用相同的地址空间,因此,线程之间的通信更方便,CPU切换(或创建)一个线程的开销远比进程要小很多。
一个进程结束,其内的所有线程都结束,但不会对另外一个进程造成影响。多线程程序,一个线程结束,有可能会造成其他线程结束
不过如何处理好同步与互斥是编写多线程程序的难点。
CPU核心数
现在,一个CPU都是多核心的。
内核(处理器):一个CPU内核在一个时间片上可以执行一个线程
逻辑处理器:同时可以处理的线程数量。(超线程时使用)
当物理CPU使用了超线程技术后,在CPU的一颗内核中,利用就是利用其中空闲的执行单元,模拟出另外一个核心(并不是真正的物理运算核心),使得CPU的这颗内核有两个逻辑核心,也就是所谓的逻辑CPU。
此时物理CPU的一颗内核在一个时间片内理论上可同时执行两个内核线程,从而提高了整个CPU的工作效率,此时逻辑CPU的数量=物理CPU的数量x单个CPU的内核数x2。
值得注意的是,一颗内核并不代表只能有一个或者两个逻辑CPU,也可以有4个逻辑CPU或者更多。
CPU时间片
在宏观上,我们可以同时打开多个应用程序,每个程序同时运行,互不打扰。
但在微观上:由于只有一个CPU(一个内核而言),一次只能运行某一个进程的某一个线程。如何公平处理,一种方法就是引入时间片的概念,每个程序轮流执行。
CPU调度机制算法,会将时间划分成一个个时间片,时间片的大小从几ms到几百ms。
线程调度
线程调度是操作系统管理线程执行顺序的一种机制。它的主要目的是有效地分配处理器的使用权,让多个线程能够共享处理器资源,从而实现并发执行。
线程调度通过调度算法决定哪个线程在何时运行,通常涉及以下几个方面:
- 调度算法:
常见见的调度算法包括先来先服务(FCFS)、短作业优先(SJF)、轮转调度(Round Robin)、优先级调度等。不同的算法有不同的性能表现,适用于不同的场景。
- 上下文切换
操作系统需要切换到另一个线程时,它会保存当前线程的状态(如寄存器、程序计数器等),并加载下一个线程的状态。这个过程称为上下文切换,会带来一定的性能开销。
- 多线程环境:
在多线程编程中,线程调度确保多个线程能够高效地共享 CPU 时间,有效利用多核处理器的优势。
- 优先级管理:
某些线程可能比其他线程更重要,调度器可以基于线程的优先级进行调度,确保高优先级的线程优先获得执行权。
线程调度是实现高效并发和响应时间的重要基础,对系统性能和用户体验有着直接影响。
进程调度
进程调度是操作系统中管理进程执行顺序的关键机制。其主要功能是确定哪些进程在何时使用 CPU,以实现系统的有效运行与资源的合理分配。下面是进程调度的几个重要方面:
调度算法:
- 先来先服务(FCFS):按照进程到达的顺序进行调度。
- 短作业优先(SJF):优先调度运行时间短的进程。
- 轮转调度(Round Robin):每个进程分配一个时间片,轮流执行。
- 优先级调度:根据进程的优先级进行调度,高优先级的进程先执行。
上下文切换:
当操作系统从一个进程切换到另一个进程时,会保存当前进程的状态并加载下一个进程的状态。这一过程称为上下文切换,虽然必要,但会消耗系统资源。
进程状态:
进程可以处于不同的状态,如就绪、运行、等待和终止。调度器负责管理这些状态,决定哪些进程可以进入运行状态,哪些需要等待。
多道程序设计:
现代操作系统通常采用多道程序设计,能够同时管理多个进程。进程调度确保这些进程能够以高效的方式共享 CPU 资源。
进程调度对系统的响应时间、吞吐量和资源利用率有着重要影响。
串行与并发
串行执行是指程序中的各个任务一个接一个地顺序执行。在这种模式下,只有一个任务在任何时间点上执行,其他任务则处于等待状态。串行执行的特点包括:
- 简单性:程序的控制流更简单,容易理解和调试。
- 资源占用:通常在资源占用方面比较高效,因为在执行一个任务时不会有另一个任务干扰。
- 性能限制:在处理较大或计算密集型的任务时,串行执行可能导致效率低下,因为没有利用到多核处理器的优势。
并发执行是指多个任务在同一时间段内进行,但不一定在同一时刻运行。也就是说,虽然有多个任务看似同时进行,但实际上它们可能会在处理器上交替执行。并发的特点包括:
- 提高效率:通过并发执行,可以更充分地利用系统资源,特别是在多核处理器上。
- 响应性:在处理需要等待的任务(如 I/O 操作)时,其他任务可以继续执行,从而提高程序的响应性。
- 复杂性:并发编程通常更复杂,涉及线程的创建、管理、同步和状态共享等,需要处理多线程之间的竞争条件、死锁等问题。
线程的调度机制
在Java中,线程(Thread)可以处于以下几种状态:
新建状态(New):线程对象已经创建,但还没有调用start()方法。
就绪状态(Runnable):线程已经调用start()方法,等待CPU调度执行。
运行状态(Running):线程获得CPU时间片,开始执行run()方法里的代码。
阻塞状态(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行,直到进入就绪状态。(释放时间片段)
等待状态(Waiting):线程因为某些条件而进入等待状态,此时不会被分配CPU时间片,直到其他线程显式地唤醒。
超时等待状态(Timed Waiting):线程在指定的时间内等待,时间到后会自动返回到就绪状态。
终止状态(Terminated):线程的run()方法执行完毕或者因异常退出而结束线程的生命周期。
抢占式调度与协同式调度
JVM线程调度的实现依赖于底层操作系统的支持。由于Java是跨平台的,JVM会利用底层操作系统提供的功能来管理线程的调度。
Java线程调度基本上是抢占式的,在这种模式下,每个线程都有机会获得CPU时间片,操作系统基于线程的优先级来决定哪个线程更应该运行。高优先级的线程会得到更多的运行机会。
而相对的协同式调度则要求线程主动释放控制权,当前运行的线程必须主动让出CPU时间,其他线程才能获得执行机会。但在Java中,由于大多数现代操作系统都采用抢占式调度,协同式调度在Java中并不常见。
线程的优先级
线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片段的概率。Java线程可以通过setPriority方法设置优先级,优先级较高的线程有更大的几率获得CPU时间片。
线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高。线程提供了三个常量来表示最低,最高,以及默认优先级(5)
-
Thread.MIN_PRIORITY
-
Thread.MAX_PRIORITY
-
Thread.NORM_PRIORITY
线程的生命周期管理
JVM通过Thread类提供的方法来管理线程生命周期,例如start()、sleep()、yield()、join()、wait()等,使得线程在适当的时候运行或暂停
start()方法
可以使线程处于就绪状态
yield()方法
可以使当前运行的线程让出自己的时间片,但不会阻塞线程,只是将线程从运行状态转移至就绪状态。
join()方法
可以让一个线程等待另一个线程完成后再继续执行。
sleep()方法
可以使当前线程暂停执行指定时间。
wait()方法
是当前线程释放锁,释放cpu等资源,进入等待状态
线程的创建及常用API
第一种
继承Thread类,重写run方法,创建该类对象,调用start方法开启线程。start方法可以将该线程对象纳入可执行线程池中。切记,不是调用run方法
可以简化:如果只使用一次,可以使用匿名内部类
public static void main(String[] args) {//创建线程对象Thread t = new Thread(){public void run(){//重写run方法int sum = 0;for (int i = 0; i < 10000; i++) {sum += i;}System.out.println("任务结束后:sum的值:"+sum);}};//调用start方法,让线程进入可运行状态t.start(); // 什么时候真正执行里面的任务,cpu只要把时间片段给了该线程,就执行,此时不是程序员说的算了System.out.println("---main线程结束---");} } /*** 定义一个任务(线程): 计算1~10000的和。*/ //class MyThread extends Thread { // // run方法就是用来定义任务的 // public void run(){ // int sum = 0; // for (int i = 0; i < 10000; i++) { // sum += i; // } // System.out.println("任务结束后:sum的值:"+sum); // } //}
第二种
实现Runnable接口,重写run方法,创建Thread类对象,将Runnable子类对象传递给Thread类对象。调用start方法开启线程。
可以简化:匿名内部类
第二种方式比第一种要好很多:
将线程对象和线程任务对象分离开。降低了耦合性,利于维护
实现Runnable接口比继承Thread类所具有的优势:
适合多个相同的程序代码的线程去处理同一个资源
可以避免java中的单继承的限制
增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
public static void main(String[] args) {Thread t = new Thread(() -> { //lambda表达式简化也可以for (int i = 0; i < 10; i++) {System.out.println("i的值:" + i);}});//Runnable task=new MyTask();//Thread t=new Thread(task);t.start();Thread.yield();System.out.println("-----main线程结束---------");} }/*** 此时可以直接实现Runnable接口,来重写run方法,定义自己的任务*/ //class MyTask implements Runnable { // // run方法就是用来定义任务的 // public void run(){ // for (int i = 0; i < 10; i++) { // System.out.println("i的值:"+i); // } // } //}
第三种
创建Callable接口的实现类对象,并实现call()方法。该call()方法将作为线程的执行体,该call()方法有返回值,(可调用)
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(未来任务)
使用FutureTask对象作为Thread对象的target创建井启动新线程。(线程)
使用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
第三种使用Runnable功能更加强大的一个子类.这个子类是具有返回值类型的任务方法.
大部分人熟知的都是继承Thread类或实现Runnable接口,但上述两种方式来执行的线程其实都是没有返回值的,如果我们想要通过线程的执行获得一些有用的信息的话,那么通过继承Thread类或实现Runnable接口都是无法办到的。
public static void main(String[] args) {//定义任务: 计算1到100的和Callable c = new Callable<Integer>() {public Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 100; i++) {Thread.sleep(100);sum += i;}return sum;}};// 创建FutrueTask对象FutureTask<Integer> task = new FutureTask<Integer>(c);//创建线程对象,传入task Thread t = new Thread(task);// 启动线程t.start();// 获取计算结果try {//注意: FutureTask的get方法有阻塞所在线程的效果。System.out.println("计算结果:"+task.get());} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
常用构造器
- Thread(Runnable target)
使用指定Runnable对象作为参数,创建一个Thread对象
- Thread(Runnable target, String name)
使用指定Runnable对象作为参数,创建一个Thread对象,并指定对象名为name.
- Thread(String name)
创建一个Thread对象,并指定名称为name
public static void main(String[] args) {//创建一个线程,对数组{10,19,39,23,45,12}进行冒泡排序Runnable task = ()->{int[] arr = {10,19,39,23,45,12};for (int i = 0; i < arr.length - 1; i++) {for (int j = 0; j < arr.length-1-i; j++) {if (arr[j] > arr[j+1]) {int temp = arr[j];arr[j] = arr[j+1];arr[j+1] = temp;}}}System.out.println("排序后的样子:"+ Arrays.toString(arr));};Thread t1 = new Thread(task);t1.start();//获取线程的名字System.out.println(t1.getName());/*第二个构造器的测试:*/Thread t2 = new Thread(task,"冒泡排序");t2.start();System.out.println(t2.getName());/*第三个构造器的测试:*/Thread t3 = new Thread("计算斐波那契数列"){public void run(){int feiBoNaQi = getFeiBoNaQi(8);System.out.println(feiBoNaQi);}};t3.start();System.out.println(t3.getName());}/** 获取第n个斐波那契数列的值:* // 1 1 2 3 5 8 13 21.....* */public static int getFeiBoNaQi(int n){if(n == 1 || n == 2){return 1;}return getFeiBoNaQi(n-2) + getFeiBoNaQi(n-1);}
常用的属性方法
- static Thread currentThread()
Thread类的静态方法,可以用于获取运行当前代码片段的线程对象
- long getId()
返回该线程的标识符
- String getName()
返回该线程的名称
- int getPriority()
返回该线程的优先级
- Thread.State getState()
获取线程的状态
- boolean isAlive()
判断线程是否处于活动状态
- boolean isInterrupted()
判断线程是否已经中断
- boolean isDaemon()
判断线程是否为守护线程
public static void main(String[] args) {Thread t1 = new Thread("mother"){public void run(){//方法1:static Thread currentThread(); 获取所在线程的对象地址Thread thread = Thread.currentThread();for(int i=0; i<10; i++){System.out.println(i+"次该线程的名字"+thread.getName());}System.out.println(thread.getName()+"的id:"+thread.getId());System.out.println(thread.getName()+"的优先级:"+thread.getPriority());System.out.println(thread.getName()+"的状态:"+thread.getState());System.out.println(thread.getName()+"是否alive:"+thread.isAlive());System.out.println(thread.getName()+"是否中断:"+thread.isInterrupted());System.out.println(thread.getName()+"是否是守护线程:"+thread.isDaemon());}};t1.setPriority(3);t1.setDaemon(true);t1.start();Thread t2 = new Thread("father"){public void run(){//方法1:static Thread currentThread(); 获取所在线程的对象地址Thread thread = Thread.currentThread();for(int i=0; i<10; i++){System.out.println(i+"次该线程的名字"+thread.getName());}System.out.println(thread.getName()+"的id:"+thread.getId());System.out.println(thread.getName()+"的优先级:"+thread.getPriority());System.out.println(thread.getName()+"的状态:"+thread.getState());System.out.println(thread.getName()+"是否alive:"+thread.isAlive());System.out.println(thread.getName()+"是否中断:"+thread.isInterrupted());System.out.println(thread.getName()+"是否是守护线程:"+thread.isDaemon());}};t2.setPriority(10);t2.setDaemon(true);t2.start();//获取main线程的id值Thread main = Thread.currentThread();System.out.println("main线程的Id:"+main.getId());System.out.println("main线程的Id:"+main.getPriority());System.out.println("t1线程是否alive:"+t1.isAlive());try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("----------5秒后------------");System.out.println("t1线程的状态:"+t1.getState());System.out.println("t1线程是否alive:"+t1.isAlive());System.out.println("t1线程是否中断:"+t1.isInterrupted()); // 中断指的是线程睡眠时,没有到睡眠时间,而是中途被唤醒。System.out.println("main线程是否为守护线程:"+main.isDaemon());}
守护线程
守护线程与普通线程在表现上没有什么区别,我们只需要通过Thread提供的方法来设定即可:
- void setDaemon(boolean on)
当参数为true时该线程为守护线程
线程分为:
- 前台线程
- 守护线程(后台)
守护线程的特点是,当进程中只剩下守护线程时,所有守护线程强制终止(即使任务并没有完成)。GC就是运行在一个守护线程上的。
Thread rose = new Thread("rose"){public void run(){for(int i=1;i<=10;i++){System.out.println(this.getName()+"喊: I jump "+i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("--扑通一声..... jumped--");}};Thread jack = new Thread("jack"){public void run(){while(true){try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(this.getName()+"喊: no jump ");}}};//设置前后台线程jack.setDaemon(true);rose.setPriority(10);rose.start();jack.start();}
生命周期相关方法
Sleep方法
线程睡眠方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()方法平台移植性好。
注意:该方法 需要处理异常,因为可能在睡眠时间不到的情况下,被唤醒。
System.out.println("1");//让当前线程:main线程 阻塞5秒。try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("2");
yield()方法
线程让步方法,暂停当前正在执行的线程对象,使之处于可运行状态,把执行机会让给相同或者更高优先级的线程。
Thread t1 = new Thread("t1"){public void run(){for(int i=1;i<=10;i++){//拿到时间片段,就让一下Thread.yield();System.out.println(Thread.currentThread().getName()+":"+i);}}};Thread t2 = new Thread("t2"){public void run(){for(int i=1;i<=10;i++){System.out.println(Thread.currentThread().getName()+":"+i);}}};t1.start();t2.start();
join()方法
线程加入方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
某一个线程对象A想要加入到另外一个线程B中. 会造成线程B进入阻塞状态,
注意: 谁加入,谁调用
//设置:显示线程是在下载线程执行完后才开始 public static void main(String[] args) {Thread download = new Thread("下载图片"){@Overridepublic void run() {for (int i = 1; i <= 100; i++) {try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("正在下载:"+i+"%");}System.out.println("下载完毕");}};Thread show = new Thread("显示图片"){@Overridepublic void run() {//分析得出: 下载先执行,执行完后,显示才开始执行。因此是下载join到显示线程中try {download.join();} catch (InterruptedException e) {e.printStackTrace();}for (int i = 1; i <= 100; i++) {System.out.println("开始显示:"+i+"%");try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("显示完毕");}};//启动线程show.start();download.start();
interrupt()方法
线程打断方法。 打断哪个线程,就用哪个线程对象调用。
Thread linyongjian = new Thread("林永健"){@Overridepublic void run() {try {Thread.sleep(100000);} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName()+"喊:干嘛呢,干嘛呢");}}};Thread huanghong = new Thread("黄宏"){@Overridepublic void run() {int count = 0;for (int i = 0; i < 10; i++) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"喊:80,,,,,,,,"+(++count));}System.out.println(Thread.currentThread().getName()+"说:凿开了,给800就行");//谁调用该方法,就表示谁被打断了。linyongjian.interrupt();System.out.println(linyongjian.isInterrupted());}};linyongjian.start();huanghong.start();
有一个数组:{1,2,3,4,5,6,7,8}
有两个线程,分别计算1~4 和 5~8. 两个线程的计算结果,在main线程里进行累计,打印结果。每次睡眠1秒。
public class SumWithThread {private static int sum1 = 0;private static int sum2 = 0;public static void main(String[] args) throws InterruptedException {int[] array = {1, 2, 3, 4, 5, 6, 7, 8};Thread thread1 = new Thread(() -> {for (int i = 0; i < 4; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}sum1 += array[i];}});Thread thread2 = new Thread(() -> {for (int i = 4; i < 8; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}sum2 += array[i];}});long before = System.currentTimeMillis();thread1.start();thread2.start();//join,这两个线程运行完main才运行thread1.join();thread2.join();int totalSum = sum1 + sum2;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("总和为:" + totalSum);System.out.println("计算了" + (System.currentTimeMillis() - before)/1000 + "秒");} }
临界资源问题(重点)
临界资源:在一个进程中,多个线程之间是资源共享的。如果一个资源同时被多个线程访问,这个资源就是一个临界资源。
当多个线程并发读写同一个临界资源时,会发生“线程并发安全问题”。
常见的临界资源:
-
多线程共享实例变量
-
多线程共享静态公共变量
安全隐患:多个线程并发(异步)执行时,可能会出现访问同一个对象(对象的属性,对象的方法)/静态变量, 造成某一个线程访问的数据并不是他希望访问的数据。
解决方法:
1.两个线程不并发(异步),进行串行(同步)操作。但是效率非常低。因为多个线程之间有些逻辑(不会涉及到安全隐患问题的)是可以同时进行的(这部分串行会浪费时间)。
2.仅仅让具有安全隐患问题的那部分逻辑串行即可----------最优解
锁机制
java提供了一种内置的锁机制来支持原子性,通过关键字synchronized来进行同步代码块。
同步代码块包含两部分:
一个是充当锁的对象的引用
一个是由这个锁保护的代码块。
synchronized(同步监视器-锁对象引用){
//代码块
}
多个线程是很可能同时执行到synchronized关键字前的。
紧接着多个线程开始抢占小括号里的那个锁对象,谁抢到了谁就可以获取cpu分配的时间片段, 就可以执行代码块了,而其他线程处于锁池状态。
获取锁的那个线程执行完代码块后,会主动释放这个锁对象,其他线程才有机会继续抢这个锁对象。
每个Java对象都可以用作一个实现同步的锁。线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,无论是通过正常途径退出还是通过抛出异常退出都一样。获取内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
合适的锁对象
谁可以作为锁对象来应用:
-- 前提条件: 保证多个线程必须访问的是同一个锁对象
1. 非静态方法中,this可以作为锁对象,前提多个线程使用的是同一个对象。
2. 单例模式的对象。
3. Class对象: Class是描述类,Class的某一个对象是用来描述一类事物的。
合适的锁范围
在使用同步块时,应该尽量在允许的情况下减少同步范围,来提高并发的执行效率。
Synchronized关键字的作用域
作用域有两种:
第一种,同步方法内的部分代码块,或者全部代码块(相当于给方法直接加锁)
通常情况下锁对象都是this,此时多个线程访问此同步块时会进行同步操作。如果这个对象中有多个同步方法,只要其中一个线程正在访问某一个同步方法,那么其他线程就不能同时访问这个对象中的任何同步方法。注意,不同的对象实例的同步方法互不干扰。
第二种,同步静态方法
当我们对一个静态方法加锁时,该方法的锁对象是类对象。每个类都有唯一的一个类对象。获取类对象的方法为:类名.class
静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。原因就在于,静态方法的锁是类对象,而非静态方法的锁对象是当前方法所属的对象。
根据票务系统举个栗子:
public static void main(String[] args) {//创建一个票务中心对象PacketCenter pc = new PacketCenter(10);//创建两个窗口,来模拟两个线程进行售票Thread t1 = new Thread(pc,"窗口1");Thread t2 = new Thread(pc,"窗口2");t1.start();t2.start();} }/*** 票务中心*/ class PacketCenter implements Runnable {// 静态常量 可以充当锁public static final Object obj = new Object();public int packet ;public PacketCenter(int packet) {this.packet = packet;}/*** 如果同步的代码块是方法中的所有代码了,就可以简化其写法,将synchronized放在方法的定义结构上,可以省略锁对象。* 注意: 这些写法,锁对象就是this*/@Overridepublic synchronized void run() {while (packet != 0) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}packet--;System.out.println(Thread.currentThread().getName()+"卖出一张,剩余数量:"+packet);}} // @Override // public void run() { // // 前置代码有50行 // // 两个窗口访问都是同一个票务中心,因此可以使用this来充当锁。 // synchronized (this){ // while (packet != 0) { // // 当票数剩下1张的时候,两个线程的时间片段用完,刚好都执行完判断条件 packet !=0 // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } // packet--; // 窗口1这个线程先获取时间片段,执行-1操作,此时票数变成了0, 然后窗口2获取时间片段也执行了-1操作,因此是在0的基础上-1, // //变成了负1. 因此可能造成死循环 ,这样,临界资源安全隐患问题就出现了。 // System.out.println(Thread.currentThread().getName()+"卖出一张,剩余数量:"+packet); // } // } // // 后置代码有50行 // }
单例模式的改进
单例模式的饿汉模式不会出现安全隐患问题:
创建的对象在加载时只创建一次,是唯一的一个对象。
懒汉模式才会出现这个问题:
多线程调用方法时可能创建得到不同的对象。
class Chairman {private Chairman() {System.out.println("一个 Chairman 对象被实例化了 ");}private static Chairman Instance = null;//当同步关键字写在静态方法时,该类的描述对象是锁。(Class)public static Chairman getInstance() {synchronized ("") {//字符串常量 只有一个可以当锁if (Instance == null) {//多线程的情况下,很可能出现多个线程使用完自己的时间片段后,恰好已经执行完了==null这个判断。//下一步再次获取时间片段是,每个线程都可能创建了Boss这个对象,就不是单例。Instance = new Chairman();}}return Instance;} }
死锁
死锁是指两个或多个线程无限期地等待对方持有的资源而导致的一种阻塞现象。 在 Java多线程编程中,死锁通常是由于多个线程在竞争资源时出现了相互等待的情况,导致所有线程都无法继续执行,从而产生死锁。
在深入了解死锁的成因之前,需要先了解死锁发生的必要条件,包括以下四个条件:
-
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
-
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
-
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
-
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
只有当这四个条件同时满足时,才可能发生死锁。如果其中任何一个条件不满足,就不会发生死锁。因此,避免死锁的关键是破坏这四个条件中的至少一个。
例如,可以通过使用同步机制来破坏请求和保持条件,避免进程在请求资源时阻塞并持有已获得的资源。同时,可以使用超时机制来破坏不剥夺条件,避免进程长时间持有资源而不释放。另外,可以使用资源分配图等工具来检测循环等待条件,从而避免出现循环等待的情况。
综上所述,了解死锁的必要条件对于理解死锁的成因和避免死锁非常重要。只有在避免或破坏这些必要条件的基础上,才能有效地避免死锁的发生。
Java多线程中产生死锁的主要原因
1)竞争同一把锁时发生死锁
如果一个线程对同一把锁,连续加了两次锁,并且该锁还是不可重入锁(默认是可重入锁,测试时需要定义为不可重入锁)的时候,就会产生死锁。
private static final NonReentrantLock lock = new NonReentrantLock();public static void main(String[] args) {new Thread(() -> {try {lock.lock();System.out.println("第一个线程:获取了锁");lock.lock(); // 尝试再次获取锁,会导致死锁System.out.println("------");lock.unlock();lock.unlock();} catch (InterruptedException e) {e.printStackTrace();}}).start();}static class NonReentrantLock {private boolean isLocked = false;public synchronized void lock() throws InterruptedException {while (isLocked) {wait();}isLocked = true;}public synchronized void unlock() {isLocked = false;notify();}}
2)多个锁的嵌套导致死锁
在 Java 中,如果多个线程在持有一个锁的情况下尝试获取另一个锁,并且在相互等待对方释放锁时,就会出现死锁。这种情况通常是由于多个线程在获取锁的顺序上存在差异,从而导致相互等待。
public static void main(String[] args) {new Thread(){public void run(){synchronized ("A"){for (int i = 0; i < 10; i++){System.out.println("第一个线程:执行外层循环");try {Thread.sleep(1000);if(i == 5){System.out.println("第一个线程:获取了锁A,等待锁B");synchronized ("B"){for (int j = 0; j < 10; j++) {Thread.sleep(1000);}}}} catch (InterruptedException e) {e.printStackTrace();}}}}}.start();new Thread(){public void run(){synchronized ("A"){for (int i = 0; i < 10; i++){System.out.println("第二个线程:执行外层循环");try {Thread.sleep(1000);if(i == 5){System.out.println("第二个线程:获取了锁B,等待锁A");synchronized ("B"){for (int j = 0; j < 10; j++) {System.out.println("第二个线程:执行内层循环");Thread.sleep(1000);}}}} catch (InterruptedException e) {e.printStackTrace();}}}}}.start();}
死锁的解决方案(重点)
- 避免使用多把锁
使用多把锁会增加死锁的概率,因此应该尽量避免使用多把锁。可以考虑使用更高级别的同步工具,例如信号量、读写锁、并发集合等。
- 避免嵌套锁
在持有一个锁的情况下,尽量避免获取其他锁,尤其是嵌套锁。如果确实需要嵌套锁,可以考虑使用线程本地变量或者其他的同步工具来避免死锁。
- 统一锁的获取顺序
在多线程中使用多把锁时,为了避免死锁,应该尽量保证所有线程获取锁的顺序是一致的。可以按照某种全局的规则来确定锁的获取顺序,例如按照对象的 hash 值来获取锁。
- 限制锁的持有时间
持有锁的时间越长,发生死锁的概率就越大。因此,可以考虑限制锁的持有时间,避免线程长时间持有锁而导致其他线程无法获取锁的情况。
- 超时等待锁
如果一个线程尝试获取锁时发现已经被其他线程占用,可以设置一个超时时间,超过这个时间就放弃获取锁。这样可以避免线程一直阻塞等待锁而导致死锁。
- 破除循环等待
--按顺序获取资源
按顺序获取资源是一种比较常见的破除循环等待的方法。如果所有的线程都按照固定的顺序获取资源,那么就不会出现循环等待的情况。
--统一资源分配器
统一资源分配器是一种能够有效避免死锁的方法。在这种方法中,所有的资源都由一个统一的资源分配器来进行分配和释放,每个线程在需要资源时向资源分配器发出请求,资源分配器根据当前的情况来分配资源。这样就能够避免循环等待和其他死锁问题的发生。
- 检测死锁
可以定期检测系统中是否存在死锁,并且采取相应的措施来解决死锁问题。例如,可以使用 jstack 工具来查看死锁情况,或者使用死锁检测算法来自动检测死锁。
锁的常见API
Object类中有几个方法如下 :
wait()
等待,让当前的线程,释放自己持有的指定的锁标记,进入到等待队列。(使用锁对象来调用该方法)
等待队列中的线程,不参与CPU时间片的争抢,也不参与锁标记的争抢。
wait(long millins): 有限等待时间,时间到了还没被唤醒,则自动醒来(拥有锁后会继续执行,没有锁依然不会执行)。如果传入了0,则表示一直等待,直到被唤醒。
notify()
通知、唤醒。唤醒等待队列中的第一个线程(官方的JVM是这样的,阿里也有自己的JVM,有可能是随机的)。
被唤醒的线程,进入到锁池,开始争抢锁标记。
notifyAll()
通知、唤醒。唤醒等待队列中所有的等待线程,他们从等待队列中出来,进入锁池状态,此时所有线程都有机会获取时间片段来运行。
被唤醒的线程,进入到锁池,开始争抢锁标记。
如果等待队列中没有线程,则调用notifyAll()方法没有任何效果。
等待队列: 就是用来存储哪些线程处于等待阻塞状态。(先进先出,先进后出,两端都可以出)
private static final Object obj = new Object();//定义锁对象public static void main(String[] args) {Thread download = new Thread("下载图片"){@Overridepublic void run() {synchronized (obj){System.out.println("---download先拥有的---");for (int i = 1; i < 101; i++) {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("正在下载"+i+"%");}System.out.println("下载完毕");// 任务执行完毕,要主动释放锁,并通知一下等待队列中的一个线程obj.notify();}}};Thread show = new Thread("显示图片"){@Overridepublic void run() {synchronized (obj){System.out.println("---show先拥有的---");//获取到锁之后,先让下载线程先执行,下载完毕后,被通知时,继续执行后续代码try {//此时,应该使用带参数的wait方法,因为一旦download先获取锁,当前线程可能再也不能被唤醒了,可以自动醒来。obj.wait(100);} catch (InterruptedException e) {e.printStackTrace();}for (int i = 1; i < 101; i++) {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("正在显示"+i+"%");}System.out.println("显示完毕");}}};show.start();download.start();}
案例分析:
只有两种情况:第一种:
show 先获取锁,然后释放锁
download先处于等待队列中,然后抢到了show释放的锁。 download执行完任务前,show不得不等待。download执行完任务后,
show才有机会继续获取锁,然后执行自己的任务。第二种:
download先获取锁,此时show处于等待队列中,然后download执行完任务后,调用了nofity方法,释放锁,并通知show。然后show获取到锁,执行自己的任务。紧接着show因为wait方法立马释放锁,进入等待队列中,然后一直等,直到被唤醒,
但是没有机会了,因为没有其他线程再调用notify方法了。(所以需要带参数的wait()方法,让他超过时间自动醒来,不需要别人唤醒,否则show会一直至死不渝的等下去,泪目了)
wait()和sleep()的区别
sleep()方法,在休眠时间结束后,会自动的被唤醒。
而wait()进入到的阻塞态,需要被 notify/notifyAll手动唤醒。
wait()会释放自己持有的指定的锁标记,进入到阻塞态。
sleep()进入到阻塞态的时候,不会释 放自己持有的锁标记。
无论是wait()方法,还是notity()/notifyAll()方法,在使用的时候要注意,一定要是自己持有的 锁标记,才可以做这个操作。否则会出现 IllegalMonitorStateException 异常。
ReentrantLock可重入锁
ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中的一个可重入锁实现,它提供了比 synchronized 关键字更灵活、功能更丰富的线程同步机制。ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync(非公平锁)与FairSync(公平锁)类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
可重入性
ReentrantLock 是一种可重入锁。这意味着持有锁的线程可以再次获取该锁,而不会发生死锁。每次成功获取锁都会增加锁的持有计数,相应的释放锁操作会减少计数。当计数降至零时,锁才会真正释放给其他等待的线程。这种特性使得在递归调用或嵌套同步块中使用同一线程多次获取同一锁成为可能。
公平性
ReentrantLock 默认实现的是非公平锁。同时,ReentrantLock 提供了公平锁和非公平锁两种模式。在构造时可以通过传递布尔参数指定:
- 公平锁(true):
按照线程请求锁的顺序进行排队,先请求的线程优先获得锁。公平锁倾向于减少线程饥饿现象,但可能降低系统的整体吞吐量。
- 非公平锁(默认 false):
不保证按照线程请求锁的顺序分配锁,允许后来的线程“插队”获取锁。非公平锁在某些场景下可能提供更高的性能,但可能增加线程饥饿的风险。
显式锁操作
与 synchronized 关键字(隐式锁)不同,ReentrantLock 需要显式地调用方法来获取和释放锁:
-
lock():上锁方法,如果发现已经上锁,当前线程将被阻塞,直到获取锁
-
unlock():解锁方法。必须确保在持有锁的线程中正确调用此方法,否则可能导致死锁或其他同步问题。
-
tryLock():尝试非阻塞地获取锁。如果锁可用,立即返回 true;否则返回 false。
-
tryLock(long timeout, TimeUnit unit):尝试在指定时间内获取锁。如果在超时时间内锁不可用,返回 false。
条件变量(Condition)
ReentrantLock 还支持条件变量,通过 newCondition() 方法创建 Condition 对象。条件变量允许线程在满足特定条件时等待,直到其他线程通知它们条件已发生变化。与 Object 类的 wait()、notify() 和 notifyAll() 方法相比,条件变量提供了更精细的线程同步控制:
-
await():当前线程进入等待状态,释放锁,并在其他线程调用对应 Condition 对象的 signal() 或 signalAll() 方法时唤醒
-
signal():唤醒一个等待在该 Condition 上的线程,但不释放锁。
-
signalAll():唤醒所有等待在该 Condition 上的线程,但不释放锁。
public static void main(String[] args) {//定义一个可重入锁对象ReentrantLock rl = new ReentrantLock();Runnable task = () -> {// 上锁rl.lock();try {String[] strs = {null, "A", "B"};System.out.println(strs[(int) (Math.random() * 3)].length());for (int i = 0; i < 10; i++) {Thread.sleep(50);System.out.println(Thread.currentThread().getName() + "," + i);}} catch (Exception e) {e.printStackTrace();} finally {//释放锁rl.unlock();}};for (int i = 0; i < 4; i++) {new Thread(task, "线程" + i).start();}}
栗子:
购票中心有一些票
多个线程开始买票,有的买一张,有的买两张:
public class ReentrantDemo02 {public static void main(String[] args) {TicketCenter tc = new TicketCenter(100);new Thread("小红"){@Overridepublic void run() {tc.sellTicketOne();}}.start();new Thread("小明"){@Overridepublic void run() {try {tc.sellTicketMore(30);} catch (InterruptedException e) {throw new RuntimeException(e);}}}.start();new Thread("小张"){@Overridepublic void run() {try {tc.sellTicketMore(80);} catch (InterruptedException e) {throw new RuntimeException(e);}}}.start();new Thread("小王"){@Overridepublic void run() {try {tc.sellTicketMore(60);} catch (InterruptedException e) {throw new RuntimeException(e);}}}.start();} } class TicketCenter {private ReentrantLock rl = new ReentrantLock(true);private int tickets;public TicketCenter(int tickets) {this.tickets = tickets;}// 出售一张票的行为public void sellTicketOne() {if(rl.tryLock()){if (tickets <=0 ) {System.out.println(Thread.currentThread().getName()+"发现票不够,无法购买");return;}tickets --;try {Thread.sleep(6000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+"买了1张票,剩余票数为" + tickets);rl.unlock();}else{System.out.println(Thread.currentThread().getName()+"走了,干其他活去了,");}}// 出售多张票的行为public void sellTicketMore(int num) throws InterruptedException {if(rl.tryLock(10, TimeUnit.SECONDS)){if (tickets < num) {System.out.println(Thread.currentThread().getName()+"购买的数量"+num+"票不够,无法出售");rl.unlock();return;}try {Thread.sleep(8000);} catch (InterruptedException e) {throw new RuntimeException(e);}tickets -= num;System.out.println(Thread.currentThread().getName()+"买了"+num+"张票,剩余票数为" + tickets);rl.unlock();}else{System.out.println(Thread.currentThread().getName()+"走了,干其他活去了,");// ......}} }
ReentrantLock和synchronized有什么不同
使用方法上:
1. ReentrantLock可以提供公平和非公平两种特性
synchronized无法提供
2. ReentrantLock需要手动加锁,解锁
synchronized可以避免加锁后忘记解锁这个问题
( 当代码执行到synchronized修饰的代码块的时候,如果在同步代码块内部发生了异常,没有及时处理的话,会提前退出并且让线程释放锁。而ReentrantLock无法做到立刻解锁,因此,unLock()的解锁操作一定要在finally代码块当中,避免加锁之后忘记解锁的情况。 )
3.synchronized无法提供lock.tryLock()这样尝试获取锁的特性
ReentrantLock可以提供这个方法
(线程如果在指定的时间之内无法获取到锁,或者锁已经被占用了,那么lock.tryLock()可以有效减少线程阻塞等待的情况,或者减少阻塞等待的时间。
而synchronized只会让无法获取到锁的线程"死等"。直到获取到锁的线程释放锁)
4.ReentrantLock可以提供中断式加锁
ReentrantLock在调用lock.lockInterruptibly()时候,可以让获取不到锁,进入阻塞等待的线程被提前"唤醒",但是synchronized不可以。具体的操作已经在上面解释了。
生产者消费者模式
生产者-消费者模型(Producer-Consumer problem)是一个非常经典的多线程并发协作的模型。
比如某个模块负责生产数据,而另一个模块负责处理数据。产生数据的模块就形象地被称为生产者;而处理数据的模块,则被称为消费者。
生产者和消费者在同一段时间内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
优点
1、解耦
由于有缓冲区的存在,生产者和消费者之间不直接依赖,耦合度降低。
2、支持并发
由于生产者与消费者是两个独立的并发体,它们之间是通过缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区中拿数据接口,这样就不会因为彼此的处理速度而发生阻塞。(通过使用多个生产者和消费者线程,可以实现并发处理,提高系统的吞吐量和响应性)
3、支持忙闲不均
缓冲区还有另一个好处:当数据生产过快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等消费者处理掉其他数据时,再从缓存区中取数据来处理。(通过使用缓冲区可以平衡生产者与消费者之间的速度差异,以及处理能力的不匹配)
规则
-
生产者仅仅在缓冲区未满时生产,缓冲区满则停止生产。
-
消费者仅仅在缓冲区有产品时才能消费,缓冲区为空则停止消费。
-
当消费者发现缓冲区没有可消费的产品时会通知生产者。
-
当生产者生产出可消费的产品时,应该通知等待的消费者去消费。
package com.youcai;import java.util.ArrayList; import java.util.List;/*** 生产者和消费者模式:* 1. 存在三个角色: 生产者,消费者,缓冲区** -. 生产者仅仅在缓冲区未满时生产,缓冲区满则停止生产。* -. 消费者仅仅在缓冲区有产品时才能消费,缓冲区为空则停止消费。* -. 当消费者发现缓冲区没有可消费的产品时会通知生产者。* -. 当生产者生产出可消费的产品时,应该通知等待的消费者去消费。*/ public class _05ProduceCousumerDemo {public static void main(String[] args) {//定义一个水果摊对象FruitPlatForm fruitPlatForm = new FruitPlatForm(100);//创建8个生产者对象new Producer("生产者1", fruitPlatForm, 10).start();new Producer("生产者2", fruitPlatForm, 50).start();new Producer("生产者3", fruitPlatForm, 30).start();new Producer("生产者4", fruitPlatForm, 60).start();new Producer("生产者5", fruitPlatForm, 20).start();new Producer("生产者6", fruitPlatForm, 70).start();new Producer("生产者7", fruitPlatForm, 30).start();new Producer("生产者8", fruitPlatForm, 80).start();//创建7个消费者对象new Consumer("消费者1", fruitPlatForm, 40).start();new Consumer("消费者2", fruitPlatForm, 50).start();new Consumer("消费者3", fruitPlatForm, 60).start();new Consumer("消费者4", fruitPlatForm, 70).start();new Consumer("消费者5", fruitPlatForm, 40).start();new Consumer("消费者6", fruitPlatForm, 50).start(); // new Consumer("消费者7", fruitPlatForm, 60).start();} } // 苹果类型 class Apple{} /*水果摊 */ class FruitPlatForm{private List<Apple> apples; //用来存储水果的容器private int capacity; //最大容量public FruitPlatForm(int capacity){this.apples = new ArrayList<>();this.capacity = capacity;}/*** 水果摊,应该有存储的行为: 供生产者调用*/public void produce(int count) {synchronized (this){while(count+ apples.size() > capacity){System.out.println(Thread.currentThread().getName()+"想要生产"+count+"个苹果, 库存充足,不用生产");try {//生产不了,进入阻塞,等待被唤醒this.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 可以生产,即向集合中添加count个Applefor (int i = 0; i < count; i++) {apples.add(new Apple());}//通知消费者,可以购买了(连带着通知了其他生产者)this.notifyAll();System.out.println(Thread.currentThread().getName()+"生产了"+count+"个苹果, 水果摊上共有"+apples.size()+"个苹果");}}/*** 水果摊,应该有出售的行为: 供消费者调用*/public void sell(int count){synchronized (this){while(apples.size() < count){System.out.println(Thread.currentThread().getName()+"想要购买"+count+"个苹果, 库存不足,购买不了");try {//然后自己再进入阻塞状态this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 可以出售,即从集合中移除count个Apple// [10,20,30,40,50] 5个 4个// 0 1for (int i= 0; i<count; i++) {apples.remove(apples.size()-1);}//购买完成后,通知其他生产者。(其他消费者连带通知了)this.notifyAll();System.out.println(Thread.currentThread().getName()+"购买了"+count+"个苹果, 水果摊上共有"+apples.size()+"个苹果");}} } class Producer extends Thread{private String name;private FruitPlatForm fruitPlatForm;private int count;public Producer(String name, FruitPlatForm fruitPlatForm, int count){//将生产者的名字作为线程的名字super(name);this.fruitPlatForm = fruitPlatForm;this.count = count;}// 生产功能public void produce(){fruitPlatForm.produce(count);}public void run(){produce();} }class Consumer extends Thread{private String name;private FruitPlatForm fruitPlatForm;private int count;public Consumer(String name, FruitPlatForm fruitPlatForm, int count){//将生产者的名字作为线程的名字super(name);this.fruitPlatForm = fruitPlatForm;this.count = count;}// 生产功能public void consume(){fruitPlatForm.sell(count);}public void run(){consume();} }