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

news2025/2/4 9:00:43

文章目录

  • 一、死锁
    • (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(){
    @Override
    public void run() {

    }
}.start();

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

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

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

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

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

new Thread(){
    @Override
    public 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(){
    @Override
    public 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(){
    @Override
    public 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();

        //线程1
        new Thread(){
            @Override
            public 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();


        //线程2
        new Thread(){
            @Override
            public 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();

        //线程1
        new Thread(){
            @Override
            public 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();


        //线程2
        new Thread(){
            @Override
            public 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;
	}

	@Override
	public 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;
	}

	@Override
	public 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() —>将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        while (true){
            if(ticket>0){   //如果票数大于0就可以售票
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //哪个窗口卖票了,票卖了多少
                System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);  //最开始票号为100
                ticket--;
            }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() —>将此线程要执行的操作,声明在此方法体中
    @Override
    public 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);  //最开始票号为100
                ticket--;
            }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() —>将此线程要执行的操作,声明在此方法体中
    @Override
    public 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);  //最开始票号为100
                    ticket--;
                }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.coloradmin.cn/o/1427295.html

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

相关文章

C++集群聊天服务器 网络模块+业务模块+CMake构建项目 笔记 (上)

跟着施磊老师做C项目&#xff0c;施磊老师_腾讯课堂 (qq.com) 一、网络模块ChatServer chatserver.hpp #ifndef CHATSERVER_H #define CHATSERVER_H#include <muduo/net/TcpServer.h> #include <muduo/net/EventLoop.h> using namespace muduo; using namespace …

Qt6入门教程 15:QRadioButton

目录 一.简介 二.常用接口 三.实战演练 1.径向渐变 2.QSS贴图 3.开关效果 4.非互斥 一.简介 QRadioButton控件提供了一个带有文本标签的单选按钮。 QRadioButton是一个可以切换选中&#xff08;checked&#xff09;或未选中&#xff08;unchecked&#xff09;状态的选项…

深信服技术认证“SCCA-C”划重点:深信服云计算关键技术

为帮助大家更加系统化地学习云计算知识&#xff0c;高效通过云计算工程师认证&#xff0c;深信服特推出“SCCA-C认证备考秘笈”&#xff0c;共十期内容。“考试重点”内容框架&#xff0c;帮助大家快速get重点知识。 划重点来啦 *点击图片放大展示 深信服云计算认证&#xff08…

SG2520CAA汽车用晶体振荡器

爱普生SG2520CAA是简单的封装晶体振荡器&#xff08;SPXO&#xff09;&#xff0c;具有CMOS输出&#xff0c;这款SPXO是汽车和高可靠性应用的理想选择&#xff0c;符合AEC-Q200标准&#xff0c;功耗低&#xff0c;工作电压范围为1.8 V ~ 3.3 V类型&#xff0c;宽工作温度-40℃~…

binder android

文心一言的回答 Binder驱动是Android操作系统中用于进程间通信&#xff08;IPC&#xff09;的机制。它提供了一种高效、跨进程的数据传输方式&#xff0c;使得应用程序的不同组件可以在Android系统上互相通信。 Binder驱动基于Linux内核&#xff0c;其核心组件是一个称为Bind…

git小白之路

初始配置 配置账号密码 git config --list # 设置git的name git config --global user.name "determination" # 设置git的邮箱 git config --global user.email "XXXX.XX.conm" 配置ssh-key ssh-keygen -t rsa -C "usernameemail.com" 添…

活用社交媒体生成二维码:拓展品牌影响力的新时代

社交媒体二维码为品牌在社交媒体平台上的拓展和互动提供了全新的可能性。在这个信息爆炸的时代&#xff0c;如何巧妙地应用社交媒体二维码&#xff0c;成为品牌拓展影响力、与用户建立深度互动的关键。本文将探讨如何精明地应用社交媒体生成二维码&#xff0c;为品牌带来更多的…

来看看思特数字创意产业的科技与狠活儿~ 冰雪大世界图集

01    哈尔滨冰雪大世界以“龙腾冰雪 逐梦亚冬”为主题&#xff0c;为世界各地游客打造一座集冰雪艺术、冰雪文化、冰雪演艺、冰雪建筑、冰雪活动、冰雪体育于一体的冰雪乐园。      02    服务商思特数字创意公司构建“龙腾之韵、雪城乐章、跳动音符、雪映流光”四…

leetcode-35.搜索插入位置

题目 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出: 2示例 2: 输入…

幻兽帕鲁服务器搭建

获取服务器 有如下方法&#xff1a; 阿里云提供现成的幻兽帕鲁服务器&#xff0c;支持一键部署和升级。购买通用服务器或利用已有的主机&#xff0c;配置幻兽帕鲁服务。 第一种可以零代码实现&#xff0c;本文不作赘述&#xff0c;本文主要介绍如何通过已有的Linux服务器实现…

时间序列预测 —— TCN模型

时间序列预测 —— TCN模型 卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;在图像处理等领域取得了显著的成就&#xff0c;一般认为在处理时序数据上不如RNN模型&#xff0c;而TCN&#xff08;Temporal Convolutional Network&#xff09;…

基于python+django,我开发了一款药店信息管理系统

功能介绍 平台采用B/S结构&#xff0c;后端采用主流的Python语言进行开发&#xff0c;前端采用主流的Vue.js进行开发。 功能包括&#xff1a;药品管理、分类管理、顾客管理、用户管理、日志管理、系统信息模块。 代码结构 server目录是后端代码web目录是前端代码 部署运行…

Camille-学习笔记-web基础知识

web基础1.系统架构 B/S :Browser/Server 网站 界面层&#xff08;UI&#xff09; 业务逻辑层&#xff08;业务&#xff09; 数据访问层&#xff08;数据库&#xff09; 静态网页&#xff1a;和服务器没有数据交互 动态网页&#xff1a;网页数据可以和服务器进行数据交互 URL…

【数据结构】(四)图

目录 言 图的入门及无向图的实现 1. 图的相关概念 2. 图的相关术语 3. 图的存储结构 3.1 邻接矩阵 3.2 邻接表 3.3 邻接表实现 图的搜索算法 1. 深度优先搜索 1.1 搜索思路 1.2 代码实现 2. 广度优先搜索 2.1 搜索思路 2.2 代码实现 后记 言 数据结构分为逻辑结…

vscode 如何修改c/c++格式化风格,大括号不换行

在Visual Studio Code&#xff08;VSCode&#xff09;中&#xff0c;若要修改C代码格式化的风格以实现大括号不换行&#xff0c;通常会借助于插件C/C扩展中的ClangFormat配置。以下是具体的步骤&#xff1a; 确保已安装了C/C扩展&#xff1a; 打开VSCode的扩展市场&#xff08;…

【飞书小技巧】——飞书文档转 markdown 详细教程

飞书文档转 markdown 详细教程 基于项目:https://github.com/Wsine/feishu2md 如何使用 在线版 访问 https://feishu2md.onrender.com/ 粘贴文档链接即可&#xff0c;文档链接可以通过 分享 > 开启链接分享 > 复制链接 获得。 点击下载之后,会提示 Please wait. It ma…

回归预测 | Matlab基于POA-LSSVM鹈鹕算法算法优化最小二乘支持向量机的数据多输入单输出回归预测

回归预测 | Matlab基于POA-LSSVM鹈鹕算法算法优化最小二乘支持向量机的数据多输入单输出回归预测 目录 回归预测 | Matlab基于POA-LSSVM鹈鹕算法算法优化最小二乘支持向量机的数据多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab基于POA-LSSVM…

回归预测 | Matlab实现CPO-LSTM【24年新算法】冠豪猪优化长短期记忆神经网络多变量回归预测

回归预测 | Matlab实现CPO-LSTM【24年新算法】冠豪猪优化长短期记忆神经网络多变量回归预测 目录 回归预测 | Matlab实现CPO-LSTM【24年新算法】冠豪猪优化长短期记忆神经网络多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现CPO-LSTM【24年新算…

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

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

从源码角度透视QTcpServer:解构QTcpServer的底层原理与技术细节

深入了解QTcpServer的底层原理和技术细节 一、背景二、QTcpServer的基本原理2.1、TCP协议简介2.2、QTcpServer的概念 三、QTcpServer源码解析3.1、QTcpServer的构造函数3.2、调用listen函数启动tcpserver3.3、QSocketNotifier的实现 总结 一、背景 QTcpServer是Qt网络模块中的…