面试中关于多线程同步,你必须要思考的问题

ReentrantLock的实现网上有很多文章了,本篇文章会简单介绍下其java层实现,重点放在分析竞争锁失败后如何阻塞线程。
因篇幅有限,synchronized的内容将会放到下篇文章。

Java Lock的实现

ReentrantLock是jdk中常用的锁实现,其实现逻辑主语基于AQS(juc包中的大多数同步类实现都是基于AQS);接下来会简单介绍AQS的大致原理,关于其实现细节以及各种应用,之后会写一篇文章具体分析。

AQS

AQS是类AbstractQueuedSynchronizer.java的简称,JUC包下的ReentrantLock、CyclicBarrier、CountdownLatch都使用到了AQS。

其大致原理如下:

  1. AQS维护一个叫做state的int型变量和一个双向链表,state用来表示同步状态,双向链表存储的是等待锁的线程
  2. 加锁时首先调用tryAcquire尝试获得锁,如果获得锁失败,则将线程插入到双向链表中,并调用LockSupport.park()方法阻塞当前线程。
  3. 释放锁时调用LockSupport.unpark()唤起链表中的第一个节点的线程。被唤起的线程会重新走一遍竞争锁的流程。

其中tryAcquire方法是抽象方法,具体实现取决于实现类,我们常说的公平锁和非公平锁的区别就在于该方法的实现。

ReentrantLock

ReentrantLock分为公平锁和非公平锁,我们只看公平锁。
ReentrantLock.lock会调用到ReentrantLock#FairSync.lock中:

FairSync.java

  static final class FairSync extends Sync {final void lock() {acquire(1);}/*** Fair version of tryAcquire.  Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}}AbstractQueuedSynchronizer.javapublic final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

 

可以看到FairSync.lock调用了AQS的acquire方法,而在acquire中首先调用tryAcquire尝试获得锁,以下两种情况返回true:

  1. state==0(代表没有线程持有锁),且等待队列为空(公平的实现),且cas修改state成功。
  2. 当前线程已经获得了锁,这次调用是重入

如果tryAcquire失败则调用acquireQueued阻塞当前线程。acquireQueued最终会调用到LockSupport.park()阻塞线程。

LockSupport.park

个人认为,要深入理解锁机制,一个很重要的点是理解系统是如何阻塞线程的。

LockSupport.javapublic static void park(Object blocker) {Thread t = Thread.currentThread();setBlocker(t, blocker);UNSAFE.park(false, 0L);setBlocker(t, null);}

  

park方法的参数blocker是用于负责这次阻塞的同步对象,在AQS的调用中,这个对象就是AQS本身。我们知道synchronized关键字是需要指定一个对象的(如果作用于方法上则是当前对象或当前类),与之类似blocker就是LockSupport指定的对象。

park方法调用了native方法UNSAFE.park,第一个参数代表第二个参数是否是绝对时间,第二个参数代表最长阻塞时间。

其实现如下,只保留核心代码,完整代码看查看unsafe.cpp

 Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time){...
 thread->parker()->park(isAbsolute != 0, time);...}

park方法在os_linux.cpp中(其他操作系统的实现在os_xxx中)

void Parker::park(bool isAbsolute, jlong time) {...//获得当前线程Thread* thread = Thread::current();assert(thread->is_Java_thread(), "Must be JavaThread");JavaThread *jt = (JavaThread *)thread;//如果当前线程被设置了interrupted标记,则直接返回if (Thread::is_interrupted(thread, false)) {return;}if (time > 0) {//unpacktime中根据isAbsolute的值来填充absTime结构体,isAbsolute为true时,time代表绝对时间且单位是毫秒,否则time是相对时间且单位是纳秒//absTime.tvsec代表了对于时间的秒//absTime.tv_nsec代表对应时间的纳秒unpackTime(&absTime, isAbsolute, time);}//调用mutex trylock方法if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {return;}//_counter是一个许可的数量,跟ReentrantLock里定义的许可变量基本都是一个原理。 unpack方法调用时会将_counter赋值为1。//_counter>0代表已经有人调用了unpark,所以不用阻塞int status ;if (_counter > 0)  { // no wait needed_counter = 0;//释放mutex锁status = pthread_mutex_unlock(_mutex);return;}//设置线程状态为CONDVAR_WAITOSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);...//等待_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;pthread_cond_timedwait(&_cond[_cur_index], _mutex,  &absTime);...//释放mutex锁status = pthread_mutex_unlock(_mutex) ;}

 

park方法用POSIX的pthread_cond_timedwait方法阻塞线程,调用pthread_cond_timedwait前需要先获得锁,因此park主要流程为:

  1. 调用pthread_mutex_trylock尝试获得锁,如果获取锁失败则直接返回
  2. 调用pthread_cond_timedwait进行等待
  3. 调用pthread_mutex_unlock释放锁

另外,在阻塞当前线程前,会调用OSThreadWaitState的构造方法将线程状态设置为CONDVAR_WAIT,在Jvm中Thread状态枚举如下

  enum ThreadState {ALLOCATED,                    // Memory has been allocated but not initializedINITIALIZED,                  // The thread has been initialized but yet startedRUNNABLE,                     // Has been started and is runnable, but not necessarily runningMONITOR_WAIT,                 // Waiting on a contended monitor lockCONDVAR_WAIT,                 // Waiting on a condition variableOBJECT_WAIT,                  // Waiting on an Object.wait() callBREAKPOINTED,                 // Suspended at breakpointSLEEPING,                     // Thread.sleep()ZOMBIE                        // All done, but not reclaimed yet
};

 

Linux的timedwait

由上文我们可以知道LockSupport.park方法最终是由POSIX的
pthread_cond_timedwait的方法实现的。
我们现在就进一步看看pthread_mutex_trylock,pthread_cond_timedwait,pthread_mutex_unlock这几个方法是如何实现的。

Linux系统中相关代码在glibc库中。

pthread_mutex_trylock

先看trylock的实现,
代码在glibc的pthread_mutex_trylock.c文件中,该方法代码很多,我们只看主要代码

//pthread_mutex_t是posix中的互斥锁结构体
int
__pthread_mutex_trylock (mutex)pthread_mutex_t *mutex;
{int oldval;pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
switch (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex),PTHREAD_MUTEX_TIMED_NP)){case PTHREAD_MUTEX_ERRORCHECK_NP:case PTHREAD_MUTEX_TIMED_NP:case PTHREAD_MUTEX_ADAPTIVE_NP:/* Normal mutex.  */if (lll_trylock (mutex->__data.__lock) != 0)break;/* Record the ownership.  */mutex->__data.__owner = id;++mutex->__data.__nusers;return 0;}} //以下代码在lowlevellock.h中  
   #define __lll_trylock(futex) \(atomic_compare_and_exchange_val_acq (futex, 1, 0) != 0)#define lll_trylock(futex) __lll_trylock (&(futex))

 

mutex默认用的是PTHREAD_MUTEX_NORMAL类型(与PTHREAD_MUTEX_TIMED_NP相同);
因此会先调用lll_trylock方法,lll_trylock实际上是一个cas操作,如果mutex->__data.__lock==0则将其修改为1并返回0,否则返回1。

如果成功,则更改mutex中的owner为当前线程。

pthread_mutex_unlock

pthread_mutex_unlock.cint
internal_function attribute_hidden
__pthread_mutex_unlock_usercnt (mutex, decr)pthread_mutex_t *mutex;int decr;
{if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP)== PTHREAD_MUTEX_TIMED_NP){/* Always reset the owner field.  */normal:mutex->__data.__owner = 0;if (decr)/* One less user.  */--mutex->__data.__nusers;/* Unlock.  */lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex));return 0;}}
pthread_mutex_unlock将mutex中的owner清空,并调用了lll_unlock方法lowlevellock.h#define __lll_unlock(futex, private)                          \((void) ({                                      \int *__futex = (futex);                              \int __val = atomic_exchange_rel (__futex, 0);                  \\if (__builtin_expect (__val > 1, 0))                      \lll_futex_wake (__futex, 1, private);                      \}))
#define lll_unlock(futex, private) __lll_unlock(&(futex), private)#define lll_futex_wake(ftx, nr, private)                \
({                                    \DO_INLINE_SYSCALL(futex, 3, (long) (ftx),                \__lll_private_flag (FUTEX_WAKE, private),        \(int) (nr));                    \_r10 == -1 ? -_retval : _retval;                    \
})

 

lll_unlock分为两个步骤:

  1. 将futex设置为0并拿到设置之前的值(用户态操作)
  2. 如果futex之前的值>1,代表存在锁冲突,也就是说有线程调用了FUTEX_WAIT在休眠,所以通过调用系统函数FUTEX_WAKE唤醒休眠线程

FUTEX_WAKE 在上一篇文章有分析,futex机制的核心是当获得锁时,尝试cas更改一个int型变量(用户态操作),如果integer原始值是0,则修改成功,该线程获得锁,否则就将当期线程放入到 wait queue中,wait queue中的线程不会被系统调度(内核态操作)。

futex变量的值有3种:0代表当前锁空闲,1代表有线程持有当前锁,2代表存在锁冲突。futex的值初始化时是0;当调用try_lock的时候会利用cas操作改为1(见上面的trylock函数);当调用lll_lock时,如果不存在锁冲突,则将其改为1,否则改为2。

#define __lll_lock(futex, private)                          \((void) ({                                      \int *__futex = (futex);                              \if (__builtin_expect (atomic_compare_and_exchange_bool_acq (__futex,      \1, 0), 0))    \{                                          \if (__builtin_constant_p (private) && (private) == LLL_PRIVATE)          \__lll_lock_wait_private (__futex);                      \else                                      \__lll_lock_wait (__futex, private);                      \}                                          \}))
#define lll_lock(futex, private) __lll_lock (&(futex), private)void
__lll_lock_wait_private (int *futex)
{
//第一次进来的时候futex==1,所以不会走这个ifif (*futex == 2)lll_futex_wait (futex, 2, LLL_PRIVATE);
//在这里会把futex设置成2,并调用futex_wait让当前线程等待while (atomic_exchange_acq (futex, 2) != 0)lll_futex_wait (futex, 2, LLL_PRIVATE);
}

 

pthread_cond_timedwait

pthread_cond_timedwait用于阻塞线程,实现线程等待,
代码在glibc的pthread_cond_timedwait.c文件中,代码较长,你可以先简单过一遍,看完下面的分析再重新读一遍代码

int
int
__pthread_cond_timedwait (cond, mutex, abstime)pthread_cond_t *cond;pthread_mutex_t *mutex;const struct timespec *abstime;
{struct _pthread_cleanup_buffer buffer;struct _condvar_cleanup_buffer cbuffer;int result = 0;/* Catch invalid parameters.  */if (abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000)return EINVAL;int pshared = (cond->__data.__mutex == (void *) ~0l)? LLL_SHARED : LLL_PRIVATE;//1.获得cond锁lll_lock (cond->__data.__lock, pshared);//2.释放mutex锁int err = __pthread_mutex_unlock_usercnt (mutex, 0);if (err){lll_unlock (cond->__data.__lock, pshared);return err;}/* We have one new user of the condvar.  *///每执行一次wait(pthread_cond_timedwait/pthread_cond_wait),__total_seq就会+1++cond->__data.__total_seq;//用来执行futex_wait的变量++cond->__data.__futex;//标识该cond还有多少线程在使用,pthread_cond_destroy需要等待所有的操作完成cond->__data.__nwaiters += 1 << COND_NWAITERS_SHIFT;/* Remember the mutex we are using here.  If there is already adifferent address store this is a bad user bug.  Do not storeanything for pshared condvars.  *///保存mutex锁if (cond->__data.__mutex != (void *) ~0l)cond->__data.__mutex = mutex;/* Prepare structure passed to cancellation handler.  */cbuffer.cond = cond;cbuffer.mutex = mutex;/* Before we block we enable cancellation.  Therefore we have toinstall a cancellation handler.  */__pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer);/* The current values of the wakeup counter.  The "woken" countermust exceed this value.  *///记录futex_wait前的__wakeup_seq(为该cond上执行了多少次sign操作+timeout次数)和__broadcast_seq(代表在该cond上执行了多少次broadcast)unsigned long long int val;unsigned long long int seq;val = seq = cond->__data.__wakeup_seq;/* Remember the broadcast counter.  */cbuffer.bc_seq = cond->__data.__broadcast_seq;while (1){//3.计算要wait的相对时间
      struct timespec rt;{
#ifdef __NR_clock_gettimeINTERNAL_SYSCALL_DECL (err);int ret;ret = INTERNAL_VSYSCALL (clock_gettime, err, 2,(cond->__data.__nwaiters& ((1 << COND_NWAITERS_SHIFT) - 1)),&rt);
# ifndef __ASSUME_POSIX_TIMERSif (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (ret, err), 0)){struct timeval tv;(void) gettimeofday (&tv, NULL);/* Convert the absolute timeout value to a relative timeout.  */rt.tv_sec = abstime->tv_sec - tv.tv_sec;rt.tv_nsec = abstime->tv_nsec - tv.tv_usec * 1000;}else
# endif{/* Convert the absolute timeout value to a relative timeout.  */rt.tv_sec = abstime->tv_sec - rt.tv_sec;rt.tv_nsec = abstime->tv_nsec - rt.tv_nsec;}
#else/* Get the current time.  So far we support only one clock.  */struct timeval tv;(void) gettimeofday (&tv, NULL);/* Convert the absolute timeout value to a relative timeout.  */rt.tv_sec = abstime->tv_sec - tv.tv_sec;rt.tv_nsec = abstime->tv_nsec - tv.tv_usec * 1000;
#endif}if (rt.tv_nsec < 0){rt.tv_nsec += 1000000000;--rt.tv_sec;}/*---计算要wait的相对时间 end---- *///是否超时/* Did we already time out?  */if (__builtin_expect (rt.tv_sec < 0, 0)){//被broadcast唤醒,这里疑问的是,为什么不需要判断__wakeup_seq?if (cbuffer.bc_seq != cond->__data.__broadcast_seq)goto bc_out;goto timeout;}unsigned int futex_val = cond->__data.__futex;//4.释放cond锁,准备waitlll_unlock (cond->__data.__lock, pshared);/* Enable asynchronous cancellation.  Required by the standard.  */cbuffer.oldtype = __pthread_enable_asynccancel ();//5.调用futex_wait/* Wait until woken by signal or broadcast.  */err = lll_futex_timed_wait (&cond->__data.__futex,futex_val, &rt, pshared);/* Disable asynchronous cancellation.  */__pthread_disable_asynccancel (cbuffer.oldtype);//6.重新获得cond锁,因为又要访问&修改cond的数据了lll_lock (cond->__data.__lock, pshared);//__broadcast_seq值发生改变,代表发生了有线程调用了广播if (cbuffer.bc_seq != cond->__data.__broadcast_seq)goto bc_out;//判断是否是被sign唤醒的,sign会增加__wakeup_seq//第二个条件cond->__data.__woken_seq != val的意义在于//可能两个线程A、B在wait,一个线程调用了sign导致A被唤醒,这时B因为超时被唤醒//对于B线程来说,执行到这里时第一个条件也是满足的,从而导致上层拿到的result不是超时//所以这里需要判断下__woken_seq(即该cond已经被唤醒的线程数)是否等于__wakeup_seq(sign执行次数+timeout次数)val = cond->__data.__wakeup_seq;if (val != seq && cond->__data.__woken_seq != val)break;/* Not woken yet.  Maybe the time expired?  */if (__builtin_expect (err == -ETIMEDOUT, 0)){timeout:/* Yep.  Adjust the counters.  */++cond->__data.__wakeup_seq;++cond->__data.__futex;/* The error value.  */result = ETIMEDOUT;break;}}//一个线程已经醒了所以这里__woken_seq +1++cond->__data.__woken_seq;bc_out://
  cond->__data.__nwaiters -= 1 << COND_NWAITERS_SHIFT;/* If pthread_cond_destroy was called on this variable already,notify the pthread_cond_destroy caller all waiters have leftand it can be successfully destroyed.  */if (cond->__data.__total_seq == -1ULL&& cond->__data.__nwaiters < (1 << COND_NWAITERS_SHIFT))lll_futex_wake (&cond->__data.__nwaiters, 1, pshared);//9.cond数据修改完毕,释放锁lll_unlock (cond->__data.__lock, pshared);/* The cancellation handling is back to normal, remove the handler.  */__pthread_cleanup_pop (&buffer, 0);//10.重新获得mutex锁err = __pthread_mutex_cond_lock (mutex);return err ?: result;
}

 

上面的代码虽然加了注释,但相信大多数人第一次看都看不懂。
我们来简单梳理下,上面代码有两把锁,一把是mutex锁,一把cond锁。另外,在调用pthread_cond_timedwait前后必须调用pthread_mutex_lock(&mutex);pthread_mutex_unlock(&mutex);加/解mutex锁。

因此pthread_cond_timedwait的使用大致分为几个流程:

  1. 加mutex锁(在pthread_cond_timedwait调用前)
  2. 加cond锁
  3. 释放mutex锁
  4. 修改cond数据
  5. 释放cond锁
  6. 执行futex_wait
  7. 重新获得cond锁
  8. 比较cond的数据,判断当前线程是被正常唤醒的还是timeout唤醒的,需不需要重新wait
  9. 修改cond数据
  10. 是否cond锁
  11. 重新获得mutex锁
  12. 释放mutex锁(在pthread_cond_timedwait调用后)

看到这里,你可能有几点疑问:为什么需要两把锁?mutex锁和cond锁的作用是什么?

mutex锁

说mutex锁的作用之前,我们回顾一下java的Object.wait的使用。Object.wait必须是在synchronized同步块中使用。试想下如果不加synchronized也能运行Object.wait的话会存在什么问题?

Object condObj=new Object();
voilate int flag = 0;
public void waitTest(){if(flag == 0){condObj.wait();}
}
public void notifyTest(){flag=1;condObj.notify();
}

 

如上代码,A线程调用waitTest,这时flag==0,所以准备调用wait方法进行休眠,这时B线程开始执行,调用notifyTest将flag置为1,并调用notify方法,注意:此时A线程还没调用wait,所以notfiy没有唤醒任何线程。然后A线程继续执行,调用wait方法进行休眠,而之后不会有人来唤醒A线程,A线程将永久wait下去!

Object condObj=new Object();
voilate int flag = 0;
public void waitTest(){synchronized(condObj){if(flag == 0){condObj.wait();}}}
public void notifyTest(){synchronized(condObj){flag=1;condObj.notify();}
}

 

在有锁保护下的情况下, 当调用condObj.wait时,flag一定是等于0的,不会存在一直wait的问题。

回到pthread_cond_timedwait,其需要加mutex锁的原因就呼之欲出了:保证wait和其wait条件的原子性

不管是glibc的pthread_cond_timedwait/pthread_cond_signal还是java层的Object.wait/Object.notify,Jdk AQS的Condition.await/Condition.signal,所有的Condition机制都需要在加锁环境下才能使用,其根本原因就是要保证进行线程休眠时,条件变量是没有被篡改的。

注意下mutex锁释放的时机,回顾上文中pthread_cond_timedwait的流程,在第2步时就释放了mutex锁,之后调用futex_wait进行休眠,为什么要在休眠前就释放mutex锁呢?原因也很简单:如果不释放mutex锁就开始休眠,那其他线程就永远无法调用signal方法将休眠线程唤醒(因为调用signal方法前需要获得mutex锁)。

在线程被唤醒之后还要在第10步中重新获得mutex锁是为了保证锁的语义(思考下如果不重新获得mutex锁会发生什么)。

cond锁

cond锁的作用其实很简单: 保证对象cond->data的线程安全。
pthread_cond_timedwait时需要修改cond->data的数据,如增加__total_seq(在这个cond上一共执行过多少次wait)增加__nwaiters(现在还有多少个线程在wait这个cond),所有在修改及访问cond->data时需要加cond锁。

这里我没想明白的一点是,用mutex锁也能保证cond->data修改的线程安全,只要晚一点释放mutex锁就行了。为什么要先释放mutex,重新获得cond来保证线程安全? 是为了避免mutex锁住的范围太大吗?

该问题的答案可以见评论区@11800222 的回答:

mutex锁不能保护cond->data修改的线程安全,调用signal的线程没有用mutex锁保护修改cond的那段临界区。

pthread_cond_wait/signal这一对本身用cond锁同步就能睡眠唤醒。
wait的时候需要传入mutex是因为睡眠前需要释放mutex锁,但睡眠之前又不能有无锁的空隙,解决办法是让mutex锁在cond锁上之后再释放。
而signal前不需要释放mutex锁,在持有mutex的情况下signal,之后再释放mutex锁。

如何唤醒休眠线程

唤醒休眠线程的代码比较简单,主要就是调用lll_futex_wake。

int
__pthread_cond_signal (cond)pthread_cond_t *cond;
{int pshared = (cond->__data.__mutex == (void *) ~0l)? LLL_SHARED : LLL_PRIVATE;//因为要操作cond的数据,所以要加锁lll_lock (cond->__data.__lock, pshared);/* Are there any waiters to be woken?  */if (cond->__data.__total_seq > cond->__data.__wakeup_seq){//__wakeup_seq为执行sign与timeout次数的和++cond->__data.__wakeup_seq;++cond->__data.__futex;...//唤醒wait的线程lll_futex_wake (&cond->__data.__futex, 1, pshared);}/* We are done.  */lll_unlock (cond->__data.__lock, pshared);return 0;
}

 

End

本文对Java简单介绍了ReentrantLock实现原理,对LockSupport.park底层实现pthread_cond_timedwait机制做了详细分析。

看完这篇文章,你可能还会有疑问:Synchronized锁的实现和ReentrantLock是一样的吗?Thread.sleep/Object.wait休眠线程的原理和LockSupport.park有什么区别?linux内核层的futex的具体是如何实现的?

 

原文:Java架构笔记

免费Java高级资料需要自己领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。                
传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

转载于:https://www.cnblogs.com/yuxiang1/p/11239825.html

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

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

相关文章

C语言学习,关于fflush 和setvbuf

最近学习C语言的时候&#xff0c;学到文件的输入和输出函数&#xff0c; 对fflush和setvbuf 一直很困惑&#xff0c;现在虽然没有解开&#xff0c;但是有了一点浅显的理解。 1、ffulsh 针对的是输出流&#xff0c;是将输出缓存中的数据推到指向的文件里。 2、如果想清空输入缓…

可怜的mysql

唉&#xff0c;今天刚看到新闻&#xff0c;mysql 5.1 GA 虽然正式发布&#xff0c;但是却有一堆bug。 连mysql的创始人自己都批评sun不应该在未修复重大bug的前提下发布mysql 5.1 GA. 可怜的mysql,可怜的sun转载于:https://www.cnblogs.com/nevernet/archive/2008/12/04/134726…

linux查看用户、创建用户、设置密码、修改用户、删除用户命令

查看用户 tail -1 /etc/passwd tail -1 /etc/shadow id alex echo 123 |passwd --stdin alex # 设置密码&#xff0c;不需要交互[rootlocalhost ~]# tail -l /etc/passwd rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin nfsnobody:x:65534:65534:Anonymous NFS …

c# 笔记 数据类型转换 数组 函数

1、数据类型的转换&#xff08;cast&#xff09;包括隐性转换和显性转换。 当目标类型一定能满足源类型转换后的要求的话就是隐性转换&#xff0c;不需要任何代码直接转换&#xff0c;如果目标类型不一定能满足源类 型转换后的要求的话就是显性转换&#xff0c;显性转换需要在要…

使用外星人进行测试:如何使用Arquillian测试JPA类型转换器

该帖子与 Aslak Knutsen &#xff08; aslakknutsen &#xff09;一起撰写。 JPA类型转换器为定义实体属性如何持久存储到数据库提供了一种简便的方法。 您可以使用它们来实现许多不同的功能&#xff0c;例如&#xff0c;如上一篇文章中所示&#xff1a;加密数据&#xff1a; …

关于 C语言的 按位取反 ~

1、相关概念&#xff1a; 不管是正整数 还是 负整数在计算机中都是以 补码的形式存在的&#xff1b; 取反&#xff1a;0变1&#xff0c;1变0 就叫做取反&#xff0c;取反 符号位也要改变&#xff1b; 反码&#xff1a;符号位不变&#xff0c;其他位置0变1&#xff0c;1变0&a…

英语句型之展现问题篇3

表达不确定的用词&#xff1a;May, seem, be likely to, possible, probably, perhaps, be said that, be said to...51. In my mind, the following factors/reasons/causes need to be taken into consideration我认为&#xff0c;我们需要考虑下列因素/原因&#xff1a;52. …

【noip模拟赛5】任务分配 降维dp

描述 现有n个任务,要交给A和B完成。每个任务给A或给B完成&#xff0c;所需的时间分别为ai和bi。问他们完成所有的任务至少要多少时间。 输入 第一行一个正整数n&#xff0c;表示有n个任务。接下来有n行&#xff0c;每行两个正整数ai&#xff0c;bi。 输出 一个数&#xff0c;他…

解决win7下无法安装突击者NO.69驱动,“WINDOWS已找到设备的驱动程序,但在试图安装它时错误”...

本人装的是win7旗舰版&#xff0c;由于是ghost安装的&#xff0c;缺少一些文件&#xff0c;之前一直无法正确安装突击者电子狗的驱动程序&#xff0c;老是显示“WINDOWS已找到设备的驱动程序&#xff0c;但在试图安装它时错误”&#xff0c;baidu、google了几天都没解决&#x…

C语言 按位或 正整数与负整数 之间

按位或&#xff1a; 两个位 比对时&#xff0c;如果有一个位 为1&#xff0c;结果就为1&#xff1b; 按位或 时 &#xff0c;都是 按照补码来比对的 &#xff0c;正数的补码 是 本身&#xff0c;负数的补码 不是本身 所以 正负数按位或 要注意&#xff1a; 举例子&#xff1a;…

使用自定义日志记录处理程序在JBoss AS 7中跟踪SQL语句

使用ORM从您的特定数据库中提取数据&#xff0c;并让它创建和发布您必须亲自编写的所有SQL语句似乎很方便。 这就是使ORM解决方案受欢迎的原因。 但是它也有一个缺点&#xff1a;由于ORM为您做了很多工作&#xff0c;因此您在某种程度上失去了对生成的SQL的控制&#xff0c;您…

C语言 >> 右移位运算符的原理和一些概念

1、右移位于左移位不同 左移位 不管是 逻辑移位 算术移位 都是低位补0&#xff1b; 右移位 的 逻辑移位和算术移位不同&#xff0c; 算术移位高位补符号位&#xff0c;逻辑移位 高位补0&#xff1b; 右移位 使用逻辑移位的话 需要强制转换成unsigned 无符号型&#xff1b; 2…

去除git版本控制

命令&#xff1a;find . -name ".git" | xargs rm –Rflinux $ find . -type d -iname __pycache__ -exec rm -rf {} \;转载于:https://www.cnblogs.com/gispathfinder/p/10555347.html

PHP基础语法6

//PHP循环语句//for循环for ($i 0; $i < 18; $i) {echo $i . <br>;}//while循环$l 0;while ($l < 18) {echo $l . <br>;$l;}//foreache()用于数组循环的语句$arr [a > 1, b > 2];foreach ($arr as $key > $value) {echo $key . . $value . <…

如何在Java中找到整数的质因数–因式分解

编程课程中的常见家庭作业/任务之一是关于Prime Factorization。 要求您编写一个程序以找到给定整数的素因子 。 一个数字的质数因子是将精确地除以给定数字的所有质数。 例如&#xff0c;35的素数因子分别是7和5&#xff0c;它们本身都是素数&#xff0c;并且精确地除以35。上…

Arduino Serial系列函数 有关print read 的总结

总结一下 在学习arduino srial函数时 的几个知识点&#xff1a; /*** 汇总一下Serial.print输出的一些情况&#xff0c;后面部分要和Serial.read配合使用&#xff1b;* 1. print 输出字符 和int数的结果&#xff0c;* 2. print 输出字符串和一连串的数字* 3. read 读取一个字符…

C#经典名著:《C#入门经典》(第4版)

博客园专题&#xff1a;http://book.cnblogs.com/zt/begin_csharp/ 作  者&#xff1a; &#xff08;美&#xff09;沃森&#xff08;Watson&#xff0c;K.&#xff09;&#xff0c;&#xff08;美&#xff09;内格尔&#xff08;Nagel&#xff0c;C.&#xff09; 等著&#…

inheritPrototypal.js

// 原型式继承// 其基本思路是借助原型可以基于已有的对象创建新的对象function object(o){function F(){}F.prototype o;return new F();}var person {name: "Tom",friends: ["Jack", "John", "Kim"]};var David object(person);…

vue 用户输入搜索 与无限下拉

vue项目中,用户输入关键字搜索,并且手机端做无限下拉 watch: {getListForm.searchKey(val) { this.radioChange(); // 还有其他逻辑,内部调用getDeviceList}} 1 getDeviceList() {2 apiGetDeviceList(Qs.stringify(this.getListForm)).then(res > {3 …

您必须学习Java 8的函数式编程吗?

我最近一直在研究Java 8&#xff0c;并掌握了Manning出版的“ Java 8 In Action” 。 让我印象深刻的第一件事是Java 8的独特销售主张是函数式编程。 函数现在是一流的变量&#xff0c;您可以像int或String一样在代码中传递它们。 这是一个很大的变化。 近年来&#xff0c;功能…