Java 基础之锁

news2024/12/25 1:41:23

Java 基础之线程_禽兽先生不禽兽的博客-CSDN博客

上一篇博客中记录了线程的一些概念,那多线程既然能与人方便必然也会带来一些问题,这些问题主要与线程的三大特性有关,Java 的一些关键字和锁机制,可以帮助我们解决这些问题。

一、volatile

volatile 是 Java 的一个关键字,它具有以下特点:

  • 只能修饰变量,不能修饰方法和代码块;
  • 保证可见性、有序性,但不能保证原子性;
  • 相对轻量。

根据 volatile 这些特性,我们就可以知道它的适用场景了,它主要应用于一些只对变量有读同步的并发场景中。在上一篇博客文末的两段代码中,我们只需要给变量加上 volatile 关键字,就可以避免可见性和有序性的问题了。

二、synchronized

volatile 只能保证可见性、有序性,那想要保证原子性应该怎么做呢?先看个例子:

package com.qinshou.resume.thread;

import java.util.concurrent.CountDownLatch;

public class SynchronizedDemo {
	private static int a = 0;

	private static void increaseA() {
		a++;
	}

	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(2);
		Thread thread1 = new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					increaseA();
				}
				countDownLatch.countDown();
			}
		});
		Thread thread2 = new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					increaseA();
				}
				countDownLatch.countDown();
			}
		});
		thread1.start();
		thread2.start();
		countDownLatch.await();
		System.out.println("a--->" + a);
	}
}

上述代码启动了两个线程做了同样的操作,理想的结果是 a 的值最终为 20000,但实际情况却差强人意,a 的值大概率不到 20000,这还是因为多个线程同时修改一个变量导致的,这个是写操作,所以用 volatile 关键字并没有用,只能在写入时加锁,保证在一个时间内只有一个线程修改该变量,而加锁的方式有两种,使用 synchronized 关键字是最简单的一种,为 increaseA() 方法增加 synchronized 关键字修饰,则可使上面的程序结果为 20000。

private synchronized static void increaseA() {
	a++;
}

JVM 会在调用被 synchronized 修饰的方法时会先尝试获取对象锁(每个对象都有一把锁),没获取到就阻塞等待,获取到之后就加锁,于是只能你访问该代码块,该代码块执行完成后再释放锁。

因为这个上锁的操作,所以三大特性在多线程下可能发生的问题就得以解决,每次只有一个线程访问代码块,在执行完后操作的变量会写入主内存,那其他线程再访问,自然是最新的值,可见性得以解决。也因为每次只有一个线程访问代码块,所以即使编译器有优化,指令重排了也无所谓,有序性就可以忽略了。加锁操作保证一个线程中该代码块一定执行完了才会轮到别的线程,正是解决原子性的问题。

synchronized 可以修饰代码块和方法(包括实例方法和静态方法)。

package com.qinshou.resume.thread;

public class SynchronizedDemo {

	/**
	 * synchronized 修饰代码块
	 */
	public void method1() {
		synchronized (this) {
			// do sth.
		}
	}

	/**
	 * synchronized 修饰实例方法
	 */
	public synchronized void method2() {
		// do sth.
	}

	/**
	 * synchronized 修饰静态方法
	 */
	public synchronized static void method3() {
		// do sth.
	}
}

其实 synchronized 修饰实例方法时,获取的是当前对象的锁,所以 method2 与 method1 中获取的是同一个锁对象,而修饰静态方法时,获取的是类对象的锁。

虽然 method2 与 method1 获取的都是同一个锁对象,但是反编译后的代码是不一样的,利用 javap -c -v 获取反编译后的代码,可以看到有这两部分字节码:

public void method1();
  descriptor: ()V
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=1
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_1
       5: monitorexit
       6: goto          14
       9: astore_2
      10: aload_1
      11: monitorexit
      12: aload_2
      13: athrow
      14: return
    Exception table:
       from    to  target type
           4     6     9   any
           9    12     9   any
    LineNumberTable:
      line 5: 0
      line 7: 4
      line 8: 14
    StackMapTable: number_of_entries = 2
      frame_type = 255 /* full_frame */
        offset_delta = 9
        locals = [ class com/qinshou/resume/thread/SynchronizedDemo, class java/lang/Object ]
        stack = [ class java/lang/Throwable ]
      frame_type = 250 /* chop */
        offset_delta = 4

public synchronized void method2();
  descriptor: ()V
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=1, args_size=1
       0: return
    LineNumberTable:
        line 12: 0

在 method1 中有 monitorenter 和 monitorexit 指令,这其实就是通过对象监视器来进行加锁和释放锁的操作,而 method2 中并没有这一对指令,取而代之的是 flags 中增加了一个 ACC_SYNCHRONIZED 标识,该标识向 JVM 标识该方法是一个同步方法。

三、Lock

除了使用 synchronized 来进行同步外,还可以使用 Lock 对象来进行同步。Lock 是一个接口,它的实现类主要是 ReentrantLock,字面意思是可重入锁(可重入锁和不可重入锁稍后记录),先来看看它怎么用:

package com.qinshou.resume.thread;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
	private Lock mLock = new ReentrantLock();
	private int a = 0;

	private void increaseA() {
		try {
			mLock.lock();
			a++;
		} finally {
			mLock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(2);
		LockDemo lockDemo = new LockDemo();
		Thread thread1 = new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					lockDemo.increaseA();
				}
				countDownLatch.countDown();
			}
		});
		Thread thread2 = new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					lockDemo.increaseA();
				}
				countDownLatch.countDown();
			}
		});
		thread1.start();
		thread2.start();
		countDownLatch.await();
		System.out.println("a--->" + lockDemo.a);
	}
}

上述代码同使用 synchronized 关键字结果相同,同时可以看到使用 ReentrantLock 对象我们需要自己手动释放锁,使用最好将释放锁的操作放到 finally 代码块中,否则一个不小心就有可能造成死锁(死锁即一个线程获取锁之后无法释放,导致后续线程无法再获得锁,从而正常工作)。

那么为什么我们还要用 ReentrantLock 呢?换句话说,它比 synchronized 好在哪儿?总结一下它们两个的异同:

相同点

  1. 都能保证线程的三大特性。
  2. 都是可重入锁。

不同点

  1. ReentrantLock 性能较之于 synchronized 更好(JDK 1.6 之后 synchronized 有过升级,升级后性能其实也不差)。
  2. synchronized 加锁释放锁操作由 JVM 控制,程序异常也会释放锁,这一点可以看看上面反编译后的代码,反编译后会有两个 monitorexit 指令,第一个指令是方法正常退出时释放锁,而第二个就是方法异常时释放锁。而 ReentrantLock 需要自己手动加锁和释放锁。
  3. 同样,因为 synchronized 加锁释放锁操作由 JVM 控制,所以一个线程如果没有获取到锁就只能一直等待,而 ReentrantLock 可以尝试在超时时间内获取锁,超时时间内没有获取到锁则可以执行其他逻辑。
  4. ReentrantLock 可以配合 Condition 对象,更灵活。
  5. ReentrantLock 可以实现公平锁(公平锁和非公平锁稍后记录)。

所以我们大致上知道,synchronized 简单易用,而 ReentrantLock 功能更强。

四、Condition

有时候我们的线程获取到了锁对象,进入了临界区,但可能之后后续代码的时候,需要满足某些条件,于是 ReentrantLock 有了 Condition 对象,它可以帮助我们管理那些获得了锁但因为条件不满足而不能正常工作的线程。

Condition 对象主要会用到如下方法:

/**
 * 当前线程进入阻塞状态直到被 signal 或中断。
 */
void await() throws InterruptedException;
/**
 * 唤醒一个阻塞在 Condition 上的线程。
 */
void signal();
/**
 * 唤醒所有阻塞在 Condition 上的线程。
 */
void signalAll();

在每个 Condition 中,都维护着一个队列,每当执行 await() 方法,都会将当前线程封装为一个节点,并添加到条件等待队列尾部。然后释放与 Condition 对象绑定的锁,在释放锁的同时还会并唤醒阻塞在锁的入口等待队列中的一个线程,完成以上操作后再将自己阻塞。

举个栗子就能大概理解 Condition 对象能做什么了,我们用 ReentrantLock + Condition 来实现一个阻塞队列:

package com.qinshou.resume.thread;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
	private List<String> mList = new LinkedList<>();
	private ReentrantLock mLock = new ReentrantLock();
	private Condition mFull = mLock.newCondition();
	private Condition mEmpty = mLock.newCondition();

	public void push(String string) throws InterruptedException {
		try {
			mLock.lock();
			if (mList.size() == 5) {
				System.out.println("队列满了");
				// 类似 wait()
				mFull.await();
			}
			// 类似 notify()
			mEmpty.signal();
			mList.add(string);
		} finally {
			mLock.unlock();
		}
	}

	public String pop() throws InterruptedException {
		try {
			mLock.lock();
			if (mList.size() == 0) {
				System.out.println("队列为空");
				mEmpty.await();
			}
			mFull.signal();
			// FIFO
			return mList.remove(0);
		} finally {
			mLock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ConditionDemo conditionDemo = new ConditionDemo();
		Thread writeThread = new Thread(new Runnable() {

			@Override
			public void run() {
				// 先 sleep 一下再插入消息,让读取消息的线程先运行,模拟队列为空的场景
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e1) {
					e1.printStackTrace();
				}
				for (int i = 0; i < 10; i++) {
					try {
						conditionDemo.push("Hello World" + i);
						System.out.println("插入消息--->" + ("Hello World" + i));
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		Thread readThread = new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					// 取出两条消息后 sleep 一下,模拟队列满了的场景
					if (i == 2) {
						try {
							Thread.sleep(1000);
						} catch (InterruptedException e1) {
							e1.printStackTrace();
						}
					}
					try {
						String string = conditionDemo.pop();
						System.out.println("读取消息--->" + string);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}

			}
		});
		writeThread.start();
		readThread.start();
	}
}

程序运行结果如下:

我们成功模拟出了阻塞队列的效果,从上述代码和运行结果中我们可以明白当某个条件不满足的时候,当前线程是阻塞的,比如我们刚开始读线程在队列为空时,并没有继续读取消息。同时这个锁对象也是被释放掉了,否则写线程是获取不到锁对象的。

用 synchronized 当然也能实现阻塞队列,但写法不会这么优雅。

五、可重入锁和不可重入锁

可重入锁即获取锁对象后,获取到该锁对象的线程可以继续获取该锁对象。

不可重入锁就为之相反,获取锁对象后,获取到该锁对象的线程不可以继续获取该锁对象。

synchronized 和 ReentrantLock 都是可重入锁,可重入锁主要解决的就是死锁问题。先看一段代码:

package com.qinshou.resume.thread;

public class ReentrantLockDemo {
	public synchronized void method1() {
		method2();
	}

	public synchronized void method2() {

	}
}

我们知道 synchronized 修饰实例方法时,获取的是当前对象的锁,那 method1 和 method2 都是用的同一把锁,试想一下,如果这个锁是不可重入锁,那 method1 调用 method2 时,因为 method1 还没有执行完成,所以还没有释放锁,导致 method2 获取不到锁,一直阻塞,于是 method1 也用于无法结束,导致死锁。

ReentrantLock 解决的也是同一问题,但是需要注意的是由于 ReentrantLock 是手动加锁和释放锁,所以加锁多少次,就一定要释放多少次锁。ReentrantLock 实现可重入锁的关键代码在于 Sync 的 tryLock() 方法:

abstract static class Sync extends AbstractQueuedSynchronizer {
	...
    final boolean tryLock() {
        Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (getExclusiveOwnerThread() == current) {
			// 已经加过锁,但之前获取到锁的线程就是当前线程,可以再次获取锁,锁数量 +1
            if (++c < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(c);
            return true;
        }
        return false;
    }
	...
}

六、公平锁与非公平锁

synchronized 是非公平锁。ReentrantLock 默认也是非公平锁,但是它在创建时指定入参为 true 来构建公平锁:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁是所有抢占锁资源的线程不分先来后到地竞争锁,公平锁则是每次等待最长的线程获取锁,即先来先得。

上面看到 ReentrantLock 会根据构造方法参数不同而创建不同的 Sync 对象,那我们就简单看看这两个对象获取锁的地方:

static final class NonfairSync extends Sync {
    ...
    protected final boolean tryAcquire(int acquires) {
        if (getState() == 0 && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

static final class FairSync extends Sync {
    ...
    protected final boolean tryAcquire(int acquires) {
        if (getState() == 0 && !hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
}

其中 getState() 方法是用来判断当前是否有线程占有锁的,如果没有,则返回 0,否则大于 0。compareAndSetState() 方法主要是用于抢占锁。NonfairSync 的 tryAcquire() 方法只是调用了 getState() 方法后就直接开始尝试抢占锁。而 FairSync 的 tryAcquire() 方法在调用了 getState() 方法后还调用了 hasQueuedPredecessors() 判断当前队列中是否还有线程在等待获取锁,如果有的话,就不尝试抢占锁,而是乖乖去队尾排队了,以此来实现公平。

eg:

package com.qinshou.resume.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class FairLockDemo {
	private static class MyRunnable implements Runnable {
		private final Lock mLock;

		public MyRunnable(Lock lock) {
			super();
			mLock = lock;
		}

		@Override
		public void run() {
			for (int i = 0; i < 5; i++) {
				mLock.lock();
				try {
					Thread.sleep(100);
					System.out.println(Thread.currentThread().getName());
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					mLock.unlock();
				}
			}
		}

	}

	public static void main(String[] args) throws InterruptedException {
		// Unfair
		System.out.println("Unfair lock");
		Lock lock = new ReentrantLock();
		Runnable runnable = new MyRunnable(lock);
		Thread thread0 = new Thread(runnable);
		Thread thread1 = new Thread(runnable);
		thread0.start();
		thread1.start();
		thread0.join();
		thread1.join();
		// Fair
		System.out.println("Fair lock");
		lock = new ReentrantLock(true);
		runnable = new MyRunnable(lock);
		Thread thread2 = new Thread(runnable);
		Thread thread3 = new Thread(runnable);
		thread2.start();
		thread3.start();
		thread2.join();
		thread3.join();
	}
}

用非公平锁时,两个线程获取到锁的几率毫无规律,使用公平锁后,两个线程交替获取到锁:

 相应的,由于公平锁会增加额外判断,因此性能会有所降低,所以在没有特别需求的情况下尽量使用非公平锁。

七、锁的其他概念

上文有提到 synchronized 在 JDK1.6 之后有过升级,优化的手段包括自适应自旋锁、轻量级锁、偏向锁、锁粗化、锁消除等,这些手段比较偏向于概念,不好模拟,所以简单记录一下这些概念。

1.偏向锁

偏向锁是指这个锁会偏向于第一个获得它的线程。如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将不需要再进行同步。

2.轻量级锁

当锁是偏向锁时,如果有其他线程访问锁,那偏向锁就会升级为轻量级锁,轻量级锁会用自旋的形式尝试获取锁。

3.重量级锁

自旋达到一次次数后还不能获取到锁,轻量级锁就会升级成重量级锁,也就是尝试获取锁的线程会以在没有获取到锁的时候就会进入阻塞状态。

4.自适应自旋锁

刚才说到轻量级锁提到了自旋,对于线程同步来说,线程状态之间的切换是比较重量级的,如果一个同步操作中逻辑代码执行的时间很短的话,那显然同步操作就很重了。如果多个线程并发时,后续获取锁的线程能够先在用户态下等待一下,如果持有锁的线程很快释放锁了,就不用切换到内核态了,那岂不是很棒?

自旋锁就是这样一个概念,在遇到同步操作时,先执行一段空循环,循环体内啥也不做,只是为了避免线程状态的切换。但是自旋操作也是会耗费 cpu 资源的,如果此时锁被占用,那影响倒还不大,但如果自旋时锁已释放,那 cpu 就是白白空转了。

在 JDK 1.4 中已有自旋锁,但默认是关闭的,JDK 1.6 才默认开启,因为 JDK 1.6 时引入了自适应自旋锁的概念,让自旋的时间不再固定,而是由上一次在该锁上自旋的时间决定,如果自旋成功,则下次自旋成功了,则下次自旋次数会增多,如果失败,则下次自旋的次数会减少甚至取消自旋。

5.锁粗化

锁粗化就是编译器在发现一段代码中频繁使用同一个锁对象时,会将所有的锁合并为一个。

举个栗子:

package com.qinshou.resume.thread;

import java.util.Hashtable;
import java.util.Map;

public class LockOptimizeDemo {
	Map<Integer, String> mHashtable = new Hashtable<>();

	public void lockCoarsening() {
		mHashtable.put(1, "A");
		mHashtable.put(2, "B");
		mHashtable.put(3, "C");
	}
}

上述代码中由于 Hashtable 是一个线程安全的集合,所以每次调用 put() 方法都是会加锁的,如果进行锁粗化的话就在第一个 put 前加锁,最后一次 put 后释放锁即可。

6.锁消除

锁消除的意思是如果编译器检测到锁对象只会被一个线程使用,不存在其他线程竞争的情况,那这个锁就是没有必要的,可以去掉。

举个栗子:

package com.qinshou.resume.thread;

import java.util.Hashtable;
import java.util.Map;

public class LockOptimizeDemo {

	public void lockRemove() {
		Map<Integer, String> hashtable = new Hashtable<>();
		hashtable.put(1, "A");
		hashtable.put(2, "B");
		hashtable.put(3, "C");
	}
}

上述代码中调用 put() 方法虽然会加锁,但是 hashtable 只是一个局部变量,并不会其他线程访问,所有同步是没有必要的,这个锁可以去掉。

八、总结

并发编程,博大精深,如果我们能很好的控制线程后,再对锁进制深入理解,相信对多线程编程得心应手不是问题。

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

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

相关文章

如何高效管理自己的电脑?文件再多也不乱!

视频教程&#xff1a;https://www.bilibili.com/video/BV18M41167jd/?share_sourcecopy_web&vd_source58093f138338062a30d9fe854824a90f 如何高效管理自己的电脑?文件再多也不乱!1. 盘符管理C盘只装系统软件&#xff0c;其他软件装到D盘绿色软件就是解压就能用&#xff…

[go学习笔记.第十八章.数据结构] 1.基本介绍,稀疏数组,队列(数组实现),链表

一.基本介绍 1.数据结构(算法)的介绍 (1).数据结构是一门研究算法的学科&#xff0c;自从有了编程语言也就有了数据结构,学好数据结构可以编写出更加漂亮&#xff0c;更加有效率的代码 (2).要学习好数据结构就要多多考虑如何将生活中遇到的问题用程序去实现解决 (3).程序&…

一 H5游戏的种类

游戏类型&#xff08;各举一个例子&#xff09;&#xff1a; 休闲益智:开心消消乐 角色扮演:阴阳师 动作冒险:神庙逃亡 棋牌游戏:欢乐斗地主 体育竞技:最强NBA 射击游戏:绝地求生 音乐游戏:节奏大师 模拟经营:模拟城市 策略游戏:植物大战僵尸 塔防游戏:保卫萝卜 游戏付费方式…

R语言最优聚类数目k改进kmean聚类算法

在本文中&#xff0c;我们将探讨应用聚类算法&#xff08;例如k均值和期望最大化&#xff09;来确定集群的最佳数量时所遇到的问题之一。 最近我们被客户要求撰写关于聚类的研究报告&#xff0c;包括一些图形和统计输出。从数据集本身来看&#xff0c;确定集群数量的最佳值的问…

FX粒子(Niagara系统)、潮湿贴花——简单雨和雨后潮湿

雨&#xff1a; 1、新建粒子&#xff0c;选择Fountain 喷泉模板 2、删除不用模块 3、添加box location并设置其大小&#xff0c;设置Spawn Rate使粒子数变多&#xff0c;这个参数也是后面用来控制开关雨效果的参数。 4、设置粒子形状&#xff0c;设置完粒子方向不对&#xff0…

11.28~12.4日学习总结

星期一&#xff0c;星期二&#xff1a; 把项目中关于评论模块的内容全部写完了&#xff0c;然后处理了一下逻辑。 星期二&#xff0c;星期三&#xff1a;开始学习webSocket的相关概念。 关于webSocket,其实和java的Socket原理相差不大&#xff0c;也是要建立一个通道进行信息…

JavaScript数据结构【数组】

一、JavaScript中数组一些方法 方法 描述 push 数组的末尾插入元素 unshift 数组的开头插入元素 pop 从数组的末尾删除元素 shift 数组的开头删除元素 splice 在数组的任意位置添加或删除元素 concat 连接两个或更多数组&#xff0c;并返回结果 filter 对数组中…

String类

String类常用的方法 字符串构造 String类的构造方法很多&#xff0c;常见的有以下的三种&#xff1a; //三种常用的构造方法 public class Test {public static void main(String[] args) {//使用常量串构造String s1 "hello world!";//直接new String对象构造St…

一个 Angular 程序员两年多的远程办公经验分享

笔者从 2020 年疫情爆发之前&#xff0c;一直从事后端开发工作。2020 年因为工作原因&#xff0c;加入了 SAP 一个代号为 Spartacus 的开源项目的开发团队。这个项目是一个基于 Angular 的电商 Storefront 框架&#xff0c;其代码贡献者来自全世界各个地区。 实际上&#xff0…

工薪族创业方案

这是一个创业的时代,想自己创业做老板的人越来越多,其中也包括众多上班族。所碰到的问题:时间紧、资金有限、经验缺乏、患得患失,是几乎所有想自主创业的上班族都会遇到的问题。有以下几个方案可以尝试尝试! 方案一:对于不想冒任何风险而又想尝一尝创业滋味的上班族来说…

jdk11新特性——局部变量类型推断(var ”关键字”)

目录一、什么是局部变量类型推断二、局部变量类型推断&#xff08;var ”关键字”&#xff09;2.1、根据右边的表达式自动推断类型2.2、var并不是关键字2.3、var声明变量和String声明变量作用相同三、var 语法&#xff1a;局部变量类型推断&#xff08;注意事项&#xff09;3.1…

jsp儿童网站系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 jsp 儿童网站系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开 发&#xff0c;数据库为Mysql&#xff0c;使用ja…

OpenShift 4 - 利用 RHSSO 实现应用认证和访问授权

《OpenShift / RHEL / DevSecOps / Ansible 汇总目录》 说明&#xff1a;本文已经在 OpenShift 4.11 环境中验证 文章目录安装环境部署应用并配置登录和授权部署应用配置 RHSSO 的用户和组配置应用身份认证配置应用访问授权为应用配置登录和访问授权参考本文将部署一个应用&…

[附源码]Python计算机毕业设计SSM进出口食品安全信息管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【Maven】jar包冲突原因与最优解决方案

【Maven】jar包冲突原因与最优解决方案 文章目录前言jar包冲突原因依赖传递冲突原因jar包冲突解决方案Maven默认处理策略排除依赖版本锁定maven-shade-plugin插件总结前言 你是否经常遇到这样的报错&#xff1a; java.lang.NoSuchMethodError java.lang.ClassNotFoundExcepti…

Endnote 中批量导出PDF

简介 最近正着手写一篇综述文章&#xff0c;来整体把握下自己研究领域的历史、方法、最新进展与趋势。由于需要对相关文献进行搜集、阅读和分类。庄小编使用 EndNote 来进行管理文献。 在使用较长时间后&#xff0c;整理了几个超级好用的小技巧。比如&#xff1a;自动导入某个…

kubernetes—Service介绍

Service介绍 在kubernetes中&#xff0c;pod是应用程序的载体&#xff0c;我们可以通过pod的ip来访问应用程序&#xff0c;但是pod的ip地址不是固定的&#xff0c;这也就意味着不方便直接采用pod的ip对服务进行访问。 为了解决这个问题&#xff0c;kubernetes提供了Service资…

错误: 找不到或无法加载主类 com.example.demo.DemoApplication 解决方案

使用场景&#xff1a;使用idea搭建springboot项目使&#xff0c;出现报错情况 错误: 找不到或无法加载主类 com.example.demo7.Demo7Application 解决方案&#xff1a; 方法一&#xff1a;取消勾选“将IDE构建/运行操作委托给Maven” 使用步骤&#xff1a;File > Setting…

Eyeshot 2023 预期计划 Eyeshot 2023 即将见面

Eyeshot 2023 预期--Crack--定制 Eyeshot 是一个基于 Microsoft .NET Framework 的 CAD 控件。它允许开发人员快速将 CAD 功能添加到 WinForms 和 WPF 应用程序。Eyeshot 提供了从头开始构建几何体、使用有限元方法对其进行分析并在其上生成刀具路径的工具。还可以使用 CAD 交…

Python源码剖析3-列表对象PyListObject

1、PyListObject对象 PyListObject 对象可以有效地支持插入,添加,删除等操作,在 Python 的列表中,无一例外地存放的都是 PyObject 的指针。所以实际上,你可以这样看待 Python 中的列表: vector<PyObject*>。 [listobject.h] typedef struct {PyObject_VAR_HEAD/* Vecto…