Java多线程--避免同步机制带来的死锁问题及用Lock锁解决线程安全问题

文章目录

  • 一、死锁
    • (1)说明
    • (2)案例
      • 1、案例1
      • 2、案例2
      • 3、案例3
    • (3)诱发死锁的原因及解决方案
      • 1、诱发死锁的原因
      • 2、避免死锁
  • 二、JDK5.0新特性:Lock(锁)
    • (1)介绍
    • (2)案例
    • (3)synchronized与Lock的对比

一、死锁

(1)说明

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

image.png

比如线程1拿着同步监视器(锁),它拿着锁1等着锁2;但是线程2拿着锁2等着锁1。两个线程僵持着,互相等待,这就构成了死锁

我们编写程序时,要避免出现死锁

一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

(2)案例

1、案例1

StringBuilder:跟字符串相关的类(后边说,这里看成字符串就可以了,里面没有任何数据)

用它造两个对象:

public class DeadLockTest {public static void main(String[] args) {StringBuilder s1=new StringBuilder();StringBuilder s2=new StringBuilder();}
}

然后new一个线程:

new Thread(){@Overridepublic void run() {}
}.start();

线程里面调用了run方法,把s1当作同步监视器(锁),然后用s1调用append方法(用于添加),添加一个“a”,如下:

new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");	//理解为空字符串" "+"a"}}
}.start();

然后s2也添加一个“1”,如下:

new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");}}
}.start();

s2当作一个锁,给s1添加一个“b”,给s2添加一个“2”。如下:

new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}
}.start();

然后在这里sleep睡一下,便于演示死锁的问题:

new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}
}.start();

说一下大致过程:

线程1进入run,获得锁s1,然后稍微sleep一会,醒来之后再拿着锁s2,执行后面的操作,执行结束后打印s1与s2。

现在我们再写一个线程,原理与上面的类似,只不过这个线程2先获得锁s2,再获得锁s1,如下:

//线程2
new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}
}.start();

若此时没有sleep,如下:

public class DeadLockTest {public static void main(String[] args) {StringBuilder s1=new StringBuilder();StringBuilder s2=new StringBuilder();//线程1new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();//线程2new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}.start();}
}

运行如下:

image.png

可以发现并没有出现死锁,这就是问题所在,能执行成功不意味着这个程序没有问题。

其实它是存在问题的可能性的。

从输出结果上来看,先执行的是线程1,s1获得a,s2获得1,然后s1获得b,s2获得2。最终s1得到ab,s2得到12。

然后线程2执行,在原有基础上,又添加了数据,最终s1得到abcd,s2得到1234。

也就是说,线程1执行结束,线程2才 开始执行。

线程1执行结束的时候,s1与s2两个同步监视器都被释放了,所以线程2执行的时候才能顺利拿到s1与s2。

如果先执行的是第2个线程,再执行第1个线程,结果就是:cd,34,cdab,3412。


现在我们加上sleep,让死锁的概率高一点,注意这里只是让它出现的概率变高了,并不是从无到有(只是数量上的,并不是质变)。

🌱代码

package yuyi04.lock;/*** ClassName: DeadLockTest* Package: yuyi04.lock* Description:** @Author 雨翼轻尘* @Create 2024/2/1 0001 15:18*/
public class DeadLockTest {public static void main(String[] args) {StringBuilder s1=new StringBuilder();StringBuilder s2=new StringBuilder();//线程1new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();//线程2new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}.start();}
}

🍺输出

image.png

可以看到,出现了死锁

线程1拿着同步监视器s1,在sleep的时候,线程2执行。

然后线程2拿着同步监视器s2往后执行,也碰到了sleep。

线程1醒来之后,去拿同步监视器s2,但是s2在线程2手里。大家就僵持住了。

这就构成了死锁,如下:

image.png

2、案例2

案例1很容易看出来会出现死锁,现在再来看一个例子:

🌱代码

package yuyi04.lock;/*** ClassName: DeadLock* Package: yuyi04.lock* Description:** @Author 雨翼轻尘* @Create 2024/2/1 0001 18:01*/class A {public synchronized void foo(B b) {System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了A实例的foo方法"); // ①try {Thread.sleep(200);} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用B实例的last方法"); // ③b.last();}public synchronized void last() {System.out.println("进入了A类的last方法内部");}
}
class B {public synchronized void bar(A a) {System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了B实例的bar方法"); // ②try {Thread.sleep(200);} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用A实例的last方法"); // ④a.last();}public synchronized void last() {System.out.println("进入了B类的last方法内部");}
}public class DeadLock implements Runnable {	//用实现Runnable接口的方式创建多线程A a = new A();B b = new B();public void init() {Thread.currentThread().setName("主线程");// 调用a对象的foo方法a.foo(b);System.out.println("进入了主线程之后");}public void run() {Thread.currentThread().setName("副线程");// 调用b对象的bar方法b.bar(a);System.out.println("进入了副线程之后");}public static void main(String[] args) {DeadLock dl = new DeadLock();new Thread(dl).start();	//分线程创建,并调用start()方法,即调用run()方法dl.init();	//主线程调用init()方法}
}

🍰分析

可以看到,类A与类B里面定义了两个方法,都加了synchronized,就是同步方法,同步监视器就是当前类的对象。

如下:

image.png

然后在实现类里面声明了a与b两个成员变量,还有两个方法,如下:

image.png

在main方法种,创建了当前实现类DeadLock的对象dl,然后将dl当作形参传入Thread()中,用实现Runnable接口的方式创建一个线程,调用start()方法,于是这个分线程就去执行run()方法。

然后又使用dl调用init()方法,这就是主线程的代码。就将dl当作普通的对象,它调用init()方法。

现在就是,主线程调用init()方法,分线程调用run()方法

分线程调用run方法,主要执行b.bar(a);

public void run() {Thread.currentThread().setName("副线程");// 调用b对象的bar方法b.bar(a);System.out.println("进入了副线程之后");
}

主线程调用init方法,主要执行a.foo(b);

public void init() {Thread.currentThread().setName("主线程");// 调用a对象的foo方法a.foo(b);System.out.println("进入了主线程之后");
}

可以得出主线程与分线程分别调用的方法如下:

image.png


分线程调用bar方法,需要拿着A类的一个对象a作为锁,这个锁就是B的对象。

image.png

然后执行a.last(),如下:

image.png

注意锁是当前对象this,用b调用bar(a),this就是b,也就是拿着锁b进入了同步方法bar中,顺便将参数a带进来了。

在执行最后有一个a.last(),就是用对象a调用last()方法,last方法也是一个同步方法a是传进来的A的对象,相当于又需要握着a这个同步监视器。如下:

image.png

也就是说,分线程握着B的对象,还要握A的对象。

主线程正好相反,握着A的对象a又需要b的同步监视器。
image.png

🍺输出结果

image.png

可以看见输出结果在这里出现了死锁,僵持不动了。

3、案例3

public class TestDeadLock {public static void main(String[] args) {Object g = new Object();Object m = new Object();Owner s = new Owner(g,m);Customer c = new Customer(g,m);new Thread(s).start();new Thread(c).start();}
}
class Owner implements Runnable{private Object goods;private Object money;public Owner(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {synchronized (goods) {System.out.println("先给钱");synchronized (money) {System.out.println("发货");}}}
}
class Customer implements Runnable{private Object goods;private Object money;public Customer(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {synchronized (money) {System.out.println("先发货");synchronized (goods) {System.out.println("再给钱");}}}
}

(3)诱发死锁的原因及解决方案

1、诱发死锁的原因

互斥条件(一定会出现)

比如线程1握着同步监视器(锁),另外一个线程就无法获得这个锁。(必然的,这是同步机制,加同步的目的就是为了某个线程能获得锁而另一个线程握不住)

占用且等待

比如线程1握着同步监视器(锁),然后又等待另外一个锁。另外一个线程又拿着这个锁。

不可抢夺(或不可抢占)

某线程等待的时候不能将其他线程拥有的锁抢过来。

循环等待

这种情况会一直僵持着,解不开。

以上4个条件,同时出现就会触发死锁。

2、避免死锁

死锁一旦出现,基本很难人为干预,只能尽量规避

可以考虑打破上面的任意一个诱发条件

①针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

②针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

③针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源,让别人先来,这样其他的线程不需要等待,不会僵持了

④针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题(按顺序获取,先获取序号小的,才能获取后边大的序号)。

二、JDK5.0新特性:Lock(锁)

(1)介绍

以前说的“同步机制”,其实就是synchronized的使用方式。

除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁的方式。

  • 除了synchronized的方式,JDK5.0还提供了Lock锁的方式(另外一种解决线程安全的方式)。
  • JDK5.0的新增功能,保证线程的安全。
  • 与采用synchronized相比,Lock可提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • 在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
    • ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
  • Lock锁也称同步锁,加锁与释放锁方法,如下:
    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。
  • 代码结构
class A{//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例private final ReentrantLock lock = new ReenTrantLock();public void m(){//2. 调动lock(),实现需共享的代码的锁定lock.lock();try{//保证线程安全的代码;}finally{//3. 调用unlock(),释放共享代码的锁定lock.unlock();  }}
}

注意:如果同步代码有异常,要将unlock()写入finally语句块。

【举例】

import java.util.concurrent.locks.ReentrantLock;class Window implements Runnable{int ticket = 100;//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例private final ReentrantLock lock = new ReentrantLock();public void run(){while(true){try{//2. 调动lock(),实现需共享的代码的锁定lock.lock();if(ticket > 0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(ticket--);}else{break;}}finally{//3. 调用unlock(),释放共享代码的锁定lock.unlock();}}}
}public class ThreadLock {public static void main(String[] args) {Window t = new Window();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();}
}

(2)案例

【步骤】

步骤1:创建Lock的实例,需要确保多个线程共用同一个Lock实例。

需要考虑将此对象声明为static final

步骤2.:执行lock()方法,锁定对共享资源的调用。

步骤3.:unlock()的调用,释放对共享数据的锁定。


以下面代码(继承的方式)为例:

🌱代码

package yuyi04.lock;/*** ClassName: WindowTest2* Package: yuyi04.lock* Description:*      使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/2/2 0002 9:54*/public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}}
}

🍺输出

image.png

上面代码有线程安全问题

现在我们用Lock来解决线程安全问题。

image.png

concurrent”就是并发的意思,Java的并发编程其实就是说这个包里面的API。

现在我们就可以看到这个包里面的一个API,叫Lock,它是一个接口,目前已知的实现类如下:

image.png

我们需要使用ReentrantLock可重入锁)这个实现类,接下来创建这个实现类的对象

如下:

class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类ReentrantLock lock=new ReentrantLock();//...
}

①权限修饰

我们需要先考虑权限修饰

首先不希望在外部可以被访问,因为这纯粹是为了保证线程安全的,所以加一个private

其次这个锁有要求,需要保证线程的安全,多个线程需要共用同一个lock,所以还要再加一个static

最后再加一个final,保证给lock赋值之后不要再修改了,是唯一的不能改变的。

如下:

private static final ReentrantLock lock=new ReentrantLock();

Runnable接口一般不需要static的,一般是把这个接口的一个实例作为多个线程对象的形参,一般情况只会有一个接口的实例。

②用方法限制

这个锁定操作,不像同步代码块,有一个大括号,里面是需要被同步的代码。

lock比较灵活,它只需要用两个方法去限制,两个方法执行当中的代码就是需要被同步的代码。

针对案例,需要被同步的代码如下:(蓝色部分)

image.png

在这一段代码之前,调用一个方法lock();然后在代码执行结束,调用unlock()方法。如下:

image.png

两个方法中间的代码就是之前我们放到同步代码块当中的代码。


🌱代码

package yuyi04.lock;import java.util.concurrent.locks.ReentrantLock;/*** ClassName: WindowTest2* Package: yuyi04.lock* Description:*      使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/2/2 0002 9:54*/public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;private static final ReentrantLock lock=new ReentrantLock();//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){lock.lock();if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}lock.unlock();}}
}

🍺输出

image.png

可以看见线程安全了。

🗳️这里可能出现了没有解锁的场景:

image.png

如果死锁的话就执行不到unlock,虽然关闭了程序,但是如果这些资源是指向别的资源的话就无法释放了。

unlock()方法一定要保证会被执行,可以考虑写入finally中。

现在我们在这里加一个try,将下面蓝色部分移入try里面:

image.png

然后再写一个finally,将unlock写入,确保它一定会被执行。如下:

image.png


🌱代码

package yuyi04.lock;import java.util.concurrent.locks.ReentrantLock;/*** ClassName: WindowTest2* Package: yuyi04.lock* Description:*      使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/2/2 0002 9:54*/public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口  创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{    //卖票  1.创建一个继承于Thread类的子类//票static int ticket=100;//①创建Lock的实例(Lock是一个接口,现在用的是可重入锁ReentrantLock)// 需要确保多个线程共用同一个Lcok实例,需要考虑将此对象声明为static final//若没有加static,Window一共造了3个对象,相当于现在就有3把锁,每一个对象锁自己的,就不好使private static final ReentrantLock lock=new ReentrantLock();    //2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){try {//②执行lock()方法,锁定对共享资源的调用lock.lock();if(ticket>0){   //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100ticket--;}else{break;}}finally{//③unlock()的调用,释放对共享数据的锁定,解锁之后其他线程就可以来操作了lock.unlock();}}}
}

🍺输出

可以看到,程序结束了,而且没有线程安全问题。如下:

image.png

(3)synchronized与Lock的对比

synchronized与Lock的对比

1、Lock显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized隐式锁,出了作用域、遇到异常等自动解锁。

2、Lock只有代码块锁synchronized代码块锁和方法锁

3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。

4、(了解)Lock锁可以对读不加锁,对写加锁,synchronized不可以。

5、(了解)Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以。

说明:

开发建议中处理线程安全问题优先使用顺序为:

Lock ----> 同步代码块 ----> 同步方法

【面试题】

🎲synchronized同步的方式 与Lock的对比 ?

  • synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
  • Lock是通过两个方法控制需要被同步的代码,更灵活一些。
  • Lock作为接口,提供了多种实现类(也就是很多锁),适合更多更复杂的场景,效率更高

image.png

对于Lock,JUC里面再详细说,这里不做深入。

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

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

相关文章

小白水平理解面试经典题目_数组类LeetCode 118 Pascal‘s Triangle【回归解法】

LeetCode 118 生成杨辉三角(Pascal’s Triangle) 小白渣翻译 给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。 在杨辉三角中,每个数是它左上方和右上方的数的和。 例子 这里是小白理解 那么这种题目一上来看&#xf…

「连载」边缘计算(十二)01-31:边缘部分源码(源码分析篇)

(接上篇) EdgeCore中功能模块的启动 EdgeCore中功能模块的启动与中的CloudCore中功能模块的启动流程完全相同,大家可以参考该部分。 组件源码分析 本节将对Kubernetes的核心组件的源码进行梳理和分析。这些组件包括控制节点的kube-apiser…

4-树-合并两个有序链表

这是树的第4篇算法,力扣链接。 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1: 输入:l1 [1,2,4], l2 [1,3,4] 输出:[1,1,2,3,4,4]示例 2: 输入&#xf…

关于使用js的循环语句打印一个金字塔的三种方案

//for循环方式 <html> <head> <title>金字塔三角形</title> <script> var ta prompt("请输入金字塔的行数"); //浏览行数 for(var i1;i<ta;i){ …

SQL Limit

在 SQL 中&#xff0c;LIMIT 通常用于限制查询结果的行数。然而&#xff0c;具体的语法和用法可能在不同的数据库系统中有所不同。以下是在一些常见的数据库系统中使用 LIMIT 的示例&#xff1a; 1. MySQL / MariaDB: -- 返回前10行 SELECT * FROM your_table LIMIT 10;-- 返…

Linux下gcc的使用与程序的翻译

gcc和程序的翻译过程 gcc介绍程序的翻译过程预编译编译汇编链接 命令行式宏定义 gcc介绍 gcc是一款编译C语言编译器&#xff0c;可以把我们用vim写的代码编译成可执行程序。编译C用g进行编译&#xff0c;C的文件后缀是test.cc或test.cpp或test.cxx 如果要安装g就执行以下命令 …

C# winform 多语言(json)方式实现

前后对比 使用nuget json工具包1.总体思路 创建对应的json字典对照表 { "测试":"Test", "语言":"Language", "设置":"Set", "中文(默认)":"Chinese (default)", "英文":"E…

【HarmonyOS应用开发】ArkTS 属性动画的使用(十二)

一、概述 属性动画&#xff0c;是最为基础的动画&#xff0c;其功能强大、使用场景多&#xff0c;应用范围较广。常用于如下场景中&#xff1a; 一、页面布局发生变化。例如添加、删除部分组件元素。二、页面元素的可见性和位置发生变化。例如显示或者隐藏部分元素&#xff0…

CANoe学习笔记——窗口类型

CANoe中的窗口类型&#xff0c;共分为三种 1&#xff1a;MDI windows 2&#xff1a;Standard Windows 3&#xff1a;Docking Windows 窗口有多种类型&#xff0c;每种类型都定义了特定的窗口行为。通过点击窗口顶部的区域&#xff0c;可以更改窗口类型。 如下图&#xff0…

Python的requests库与HTTP代理的使用:魔法般的网络探险之旅

嘿&#xff0c;各位魔法探险家们&#xff01;今天我们要一起探索Python的requests库与HTTP代理的神奇组合&#xff0c;开启一段魔法般的网络探险之旅&#xff01; 首先&#xff0c;我们要明白什么是requests库。简单说&#xff0c;requests库就是Python中的魔法飞毯&#xff0…

Linux基础知识合集

整理了一下学习的一些关于Linux的一些基础知识&#xff0c;同学们也可以通过公众号菜单栏查看&#xff01; 一、基础知识 Linux基础知识 Linux命令行基础学习 Linux用户与组概念初识 Linux文件与目录权限基础 Linux中文件内容的查看 Linux系统之计划任务管理 二、服务器管理 Vm…

MySql主从同步,同步SQL_ERROR 1032解决办法

1.登录从库 mysql -u root -p 2.输入命令查看状态 SHOW SLAVE STATUS\G; 3.找到对应的错误数据位置 Slave_IO_Running: YesSlave_SQL_Running: NoReplicate_Do_DB: app_push_centerReplicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: Replicate_Wild_Do_Tabl…

“欢天喜地迎新春”下姜村邻里守望写对联活动

卯兔追冬去&#xff0c;辰龙报春来。空谷幽香谱佳期&#xff0c;红联金句寄吉祥。春联是我国特有的文学形式&#xff0c;贴春联是继承传统习俗的一种方式&#xff0c;是对祖先的尊敬&#xff0c;对传统的继承。春节前夕&#xff0c;家家户户贴上红红的春联&#xff0c;一副副透…

Ubuntu 关闭rsyslog,var/log/syslog文件过大解决

关闭命令&#xff1a; systemctl stop syslog.socket rsyslog.service这个命令会停止rsyslog服务和syslog.socket。在某些情况下&#xff0c;仅仅停止rsyslog服务是不够的&#xff0c;因为syslog.socket可能会重新启动它。所以&#xff0c;你需要同时停止这两个服务。 请注意&…

Android ViewPager2 同屏显示左右item

大家都知道Google最近发布的ViewPager2基本不兼容ViewPager&#xff0c;虽然众多特性仍然可以使用&#xff0c;但两者在代码和应用方面相差甚远。就比如ViewPager的显示左右ITEM使用在ViewPager2之上仍然会留下左右的边距&#xff0c;但是静止状态下无法显示出左右item&#xf…

西圣Olite开放式耳机持续100+天霸榜:品质优势再掀数码狂潮

随着开放式耳机的市场竞争加剧&#xff0c;用户对耳机的音质和配置要求越来越高。而西圣开放式耳机的不断推陈出新&#xff0c;正是对客户需求的完美回应&#xff01;西圣开放式耳机&#xff0c;在现在鱼龙混杂的市场上&#xff0c;能够获得着卓越的研发成果并且还在不断的追求…

qt学习:停车场管理系统+摄像头+http识别车牌+sqlite3数据库

目录 参考前面发的几篇文章http识别车牌&#xff0c;sqlite3数据库、摄像头的文章 步骤 部分代码 新建一个项目&#xff0c;加入前面用到的http和image两个文件&#xff0c;和加入用到的模块和头函数和成员&#xff0c;加入前面用到的全局变量 配置ui界面 在构造函数中初…

Java14常用类4:Java比较器

5.Java比较器 基本数据类型的数据&#xff08;除Boolean类型外&#xff09;需要比较大小的话&#xff0c;使用比较运算符即可&#xff0c;但是引用数据类型不能直接使用比较运算符来比较大小。 Java中经常涉及到对象数组的排序问题&#xff0c;对此Java有 2种方式实现对象的排…

Qt5 基于OpenGL实现六轴机械臂三维仿真

需求 在Qt中通过OPenGL方式加载三维模型STL文件&#xff0c;然后将多个结构的STL文件类型的模型进行组装&#xff0c;形成6轴机械臂三维模型的显示&#xff0c;并且可以对每个关节进行关节角度的控制。 新建一个C类STLFileLoader&#xff0c;用于加载STL文件&#xff0c;并进…

IP协议(2) 和 数据链路层协议基础

IP协议续 1.路由选择 在复杂的网络结构中,我们需要找到一个通往终点的路线,这就是路由选择 举个例子:我们在没有手机导航之前,想去一个地方得是到一个地方问一下路的方式最终找到目的地 路由的过程,其实就是样子问路的过程 1.当IP数据包到达路由器的时候,会查看目的IP 2.路由器…