Java学习之线程锁

news2024/12/23 3:59:40

一、多线程

对于多线程的概念,其实很容易理解,之前我们的学习中无论多长或者怎样的代码,都是线性执行的,也就是很显而易见的自上而下执行,这也是所有语言中最常见的执行方式,那么这种执行方式有什么弊端呢?或者说有什么可以优化的点呢?

在一些较为复杂的场景里,实际上这种线性执行并不需要,举个例子:

当我们在实际应用中想要获取用户的个人信息时,可能即要用户的用户名和头像,还有用户的手机号和地址等信息,一些而用户名和头像是非隐私信息,我们可以存在一起,而手机等隐私信息显然我们需要用更加保密的方式存,既然不是存在一起的,那么线性获取就有弊端。

为什么不能同时获取呢?假设获取用户名和头像需要50ms,手机号需要50ms,同时获取仅需50ms,但是线性获取就需要100ms。

所以我们需要多线程!也就是不同的事情同时干

注:

可能学习之前我已经了解过线程之类的知识,还多了解了一个叫进程的东西,这两个实际上很有区别。

1、进程和线程的区别

进程:正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。进程是正在运行的程序,负责给程序分配内存空间,而每一个进程都是由程序代码组成的,这些代码在进程中执行的流程就是线程
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程,但至少有一个线程

2、主线程

必然有一个执行路径(线程)从main方法开始的。一直执行到main方法结束。这个线程在java中称之为主线程。当主线程在这个程序中执行时,如果遇到了循环而导致程序在指定位置停留时间过长,无法执行下面的程序。
多线程可以解决一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行。

1、创建线程方式

1.1、继承Thread类

创建线程的步骤:

  •  定义一个类继承Thread。
  •  重写run()方法。
  •  创建子类对象,就是创建线程对象。
  •  调用start()方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
class Demo extends Thread  //继承Thread
{
	String name;
	Demo(String name)
	{
		this.name = name;
	}
	//复写其中的run方法
	public void run()
	{
		for (int i=1;i<=20 ;i++ )
		{
			System.out.println("name="+name+",i="+i);
		}
	}
}
class ThreadDemo 
{
	public static void main(String[] args) 
	{
		//创建两个线程任务
		Demo d = new Demo("小强");
		Demo d2 = new Demo("旺财");
		//d.run(); 这里仍然是主线程在调用run方法,并没有开启两个线程
		//d2.run();
		d2.start();//开启一个线程
		d.start();//主线程在调用run方法
	}
}

继承Thread类原理

建立单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定的代码(线程的任务)。对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类中的run方法内部的任务并不是我们所需要,只有重写这个run方法,既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可。所以进行了重写run方法动作。

 1.2、实现Runnable接口

创建线程的第二种方式:实现Runnable接口。

  1. 定义类实现Runnable接口。
  2. 覆盖接口中的run方法。
  3. 创建Thread类的对象。
  4. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
  5. 调用Thread类的start方法开启线程。
class Demo implements Runnable{
	private String name;
	Demo(String name){
		this.name = name;
	}
	//覆盖了接口Runnable中的run方法。
	public void run(){
		for(int i=1; i<=20; i++){
            System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
		}
	}
}
class ThreadDemo2 {
	public static void main(String[] args) {
		//创建Runnable子类的对象。注意它并不是线程对象。
		Demo d = new Demo("Demo");
		//创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		//将线程启动。
		t1.start();
		t2.start();
		System.out.println(Thread.currentThread().getName()+"----->");
		System.out.println("Hello World!");
	}
}

实现Runnable的原理

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处

实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。
实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

2、解析

2.1、多线程内存图解

在这里插入图片描述
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了

2.2、获取线程名称

Thread.currentThread()获取当前线程对象

Thread.currentThread().getName();获取当前线程对象的名称

2.3、多线程的异常信息

当主线程执行完成了,并不代表程序就结束,如果此时还有其他线程正常执行,程序仍然在执行过程中。

当任何一个线程出现了异常,其他线程还是会继续运行的。异常只会影响到异常所属的那个线程。

 

3、线程的安全问题

问题产生的原因:

  1. 线程任务中可能在操作共享的数据。
  2. 线程任务操作共享数据的代码有多条(运算有多个)。(共享资源弊端隐患)

解决共享的资源

只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,在执行过程中,不要让其他线程参与运算。就可以解决这个问题。
解决这个问题Java中给我们提供相应的独立代码块,这段代码块需要使用关键字synchronized来标识其为一个同步代码块

二、synchronized 

1、同步

synchronized关键字在使用时需要一个对象作为标记,当任何线程进入synchronized标识的这段代码时,首先都会先判断目前有没有线程正在使用synchronized标记对象,若有线程正在使用这个标记对象, 那么当前这个线程就在synchronized标识的外面等待,直到获取到这个标记对象后,这个线程才能执行同步代码块

例如:

class Ticket implements Runnable{
	//1、描述票的数量。
	private int tickets = 100;
	//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
	//定义同步代码块的标记对象。相当与锁的功能
	private Object obj = new Object();
	public void run(){
		//线程任务中通常都有循环结构。
		while(true){
			//使用同步代码块解决线程安全问题。
			synchronized(obj){
                //由于run方法是复写接口中的,run方法没有抛出异常, 此时这里只能捕获异常,而不能抛出
				if(tickets>0){	
					//让线程在此冻结10毫秒
					try{
						    Thread.sleep(10);
						}catch(InterruptedException e){/*异常处理代码*/}
					        System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。
				    }
			}
		}
	}
}
class ThreadDemo3 {
	public static void main(String[] args) {
		//1,创建Runnable接口的子类对象。
		Ticket t = new Ticket();

		//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);

		//3,开启四个线程。
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

这样一来,每一个线程操作票数的时候,其他线程是不能操作的,就不会引起负票或某一张票卖多次的情况。

1、同步的好处和弊端

同步好处:解决多线程安全问题。这里举例(火车上的卫生间)说明同步锁机制。

同步弊端:降低了程序的性能。每个线程都要去判断锁机制,那么会增加程序运行的负担,同时只要做判断,CPU都要处理,那么也会消耗CPU的资源。即就是加同步会降低程序的性能。

2、同步的前提

多个线程操作了共享数据,并且操作共享数据的代码有多句,必须使用同步代码块来解决。

当线程任务代码只会有一个线程执行时,加不加同步都可以。
当线程任务代码会有被多个线程执行时,这时需要加同步,但是加同步时一定要保证多个线程使用的是同一把锁

同步的前提:必须保证多个线程在同步中使用的是同一个锁

 3、同步代码块使用的锁

同步代码块使用的锁可以是任意对象的。因为synchronized中的对象可以我们自己指定。

4、同步函数锁

函数是需要对象去调用的,则同步函数中使用的锁就是当前调用这个方法的对象。this

5、同步代码块和同步函数的区别

  1. 同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
  2. 同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用同步代码块。同步代码块较为常用。
  3. 同理,若是继续向上,若是使用静态同步函数锁,就得是class文件对象

sleep和wait的区别:

相同点:可以让线程处于冻结状
不同点

  1. sleep必须指定时间;wait可以指定时间,也可以不指定时间。
  2. sleep时间到,线程处于临时阻塞或者运行;wait如果没有时间,必须要通过notify或者notifyAll唤醒 一般放在逻辑的最后。
  3. sleep不一定非要定义在同步中wait必须定义在同步中
  4. sleep和wait都定义在同步中时,线程执行到sleep,不会释放锁。线程执行到wait,会释放锁。

2、死锁

当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:死锁。这种情况能避免就避免掉。

死锁代码(面试)

class Test implements Runnable{
    private boolean flag ;
    Test(boolean flag){
        this.flag = flag;
    }
    public void run(){
        if(flag){
            synchronized(MyLock.LOCKA){
                System.out.println(Thread.currentThread().getName()+"...if...MyLock.LOCKA");
                synchronized(MyLock.LOCKB){
                    System.out.println(Thread.currentThread().getName()+"...if...MyLock.LOCKB");
                }
            }
        }
        else {
            synchronized(MyLock.LOCKB) {
                System.out.println(Thread.currentThread().getName()+"...if...MyLock.LOCKB");
                synchronized(MyLock.LOCKA) {
                    System.out.println(Thread.currentThread().getName()+"...if...MyLock.LOCKA");
                }
            }
        }

    }
}
//单独描述锁对象
class MyLock {
    public static final MyLock LOCKA = new MyLock();
    public static final MyLock LOCKB = new MyLock();
}
class DeadThread {
    public static void main(String[] args) {
        Test t1 = new Test(true);
        Test t2 = new Test(false);
        Thread t11 = new Thread(t1);
        Thread t22 = new Thread(t2);
        t11.start();
        t22.start();
    }
}

 

2.1、死锁产生的条件

1、竞争不可抢占性资源

如上述例子中,LockA想去打开LockB,LockB又想去打开LockA,但是LockA和LockB都是不可抢占的,这是发生死锁。

2、竞争可消耗资源引起死锁

进程间通信,如果顺序不当,会产生死锁,比如p1发消息m1给p2,p1接收p3的消息m3,p2接收p1的m1,发m2给p3,p3,以此类推,如果进程之间是先发信息的那么可以完成通信,但是如果是先接收信息就会产生死锁。

3、进程推进顺序不当

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。
  
死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 1、互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 2、不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 3、请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 4、循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。

2.2、避免死锁的方法

1、破坏“请求和保持”条件:

想办法,让进程不要那么贪心,自己已经有了资源就不要去竞争那些不可抢占的资源。

比如,让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。

不过这个方法比较浪费资源,进程可能经常处于饥饿状态;还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。

2、破坏“不可抢占”条件:

允许进程进行抢占。

方法一:如果去抢资源,被拒绝,就释放自己的资源。

方法二:操作系统允许抢,只要你优先级大,可以抢到。

3、破坏“循环等待”条件:

将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。

死锁解决方法:

在有些情况下死锁是可以避免的。以下是避免死锁的技术:

  • 1、加锁顺序(线程按照一定的顺序加锁)
  • 2、加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 3、死锁检测
    • 步骤一:每个进程、每个资源制定唯一编号
    • 步骤二:设定一张资源分配表,记录各进程与占用资源之间的关系
    • 步骤三:设置一张进程等待表,记录各进程与要申请资源之间的关系

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1315865.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CLEARTEXT communication not enabled for client

CLEARTEXT communication not enabled for client 把BaseHttpClient中的supportTsl修改为false

Python生成器(Generator)的应用场景和使用(继续更新...)

学习网页&#xff1a; Welcome to Python.orghttps://www.python.org/https://www.python.org/ Python生成器&#xff08;Generator&#xff09; 生成器在Python中有很多应用场景&#xff0c;以下是一些常见的应用场景&#xff1a; 处理大数据量和耗时操作的场景&#xff…

魔众文库系统v5.7.0版本文件顺序选择,短信注册支持设置密码,前端界面升级

文件顺序选择&#xff0c;短信注册支持设置密码&#xff0c;前端界面升级 [新功能] 富文本支持文档一键导入&#xff0c;支持Word文档&#xff08;docx&#xff09;、Markdown文档&#xff08;md&#xff09; [新功能] 财务中心→全部订单新增"业务订单ID"筛选条件…

C++相关闲碎记录(15)

1、string字符串 #include <iostream> #include <string> using namespace std;int main (int argc, char** argv) {const string delims(" \t,.;");string line;// for every line read successfullywhile (getline(cin,line)) {string::size_type beg…

布局前沿技术,紫光展锐推动6G创新融合发展

随着5G进入规模化商用阶段&#xff0c;6G研究已在全球范围内拉开帷幕。2023年6月&#xff0c;ITU发布了《IMT面向2030及未来发展的框架和总体目标建议书》&#xff0c;在升级5G三大应用场景的同时&#xff0c;扩展出三个跨领域场景&#xff0c;形成6G的六大应用场景&#xff0c…

读书心得(内容取自高质量C/C++编程)

版式虽然不会影响程序的功能&#xff0c;但会影响可读性。程序的版式追求清晰、美观&#xff0c;是 程序风格的重要构成因素。 可以把程序的版式比喻为“书法”。好的“书法”可让人对程序一目了然&#xff0c;看得兴致勃勃。差的程序“书法”如螃蟹爬行&#xff0c;让人看得…

C++实现简单的猜数字小游戏

猜数字 小游戏介绍&#xff1a;猜数字游戏是令游戏机随机产生一个100以内的正整数&#xff0c;用户输入一个数对其进行猜测&#xff0c;需要你编写程序自动对其与随机产生的被猜数进行比较&#xff0c;并提示大了&#xff0c;还是小了&#xff0c;相等表示猜到了。如果猜到&…

音频DAC,ADC,CODEC的选型分析,高性能立体声

想要让模拟信号和数字信号顺利“交往”&#xff0c;就需要一座像“鹊桥”一样的中介&#xff0c;将两种不同的语言转变成统一的语言&#xff0c;消除无语言障碍。这座鹊桥就是转换器芯片&#xff0c;也就是ADC芯片。ADC芯片的全称是Analog-to-Digital Converter, 即模拟数字转换…

TCPIP介绍

可见 TCP/IP 被分为 4 层&#xff0c;每层承担的任务不一样&#xff0c;各层的协议的工作方式也不一样&#xff0c;每层封装上层数据的方式也不一样&#xff1a; 应用层&#xff1a;应用程序通过这一层访问网络&#xff0c;常见 FTP、HTTP、DNS 和 TELNET 协议&#xff1b; 传输…

【C语言】详解文件操作

&#xff08;零&#xff09;引入 终端是计算机系统中与用户进行交互的界面。 在以往的程序中&#xff0c;我们通过终端用键盘输入数据&#xff0c;通过屏幕输出信息。 但是&#xff0c;如果我们不想手动低效地输入数据&#xff0c;而是通过文件一次性高效输入&#xff1b; 如果…

OpenHarmony 如何去除系统锁屏应用

前言 OpenHarmony源码版本&#xff1a;4.0release / 3.2 release 开发板&#xff1a;DAYU / rk3568 一、3.2版本去除锁屏应用 在源码根目录下&#xff1a;productdefine/common/inherit/rich.json 中删除screenlock_mgr组件的编译配置&#xff0c;在rich.json文件中搜索th…

GraphicsProfiler 使用教程

GraphicsProfiler 使用教程 1.工具简介&#xff1a;2.Navigation介绍2.1.打开安装好的Graphics Profiler。2.2.将手机连接到计算机&#xff0c;软件会在手机中安装一个GraphicsProfiler应用(该应用是无界面的&#xff09;。2.3.Show files list2.4.Record new trace2.4.1.Appli…

自然数分解 C语言xdoj64

输入说明 一个正整数 n&#xff0c;0<n<30 输出说明 输出n个连续奇数&#xff0c;数据之间用空格隔开&#xff0c;并换行 输入样例 4 输出样例 13 15 17 19 int main() {int n;scanf("%d",&n);if(n % 2 0){//n为偶数int in;//打印数字个数&#xff0c;做循…

【每日一题】统计区间中的整数数目

文章目录 Tag题目来源解题思路方法一&#xff1a;平衡二叉搜索树 写在最后 Tag 【平衡二叉搜索树】【设计类】【2023-12-16】 题目来源 2276. 统计区间中的整数数目 解题思路 方法一&#xff1a;平衡二叉搜索树 思路 用一棵平衡二叉搜索树维护插入的区间&#xff0c;树中的…

Java-----链表练习题(上)

本篇碎碎念&#xff1a;本篇无碎碎念 今日份励志文案: 很多人认为他们在思考&#xff0c;其实他们只是在整理自己的偏见 目录 一.203. 移除链表元素 - 力扣&#xff08;LeetCode&#xff09; 二.21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09…

【STM32入门】4.2对射红外传感器计次

1.接线方式 主要是编写传感器的驱动、配合OLED&#xff0c;每遮挡对射红外传感器&#xff0c;OLED屏幕的计数就加一。 2.驱动编写 首先新建.c文件和.h文件&#xff0c;命名为CountSensor 国际惯例&#xff0c;.c文件内要包含stm32.h头文件&#xff0c;然后编写 CountSensor_…

C++初阶-list类的模拟实现

list类的模拟实现 一、基本框架1.1 节点类1.2 迭代器类1.3 list类 二、构造函数和析构函数2.1 构造函数2.2 析构函数 三、operator的重载和拷贝构造3.1 operator的重载3.2 拷贝构造 四、迭代器的实现4.1 迭代器类中的各种操作4.1 list类中的迭代器 五、list的增容和删除5.1 尾插…

力扣第2题-判断一个数值是否是回文数[简单]

题目描述 给你一个整数 x &#xff0c;如果 x 是一个回文整数&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 回文数是指正序&#xff08;从左向右&#xff09;和倒序&#xff08;从右向左&#xff09;读都是一样的整数。 例如&#xff0c;121 是回文&am…

c语言中的static静态(1)static修饰局部变量

#include<stdio.h> void test() {static int i 1;i;printf("%d ", i); } int main() {int j 0;while (j < 5){test();j j 1;}return 0; } 在上面的代码中&#xff0c;static修饰局部变量。 当用static定义一个局部变量后&#xff0c;这时局部变量就是…

蓝桥杯专题-真题版含答案-【扑克牌排列】【放麦子】【纵横放火柴游戏】【顺时针螺旋填入】

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…