「万字长文,建议收藏」关于Synchronized锁升级,你该了解这些
nanshan 2024-11-10 10:10 24 浏览 0 评论
前言
毫无疑问,synchronized是我们用过的第一个并发关键字,很多博文都在讲解这个技术。不过大多数讲解还停留在对synchronized的使用层面,其底层的很多原理和优化,很多人可能并不知晓。因此本文将通过对synchronized的大量C源码分析,让大家对他的了解更加透彻点。
本篇将从为什么要引入synchronized,常见的使用方式,存在的问题以及优化部分这四个方面描述,话不多说,开始表演。
可见性问题及解决
概念描述
指一个线程对共享变量进行修改,另一个能立刻获取到修改后的最新值。
代码展示
类:
public class Example1 {
//1.创建共享变量
private static boolean flag = true;
public static void main(String[] args) throws Exception {
//2.t1空循环,如果flag为true,不退出
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(!flag){
System.out.println("进入if");
break;
}
}
}
});
t1.start();
Thread.sleep(2000L);
//2.t2修改flag为false
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
flag = false;
System.out.println("修改了");
}
});
t2.start();
}
} 复制代码
运行结果:
分析
这边先要了解下Java的内存模式,不明白的可点击传送门,todo。
下图线程t1,t2从主内存分别获取flag=true,t1空循环,直到flag为false的时候退出循环。t2拿到flag的值,将其改为false,并写入到主内存。此时主内存和线程t2的工作内存中flag均为false,但是线程t1工作内存中的flag还是true,所以一直退不了循环,程序将一直执行。
synchronized如何解决可见性
首先我们尝试在t1线程中加一行打印语句,看看效果。
代码:
public class Example1 {
//1.创建共享变量
private static boolean flag = true;
public static void main(String[] args) throws Exception {
//2.t1空循环,如果flag为true,不退出
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//新增的打印语句
System.out.println(flag);
if(!flag){
System.out.println("进入if");
break;
}
}
}
});
t1.start();
Thread.sleep(2000L);
//2.t2修改flag为false
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
flag = false;
System.out.println("修改了");
}
});
t2.start();
}
} 复制代码
运行结果:
我们发现if里面的语句已经打印出来了,线程1已经感知到线程2对flag的修改,即这条打印语句已经影响了可见性。这是为啥?
答案就是println中,我们看下源码:
println有个上锁的过程,即操作如下:
1.获取同步锁。
2.清空自己工作内存上的变量。
3.从主内存获取最新值,并加载到工作内存中。
4.打印并输出。
所以这里解释了为什么线程t1加了打印语句之后,t1立刻能感知t2对flag的修改。因为每次打印的时候其都从主内存上获取了最新值,当t2修改的时候,t1立刻从主内存获取了值,所以进入了if语句,并最终能跳出循环。
synchronized的原理就是清空自己工作内存上的值,通过将主内存最新值刷新到工作内存中,让各个线程能互相感知修改。
原子性问题及解决
概念描述
在一次或多个操作中,要不所有操作都执行,要不所有操作都不执行。
代码展示
类:
public class Example2 {
//1.定义全局变量number
private static int number = 0;
public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
number++;
}
};
//2.t1让其自增10000
Thread t1 = new Thread(runnable);
t1.start();
//3.t2让其自增10000
Thread t2 = new Thread(runnable);
t2.start();
//4.等待t1,t2运行结束
t1.join();
t2.join();
System.out.println("number=" + number);
}
} 复制代码
运行结果:
分析
每个线程执行的逻辑是循环1万次,每次加1,那我们希望的结果是2万,但是实际上结果是不足2万的。我们先用javap命令反汇编,我们看到很多代码,但是number++涉及的指令有四句,具体看第二张图。
如果有多条线程执行这段number++代码,当前number为0,线程1先执行到iconst_1指令,即将执行iadd操作,而线程2执行到getstatic指令,这个时候number值还没有改变,所以线程2获取到的静态字段是0,线程1执行完iadd操作,number变为1,线程2执行完iadd操作,number还是1。这个时候就发现问题了,做了两次number++操作,但是number只增加了1。
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半的时候,另外一个线程也有可能来操作共享变量,这个时候就出现了问题。
synchronized如何解决原子性问题
在上面的分析中,我们已经知道发生问题的原因,number++是由四条指令组成,没有保证原子操作。所以,我们只要将number++作为一个整体就行,即保证他的原子性。具体代码如下:
public class Example2 {
//1.定义全局变量number
private static int number = 0;
//新增一个静态变量object
private static Object object = new Object();
public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
//将number++的操作用object对象锁住
synchronized (object) {
number++;
}
}
};
//2.t1让其自增10000
Thread t1 = new Thread(runnable);
t1.start();
//3.t2让其自增10000
Thread t2 = new Thread(runnable);
t2.start();
//4.等待t1,t2运行结束
t1.join();
t2.join();
System.out.println("number=" + number);
}
} 复制代码
我们看到最终number为20000,那为什么要加上synchronized,结果就正确了?我们再反编译下Example2,可以看到在四行指令前后分别有monitorenter和monitorexist,线程1在执行中间指令时,其他线程不可以进入monitorenter,需要等线程1执行完monitorexist,其他进程才能继续monitorenter,进行自增操作。
有序性问题及解决
概念描述
代码中程序执行的顺序,Java在编译和运行时会对代码进行优化,这样会导致我们最终的执行顺序并不是我们编写代码的书写顺序。
代码展示
咱先来看一个概念,重排序,也就是语句的执行顺序会被重新安排。其主要分为三种:
1.编译器优化的重排序:可以重新安排语句的执行顺序。
2.指令级并行的重排序:现代处理器采用指令级并行技术,将多条指令重叠执行。
3.内存系统的重排序:由于处理器使用缓存和读写缓冲区,所以看上去可能是乱序的。
上面代码中的a = new A();可能被被JVM分解成如下代码:
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址复制代码复制代码
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象 复制代码复制代码
一旦假设发生了这样的重排序,比如线程A在执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候线程B进入了第一个语句,它会判断a不为空,即直接返回了a。其实这是一个未初始化完成的a,即会出现问题。
synchronized如何解决有序性问题
给上面的三个步骤加上一个synchronized关键字,即使发生重排序也不会出现问题。线程A在执行步骤1和步骤3时,线程B因为没法获取到锁,所以也不能进入第一个语句。只有线程A都执行完,释放锁,线程B才能重新获取锁,再执行相关操作。
synchronized的常见使用方式
修饰代码块(同步代码块)
synchronized (object) {
//具体代码
}复制代码
修饰方法
synchronized void test(){
//具体代码
}复制代码
synchronized不能继承?(插曲)
父类A:
public class A {
synchronized void test() throws Exception {
try {
System.out.println("main 下一步 sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("main 下一步 sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
}复制代码
子类B:(未重写test方法)
public class B extends A {
}复制代码
子类C:(重写test方法)
public class C extends A {
@Override
void test() throws Exception{
try {
System.out.println("sub 下一步 sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("sub 下一步 sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
} 复制代码
线程A:
public class ThreadA extends Thread {
private A a;
public void setter (A a) {
this.a = a;
}
@Override
public void run() {
try{
a.test();
}catch (Exception e){
}
}
}复制代码
线程B:
public class ThreadB extends Thread {
private B b;
public void setB(B b){
this.b=b;
}
@Override
public void run() {
try{
b.test();
}catch (Exception e){
}
}
} 复制代码
线程C:
public class ThreadC extends Thread{
private C c;
public void setC(C c){
this.c=c;
}
@Override
public void run() {
try{
c.test();
}catch (Exception e){
}
}
}复制代码
测试类test:
public class test {
public static void main(String[] args) throws Exception {
A a = new A();
ThreadA A1 = new ThreadA();
A1.setter(a);
A1.setName("A1");
A1.start();
ThreadA A2 = new ThreadA();
A2.setter(a);
A2.setName("A2");
A2.start();
A1.join();
A2.join();
System.out.println("=============");
B b = new B();
ThreadB B1 = new ThreadB();
B1.setB(b);
B1.setName("B1");
B1.start();
ThreadB B2 = new ThreadB();
B2.setB(b);
B2.setName("B2");
B2.start();
B1.join();
B2.join();
System.out.println("=============");
C c = new C();
ThreadC C1 = new ThreadC();
C1.setName("C1");
C1.setC(c);
C1.start();
ThreadC C2 = new ThreadC();
C2.setName("C2");
C2.setC(c);
C2.start();
C1.join();
C2.join();
}
}复制代码
运行结果:
子类B继承了父类A,但是没有重写test方法,ThreadB仍然是同步的。子类C继承了父类A,也重写了test方法,但是未明确写上synchronized,所以这个方法并不是同步方法。只有显式地写上synchronized关键字,才是同步方法。
所以synchronized不能继承这句话有歧义,我们只要记住子类如果想要重写父类的同步方法,synchronized关键字一定要显示写出,否则无效。
修饰静态方法
synchronized static void test(){
//具体代码
}复制代码
修饰类
synchronized (Example2.class) {
//具体代码
}复制代码
Java对象 Mark Word
在JVM中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐数据,如下图:
其中Mark Word值在不同锁状态下的展示如下:(重点看线程id,是否为偏向锁,锁标志位信息)
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。Talk is cheap. Show me the code. 咱来看代码。
- 我们想要看Java对象的Mark Word,先要加载一个jar包,在pom.xml添加即可。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>复制代码
- 新建一个对象A,拥有初始值为666的变量x。
public class A {
private int x=666;
}复制代码
- 新建一个测试类test,这涉及到刚才加载的jar,我们打印Java对象。
import org.openjdk.jol.info.ClassLayout;
public class test {
public static void main(String[] args) {
A a=new A();
System.out.println( ClassLayout.parseInstance(a).toPrintable());
}
}复制代码
- 我们发现对象头(object header)占了12个字节,为啥和上面说的16个字节不一样。
- 其实上是默认开启了指针压缩,我们需要关闭指针压缩,也就是添加-XX:-UseCompressedOops配置。
- 再次执行,发现对象头为16个字节。
偏向锁
什么是偏向锁
JDK1.6之前锁为重量级锁(待会说,只要知道他和内核交互,消耗资源),1.6之后Java设计人员发现很多情况下并不存在多个线程竞争的关系,所以为了资源问题引入了无锁,偏向锁,轻量级锁,重量级锁的概念。先说偏向锁,他是偏心,偏袒的意思,这个锁会偏向于第一个获取他的线程。
偏向锁演示
- 创建并启动一个线程,run方法里面用了synchronized关键字,功能是打印this的Java对象。
public class test {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
synchronized (this){
System.out.println(ClassLayout.parseInstance(this).toPrintable());
}
}
});
thread.start();
}
}复制代码
标红的地方为000,根据之前Mark Word在不同状态下的标志,得此为无锁状态。理论上一个线程使用synchronized关键字,应为偏向锁。
- 实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。
- 重新运行下代码,发现标红地方101,对比Mark Word在不同状态下的标志,得此状态为偏向锁。
偏向锁原理图解
- 在线程的run方法中,刚执行到synchronized,会判断当前对象是否为偏向锁和锁标志,没有任何线程执行该对象,我们可以看到是否为偏向锁为0,锁标志位01,即无锁状态。
- 线程会将自己的id赋值给markword,即将原来的hashcode值改为线程id,是否是偏向锁改为1,表示线程拥有对象锁,可以执行下面的业务逻辑。如果synchronized执行完,对象还是偏向锁状态;如果线程结束之后,会撤销偏向锁,将该对象还原成无锁状态。
- 如果同一个线程中又对该对象进行加锁操作,我们只要对比对象的线程id是否与线程id相同,如果相同即为线程锁重入问题。
优势
加锁和解锁不需要额外的消耗,和执行非同步方法相比只有纳秒级的差距。
白话翻译
线程1锁定对象this,他发现对象为无锁状态,所以将线程id赋值给对象的Mark Word字段,表示对象为线程1专用,即使他退出了同步代码,其他线程也不能使用该对象。
同学A去自习教室C,他发现教室无人,所以在门口写了个名字,表示当前教室有人在使用,这样即使他出去吃了饭,其他同学也不能使用这个房间。
轻量锁
什么是轻量级锁
在多线程交替同步代码块的情况下,线程间没有竞争,使用轻量级锁可以避免重量级锁引入的性能消耗。
轻量级图解
- 在刚才偏向锁的基础上,如果有另外一个线程也想错峰使用该资源,通过对比线程id是否相同,Java内存会立刻撤销偏向锁(需要等待全局安全点),进行锁升级的操作。
- 撤销完轻量级锁,会在线程1的方法栈中新增一个锁记录,对象的Mark Word与锁记录交换。
优势
线程不竞争的时候,避免直接使用重量级锁,提高了程序的响应速度。
白话翻译
在刚才偏向锁的基础上,另外一个线程也想要获取资源,所以线程1需要撤销偏向锁,升级为轻量锁。
同学A在使用自习教室外面写了自己的名字,所以同学B来也想要使用自习教室,他需要提醒同学A,不能使用偏向锁,同学A将自习教室门口的名字擦掉,换成了一个书包,里面是自己的书籍。这样在同学A不使用自习教室的时候,同学B也能使用自习教室,只需要将自己的书包也挂在外面即可。这样下次来使用的同学就能知道已经有人占用了该教室。
重量级锁
什么是重量级锁
当多线程之间发生竞争,Java内存会申请一个Monitor对象来实现。
重量级锁原理图解
在刚才的轻量级锁的基础上,线程2也想要申请资源,发现锁的标志位为00,即为轻量级锁,所以向内存申请一个Monitor,让对象的MarkWord指向Monitor地址,并将ower指针指向线程1的地址,线程2放在等待队列里面,等线程1指向完毕,释放锁资源。
Monitor源码分析
环境搭建
我们去官网http://openjdk.java.net/找下open源码,也可以通过其他途径下载。源码是C实现的,可以通过DEV C++工具打开,效果如下图:
构造函数
我们先看下\hotspot\src\share\vm\runtime\ObjectMonitor.hpp,以.hpp结尾的文件是导入的一些包和一些声明,之后可以被.cpp文件导入。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//线程重入次数
_object = NULL;//存储该monitor的对象
_owner = NULL;//标识拥有该monitor的线程
_WaitSet = NULL;//处于wait状态的线程,会加入到_waitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//多线程竞争锁时的单项列表
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁lock状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}复制代码
锁竞争的过程
我们先看下\hotspot\src\share\vm\interpreter\interpreterRuntime.cpp,IRT_ENTRY_NO_ASYNC即为锁竞争过程。
//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
//是否使用偏向锁,可加参数进行设置 if (UseBiasedLocking) { //如果可以使用偏向锁,即进入fast_enter
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {//如果不可以使用偏向锁,即进行slow_enter
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END复制代码
slow_enter实际上调用的ObjectMonitor.cpp的enter 方法
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
//通过CAS操作尝试将monitor的_owner设置为当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
//如果设置不成功,直接返回
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//如果_owner等于当前线程,重入数_recursions加1,直接返回
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
//如果当前线程第一次进入该monitor,设置重入数_recursions为1,_owner为当前线程,返回
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
//如果未抢到锁,则进行自旋优化,如果还未获取锁,则放入到list里面
// We've encountered genuine contention.
assert (Self->_Stalled == 0, "invariant") ;
Self->_Stalled = intptr_t(this) ;
// Try one round of spinning *before* enqueueing Self
// and before going through the awkward and expensive state
// transitions. The following spin is strictly optional ...
// Note that if we acquire the monitor from an initial spin
// we forgo posting JVMTI events and firing DTRACE probes.
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_recursions == 0 , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
Self->_Stalled = 0 ;
return ;
}
assert (_owner != Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (Self->is_Java_thread() , "invariant") ;
JavaThread * jt = (JavaThread *) Self ;
assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
assert (jt->thread_state() != _thread_blocked , "invariant") ;
assert (this->object() != NULL , "invariant") ;
assert (_count >= 0, "invariant") ;
// Prevent deflation at STW-time. See deflate_idle_monitors() and is_busy().
// Ensure the object-monitor relationship remains stable while there's contention.
Atomic::inc_ptr(&_count);
EventJavaMonitorEnter event;
{ // Change java thread status to indicate blocked on monitor enter.
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_enter()) {
JvmtiExport::post_monitor_contended_enter(jt, this);
}
OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);
Self->set_current_pending_monitor(this);
// TODO-FIXME: change the following for(;;) loop to straight-line code.
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
Atomic::dec_ptr(&_count);
assert (_count >= 0, "invariant") ;
Self->_Stalled = 0 ;
// Must either set _recursions = 0 or ASSERT _recursions == 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
// The thread -- now the owner -- is back in vm mode.
// Report the glorious news via TI,DTrace and jvmstat.
// The probe effect is non-trivial. All the reportage occurs
// while we hold the monitor, increasing the length of the critical
// section. Amdahl's parallel speedup law comes vividly into play.
//
// Another option might be to aggregate the events (thread local or
// per-monitor aggregation) and defer reporting until a more opportune
// time -- such as next time some thread encounters contention but has
// yet to acquire the lock. While spinning that thread could
// spinning we could increment JVMStat counters, etc.
DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_entered()) {
JvmtiExport::post_monitor_contended_entered(jt, this);
}
if (event.should_commit()) {
event.set_klass(((oop)this->object())->klass());
event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
event.commit();
}
if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
}
} 复制代码
白话翻译
同学A在使用自习教室的时候,同学B在同一时刻也想使用自习教室,那就发生了竞争关系。所以同学B在A运行过程中,加入等待队列。如果此时同学C也要使用该教室,也会加入等待队列。等同学A使用结束,同学B和C将竞争自习教室。
自旋优化
自旋优化比较简单,如果将其他线程加入等待队列,那之后唤醒并运行线程需要消耗资源,所以设计人员让其空转一会,看看线程能不能一会结束了,这样就不要在加入等待队列。
白话来说,如果同学A在使用自习教室,同学B可以回宿舍,等A使用结束再来,但是B回宿舍再来的过程需要1个小时,而A只要10分钟就结束了。所以B可以先不回宿舍,而是在门口等个10分钟,以防止来回时间的浪费。
结语
唉呀妈呀,终于结束了,累死了。终于将synchronized写完了,如果有不正确的地方,还需要各位指正。如果觉得写得还行,麻烦帮我点赞,评论哈。
相关推荐
- F5负载均衡器如何通过irules实现应用的灵活转发?
-
F5是非常强大的商业负载均衡器。除了处理性能强劲,以及高稳定性之外,F5还可以通过irules编写强大灵活的转发规则,实现web业务的灵活应用。irules是基于TCL语法的,每个iRules必须包含...
- 映射域名到NAS
-
前面介绍已经将域名映射到家庭路由器上,现在只需要在路由器上设置一下端口转发即可。假设NAS在内网的IP是192.168.1.100,NAS管理端口2000.你的域名是www.xxx.com,配置外部端...
- 转发(Forward)和重定向(Redirect)的区别
-
转发是服务器行为,重定向是客户端行为。转发(Forward)通过RequestDispatcher对象的forward(HttpServletRequestrequest,HttpServletRe...
- SpringBoot应用中使用拦截器实现路由转发
-
1、背景项目中有一个SpringBoot开发的微服务,经过业务多年的演进,代码已经累积到令人恐怖的规模,亟需重构,将之拆解成多个微服务。该微服务的接口庞大,调用关系非常复杂,且实施重构的人员大部分不是...
- 公司想搭建个网站,网站如何进行域名解析?
-
域名解析是将域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转...
- 域名和IP地址什么关系?如何通过域名解析IP?
-
一般情况下,访客通过域名和IP地址都能访问到网站,那么两者之间有什么关系吗?本文中科三方针对域名和IP地址的关系和区别,以及如何实现域名与IP的绑定做下介绍。域名与IP地址之间的关系IP地址是计算机的...
- 分享网站域名301重定向的知识
-
网站域名做301重定向操作时,一般需要由专业的技术来协助完成,如果用户自己在维护,可以按照相应的说明进行操作。好了,下面说说重点,域名301重定向的操作步骤。首先,根据HTTP协议,在客户端向服务器发...
- NAS外网到底安全吗?一文看懂HTTP/HTTPS和SSL证书
-
本内容来源于@什么值得买APP,观点仅代表作者本人|作者:可爱的小cherry搭好了NAS,但是不懂做好网络加密,那么隐私泄露也会随时发生!大家好,这里是Cherry,喜爱折腾、玩数码,热衷于分享数...
- ForwardEmail免费、开源、加密的邮件转发服务
-
ForwardEmail是一款免费、加密和开源的邮件转发服务,设置简单只需4步即可正常使用,通过测试来看也要比ImprovMX好得多,转发近乎秒到且未进入垃圾箱(仅以Mailbox.org发送、Out...
- 使用CloudFlare进行域名重定向
-
当网站变更域名的时候,经常会使用域名重定向的方式,将老域名指向到新域名,这通常叫做:URL转发(URLFORWARDING),善于使用URL转发,对SEO来说非常有用,因为用这种方式能明确告知搜索引...
- 要将端口5002和5003通过Nginx代理到一个域名上的操作笔记
-
要将端口5002和5003通过Nginx代理到域名www.4rvi.cn的不同路径下,请按照以下步骤配置Nginx:步骤说明创建或编辑Nginx配置文件通常配置文件位于/etc/nginx/sites...
- SEO浅谈:网站域名重定向的三种方式
-
在大多数情况下,我们输入网站访问网站的时候,很难发现www.***.com和***.com的区别,因为一般的网站主,都会把这两个域名指向到同一网站。但是对于网站运营和优化来说,www.***.com和...
- 花生壳出现诊断域名与转发服务器ip不一致的解决办法
-
出现诊断域名与转发服务器ip不一致您可以:1、更改客户端所处主机的drs为223.5.5.5备用dns为119.29.29.29;2、在windows上进入命令提示符输入ipconfig/flush...
- 涨知识了!带你认识什么是域名
-
1、什么是域名从技术角度来看,域名是在Internet上解决IP地址对应的一种方法。一个完整的域名由两个或两个以上部分组成,各部分之间用英文的句号“.”来分隔。如“abc.com”。其中“com”称...
- 域名被跳转到其他网站是怎么回事
-
当你输入域名时被跳转到另一个网站,这可能是由几种原因造成的:一、域名可能配置了域名转发服务。无论何时有人访问域名,比如.com、.top等,都会自动重定向到另一个指定的URL,这通常是在域名注册商设...
你 发表评论:
欢迎- 一周热门
-
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
[常用工具] OpenCV_contrib库在windows下编译使用指南
-
WindowsServer2022|配置NTP服务器的命令
-
Ubuntu系统Daphne + Nginx + supervisor部署Django项目
-
WIN11 安装配置 linux 子系统 Ubuntu 图形界面 桌面系统
-
解决Linux终端中“-bash: nano: command not found”问题
-
NBA 2K25虚拟内存不足/爆内存/内存占用100% 一文速解
-
Linux 中的文件描述符是什么?(linux 打开文件表 文件描述符)
-
K3s禁用Service Load Balancer,解决获取浏览器IP不正确问题
-
- 最近发表
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)