JAVA基础 - synchronized

看下面文字可以先看一下

JAVA基础-volatile
了解一下 什么是可见性。

目录

1. synchronized 特点

	 1.1 可重入性
	 1.2 不可中断性

2. synchronized 原理

3. monitor 原理

4. 锁的分析

	4.0  前置知识
	4.1 偏向锁
	4.2 轻量级锁
	4.3 重量级锁

5. synchronized


正文

1. synchronized 特点

1.1 可重入性

描述: 一个线程可以多次执行synchronized,重复获取同一把锁。
同一个线程获得锁之后,可以直接再次获取该锁。
原理:synchronize锁对象中有计数器 recursions 变量。会记录线程获得几次锁。


public static void main(String[] args) {
        Runnable sellTicket = new Runnable() {
            @Override
		public void run() {
		synchronized (Demo01.class) {
		System.out.println("我是run");
		test01(); }
		}
public void test01() { synchronized (Demo01.class) {
			System.out.println("我是test01"); }
		} 
	};
		new Thread(sellTicket).start();
		new Thread(sellTicket).start(); }
1.2 不可中断性?

描述:一个线程获得锁后,另外一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二线程会一直阻塞或等待,不可中断。

private static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "进入同步代码块");
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(runnable);
        t2.start();
        System.out.println("停止线程前");
        t2.interrupt();
        System.out.println("停止线程后");
        System.out.println(t1.getState());
        System.out.println(t2.getState());

    }
//输出:
// Thread-0进入同步代码块
// 停止线程前
// 停止线程后
// TIMED_WAITING
// BLOCKED

难道真的没有办法 可以控制了吗? 可以控制的。

    private static Lock lock = new ReentrantLock();
    private static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                boolean flag = false;
                try {
                    flag = lock.tryLock(3, TimeUnit.SECONDS);
                    if (flag) {
                        System.out.println(name + "获得锁");
                        Thread.sleep(4888);
                    } else {
                        System.out.println("没有获得锁");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (flag) {
                        lock.unlock();
                    }
                    System.out.println("释放锁");
                }
            }
        };
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(runnable);
        t2.start();
    }
//  输出
// Thread-0获得锁
// 没有获得锁
// 释放锁
// 释放锁

总结lock和synchronized:

  1. synchronized是关键字,而 lock是一个接口。
  2. synchronize会自动释放锁,而lock 必须手动释放锁。
  3. synchronize是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronize不能
  5. synchronize能锁住方法和代码块,而lock只能锁住代码块
  6. Lock可以使用读锁提高多线程读效率
  7. synchronize是非公平锁,ReentrantLock可以控制是否公平锁。

3.monitor 原理

一下代码 先反编译看:

// Test07Visibility.java
 //  执行 javap -p -v -c Test07Visibility.class  
  	private static  Object obj=new Object();
    public static  void main(String [] args){
        synchronized (obj){
            System.out.println(" 锁1  ");
        }
    }
    public synchronized  void test(){
        System.out.println(" 锁2  ");
    }

java 编译
monitorenter 红框1 JVM 规范中写到:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

翻译一下:
说明: 每一个对象都会和一个监视器monitor关联,监视器被占用时候会被锁住,其他线程无法来
获取该monitor,当jvm执行某个现场的某个方法内部的monitorenter时,它会尝试去获取当前对象
对应的monitor的所有权,其过程如下:

  1. 若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数值为1,当前线程成为monitor的owner(所有者)
  2. 若线程已经拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能 重新尝试获取monitor的所有权。

monitorenter小结:
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是jvm的线程执行到这个同步代码块
发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner,拥有这把所的线程,
recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待。

monitorenter 红框2 JVM 规范中写到:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

翻译一下:
1.能执行monitore exit 指令的线程一定是拥有当前对象的monitor的所有权的线程。
2.执行monitorexitexit时会将monitor的进入数减一,当monitor的进入数减为0时,当前线程退出monitor
,不在拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor得所有权。

monitorexit 释放锁
monitorexit 插入在方法结束处和异常处,jvm保证每个monitorenter必须有对应的monitoreexit。
可以参考 红框3

面试题synchroznied出现异常会释放锁吗?
答: 会释放锁.

monitor 存储结构

在HotSpot虚拟机忠, monitor 是由ObjectMonitor实现的,其源码是用C++来实现的,
位于HotSpot虚拟机源码 ObjectMonitor.hpp 文件

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;
 *   }

描述:

  1. _owner:初始化为NULL,当前线程占有该monitor时,owner标记为该线程的唯一标识。
    当线程释放monitor时,owner又恢复为Null,owner是一个临界资源,jvm是通过CAS操作来保证其线程安全带。
  2. _cxq:竞争队列,所有请求锁的线程首先会被这个队列中 (单向链接),_cxq是一个临界资源,jvm
    通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入node的next字段,_cxq指向新值(新线程),因此cxq是一个
    后进先出的stack(栈).
  3. entryList: cxq 队列中有资格成为候选资源的线程会被移动到该队列中。
  4. WaitSet: 因为调用wait方法而被阻塞的线程会被放在该队列中。

每一个java 对象都可以与一个监视器monitor 关联,我们可以把它理解为一把锁,当一个线程想要执行一段被synchronize圈起来的同步方法或者代码块时,该线程得先获取到synchronize修饰的对象对应的monitor

我们得java代码里不会显示地区创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:

monitor并不是随着对象创建而创建的。我们是通过synchronized修改事符告诉jvm需要为我们得某个对象
创建关联monitor对象

。每个线程都存在两个ObjectMonitor对象列表。分别为free和used列表
同时JVM也在维护着global lockList。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,
若不存在则从global list中申请。

在这里插入图片描述

分析 monitorenter

interpreterRuntime.cpp

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) { // 是否使用偏向锁
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
	ObjectSynchronizer::fast_enter(h_obj, elem->lock()true, CHECK); 
} else {
	ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); 
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj())"must be NULL or an object");
  1. 对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  2. 最终调用ObjectMonitor:enter (objectMonitor.cpp)
objectMonitor.cpp
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 ;
}
	// 线程重入,
	recursions++
    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 ;
}
// 省略一些代码 
	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); }

描述:
1.通过CAS尝试把monitor的owner字段设置为当前线程。
2. 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,
执行recursions++,记录重入的次数
3.如果当前线程时是一次进入该monitor,设置recursions为1,_owner为当前线程。该线程成功获得锁并返回
4.如果获取锁失败,则等待锁的释放。

分析 EnterI

void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
 // Try the lock - TATAS
if (TryLock (Self) > 0) {
    assert (_succ != Self, "invariant") ;
    assert (_owner == Self, "invariant") ;
    assert (_Responsible != Self, "invariant") ;
    return ;
}
if (TrySpin (Self) > 0) {
    assert (_owner == Self , "invariant") ;
    assert (_succ != Self , "invariant") ;
    assert (_Responsible != Self  , "invariant") ;
    return ;
}
// 省略部分代码
// 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ; ObjectWaiter node(Self) ;
	Self->_ParkEvent->reset() ;
	node._prev = (ObjectWaiter *) 0xBAD ;
	node.TState = ObjectWaiter::TS_CXQ ;
// 通过CAS把node节点push到_cxq列表中 
	ObjectWaiter * nxt ;
	for (;;) {
		node._next = nxt = _cxq ;
	if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
	// Interference - the CAS failed because _cxq changed. Just retry.
    // As an optional optimization we
    if (TryLock (Self) > 0) {
        assert (_succ != Self, "invariant") ;
        assert (_owner == Self, "invariant") ; 
        assert (_Responsible != Self, "invariant") ;
        return ;
	} 
}
// 省略部分代码 
	for (;;) { 
// 线程在被挂起前做一下挣扎,看能不能获取到锁 
		if (TryLock (Self) > 0) break ;
		assert (_owner != Self, "invariant") ;

		if ((SyncFlags & 2) && _Responsible == NULL) {
		 Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
	}
// park self
if (_Responsible == Self || (SyncFlags & 1)) {
	TEVENT (Inflated enter - park TIMED) ;
 	Self->_ParkEvent->park ((jlong) RecheckInterval) ;
		// Increase the RecheckInterval, but clamp the value. 
	RecheckInterval *= 8 ;
    if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
	TEVENT (Inflated enter - park UNTIMED) ;
	// 通过park将当前线程挂起,等待被唤醒
	Self->_ParkEvent->park() ; 
}
    if (TryLock(Self) > 0) break ;
// 省略部分代码
 }
// 省略部分代码
 }   

说明:
当线程被唤醒时,会从挂起的点继续执行,通过objectMoniter::TryLock尝试获取锁,
TryLock 方法实现如下:

int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
		void * own = _owner ;
		if (own != NULL) return 0 ;
		if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
			// Either guarantee _recursions == 0 or set _recursions = 0.
			assert (_recursions == 0"invariant") ;
			assert (_owner == Self, "invariant") ;
			// CONSIDER: set or assert that OwnerIsThread == 1
			 return 1 ;
}
// The lock had been free momentarily, but we lost the race to the lock.
// Interference -- the CAS failed.
// We can either return -1 or retry.
// Retry doesn't make as much sense because the lock was just acquired.
		if (true) return -1 ;
	}
 }

以上代码流程:
1.当前线程被封装成objectWaiter对象node,状态设置成为ObjectWaiter::TS_CXQ.
2在for循环中,通过CAS把node结点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点
push到_cxq列表中。
3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取锁,则通过park将
当前线程挂起,等待被唤醒。
4.当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock 尝试获取锁

monitor释放

当某个持有锁的线程执行完同步代码块,会进行锁的释放。给其他线程机会执行同步代码,
在hotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程。
具体实现位于ObjectMonitor的exit方法中

objectMonitor.cpp
oid ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
	 Thread * Self = THREAD ;
// 省略部分代码
	if (_recursions != 0) {
	_recursions--; // this is simple recursive enter
	TEVENT (Inflated exit - recursive) ;
	return ;
 }

// 省略部分代码 
	ObjectWaiter * w = NULL ;
	int QMode = Knob_QMode ;
	
// qmode = 2:直接绕过EntryList队列,从cxq队列中获取线程用于竞争锁
	if (QMode == 2 && _cxq != NULL) {
		w = _cxq ;
		assert (w != NULL"invariant") ;
		assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
		ExitEpilog (Self, w) ;
		return ;
}

// qmode =3:cxq队列插入EntryList尾部;
if (QMode == 3 && _cxq != NULL) {
     w = _cxq ;
     for (;;) {
		assert (w != NULL"Invariant") ;
		ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL,, "invariant") ;
	&_cxq, w) ; 
    if (u == w) break ;
    w=u;
}
	assert (w != NULL"Invariant") ;
	ObjectWaiter * q = NULL ;
	ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
 	 guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
	 p->TState = ObjectWaiter::TS_ENTER ;
	 p->_prev = q ;
	 q=p;
}
     ObjectWaiter * Tail ;
	for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
     if (Tail == NULL) {
            _EntryList = w ;
 	} else {
	Tail->_next = w ;
	 w->_prev = Tail ;
 } 
}

// qmode =4:cxq队列插入到_EntryList头部 
if (QMode == 4 && _cxq != NULL) {
         w = _cxq ;
         for (;;) {
			assert (w != NULL"Invariant") ;
			ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL&_cxq, w) ;
		if(u == w) break;
		 w=u;
}
assert (w != NULL"Invariant") ;

	ObjectWaiter * q = NULL ;
	ObjectWaiter * p ;
	for (p = w ; p != NULL ; p = p->_next) {
		guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; 
		p->TState = ObjectWaiter::TS_ENTER ;
		p->_prev = q ;
		q=p;
	}

	if (_EntryList != NULL) { 
		q->_next = _EntryList ;
		 _EntryList->_prev = q ;
	}
    _EntryList = w ;
}
	w = _EntryList  ;
	if (w != NULL) {
		assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
		ExitEpilog (Self, w) ;
		return ;
	}
	w = _cxq ;
	if (w == NULL) continue ;

	for (;;) {
		assert (w != NULL"Invariant") ;
		ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL&_cxq,
 	   if (u == w) break ;
		w=u; 
}
	TEVENT (Inflated exit - drain cxq into EntryList) ;
	assert (w != NULL"invariant") ;
	assert (_EntryList == NULL"invariant") ;
}

	ObjectWaiter * q = NULL ;
	ObjectWaiter * p ;
	for (p = w ; p != NULL ; p = p->_next) {
		guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
	 	p->TState = ObjectWaiter::TS_ENTER ;
		p->_prev = q ;
		q=p;
}		

	if (_EntryList != NULL) { 
		q->_next = _EntryList ; _EntryList->_prev = q ;
	}
 	   _EntryList = w ;
}

	w = _EntryList  ;
	if (w != NULL) {
		assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; ExitEpilog (Self, w) ;
		return ;
}
	w = _cxq ;
	if (w == NULL) continue ;

	for (;;) {
		assert (w != NULL"Invariant") ;
		ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL&_cxq,
 	   if (u == w) break ;
			w=u; 
	}

	TEVENT (Inflated exit - drain cxq into EntryList) ; 
	assert (w != NULL"invariant") ;
	assert (_EntryList == NULL"invariant") ;

	if (QMode == 1) {
		// QMode == 1 : drain cxq to EntryList, reversing order 
		// We also reverse the order of the list.
		ObjectWaiter * s = NULL ;
		ObjectWaiter * t = w ;
		ObjectWaiter * u = NULL ;
	while (t != NULL) {
		guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ; t->TState = ObjectWaiter::TS_ENTER ;
		u = t->_next ;
		t->_prev = u ;
		t->_next = s ; s = t;
		t=u;
}
	_EntryList = s ;
	assert (s != NULL"invariant") ;
	 } else {
		// QMode == 0 or QMode == 2
		_EntryList = w ;
		ObjectWaiter * q = NULL ;
		ObjectWaiter * p ;
	for (p = w ; p != NULL ; p = p->_next) {
		guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ;
		p->_prev = q ;
		q=p;
	}
 }
if (_succ != NULL) continue;
     w = _EntryList  ;
     if (w != NULL) {
		guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; ExitEpilog (Self, w) ;
		return ;
	}
  }
}
 

1.退出同步代码块会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。
2.根据不同的策略(由QMode指定),从cxq或者EntryList获取头节点,通过objectMonitor""EXITepilog方法
唤醒该节点封装线程,唤醒操作最终由unpark完成。实现如下:

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
	 assert (_owner == Self, "invariant") ;
	_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
	 ParkEvent * Trigger = Wakee->_event ;
	Wakee = NULL ;
   // Drop the lock
	OrderAccess::release_store_ptr (&_owner, NULL) ;
   OrderAccess::fence() ;
unpark()

   if (SafepointSynchronize::do_call_back()) {
      TEVENT (unpark before SAFEPOINT) ;
	// ST _owner vs LD in
}

DTRACE_MONITOR_PROBE(contended__exit, thisobject(), Self);
Trigger->unpark() ; // 唤醒之前被pack()挂起的线程.
   // Maintain stats and report events to JVMTI
	if (ObjectMonitor::_sync_Parks != NULL) { 
		ObjectMonitor::_sync_Parks->inc() ;
	} 
}

被唤醒的线程,会回到 EnterI 的600 行,继续执行monitor的竞争

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
	TEVENT (Inflated enter - park TIMED) ;
	Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value. 
	RecheckInterval *= 8 ;
    if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
	TEVENT (Inflated enter - park UNTIMED) ;
	Self->_ParkEvent->park() ;
 }
if (TryLock(Self) > 0) break ;

monitor是重量级锁

可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。
(这是未优化之前)

CSDN 篇幅写不下
请看: JAVA基础 - 锁

参考

https://www.bilibili.com/video/av82315694
理解Java内存模型
你了解Java内存结构么(Java7、8、9内存结构的区别)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值