并发编程10大坑,你踩过几个?

目录

前言

1. SimpleDateFormat线程不安全

2. 双重检查锁的漏洞

3. volatile的原子性

4. 死锁

4.1 缩小锁的范围

4.2 保证锁的顺序

5. 没释放锁

6. HashMap导致内存溢出

7. 使用默认线程池

8. @Async注解的陷阱

9. 自旋锁浪费cpu资源

10. ThreadLocal用完没清空


前言

对于从事后端开发的同学来说,并发编程肯定再熟悉不过了。

说实话,在java中并发编程是一大难点,至少我是这么认为的。不光理解起来比较费劲,使用起来更容易踩坑。

不信,让继续往下面看。

今天重点跟大家一起聊聊并发编程的10个坑,希望对你有帮助。

1. SimpleDateFormat线程不安全

在java8之前,我们对时间的格式化处理,一般都是用的SimpleDateFormat类实现的。例如:

@Service
public class SimpleDateFormatService {public Date time(String time) throws ParseException {SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return dateFormat.parse(time);}
}

如果你真的这样写,是没问题的。

就怕哪天抽风,你觉得dateFormat是一段固定的代码,应该要把它抽取成常量。

于是把代码改成下面的这样:

@Service
public class SimpleDateFormatService {private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public Date time(String time) throws ParseException {return dateFormat.parse(time);}
}

dateFormat对象被定义成了静态常量,这样就能被所有对象共用。

如果只有一个线程调用time方法,也不会出现问题。

但Serivce类的方法,往往是被Controller类调用的,而Controller类的接口方法,则会被tomcat线程池调用。换句话说,可能会出现多个线程调用同一个Controller类的同一个方法,也就是会出现多个线程会同时调用time方法的情况。

而time方法会调用SimpleDateFormat类的parse方法:

@Override
public Date parse(String text, ParsePosition pos) {...Date parsedDate;try {parsedDate = calb.establish(calendar).getTime();...} catch (IllegalArgumentException e) {pos.errorIndex = start;pos.index = oldStart;return null;}return parsedDate;
} 

该方法会调用establish方法:

Calendar establish(Calendar cal) {...//1.清空数据cal.clear();//2.设置时间cal.set(...);//3.返回return cal;
}

其中的步骤1、2、3是非原子操作。

但如果cal对象是局部变量还好,坏就坏在parse方法调用establish方法时,传入的calendar是SimpleDateFormat类的父类DateFormat的成员变量:

public abstract class DateFormat extends Forma {....protected Calendar calendar;...
}

这样就可能会出现多个线程,同时修改同一个对象即:dateFormat,他的同一个成员变量即:Calendar值的情况。

这样可能会出现,某个线程设置好了时间,又被其他的线程修改了,从而出现时间错误的情况。

那么,如何解决这个问题呢?

  1. SimpleDateFormat类的对象不要定义成静态的,可以改成方法的局部变量。

  2. 使用ThreadLocal保存SimpleDateFormat类的数据。

  3. 使用java8的DateTimeFormatter类。

2. 双重检查锁的漏洞

单例模式无论在实际工作,还是在面试中,都出现得比较多。

我们都知道,单例模式有:饿汉模式懒汉模式两种。

饿汉模式代码如下:

public class SimpleSingleton {//持有自己类的引用private static final SimpleSingleton INSTANCE = new SimpleSingleton();//私有的构造方法private SimpleSingleton() {}//对外提供获取实例的静态方法public static SimpleSingleton getInstance() {return INSTANCE;}
}

使用饿汉模式的好处是:没有线程安全的问题,但带来的坏处也很明显。

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?

还真是啊。

这个时候你也许会想到,不用提前实例化对象,在真正使用的时候再实例化不就可以了?

这就是我接下来要介绍的:懒汉模式

具体代码如下:

public class SimpleSingleton2 {private static SimpleSingleton2 INSTANCE;private SimpleSingleton2() {}public static SimpleSingleton2 getInstance() {if (INSTANCE == null) {INSTANCE = new SimpleSingleton2();}return INSTANCE;}
}

示例中的INSTANCE对象一开始是空的,在调用getInstance方法才会真正实例化。

嗯,不错不错。但这段代码还是有问题。

假如有多个线程中都调用了getInstance方法,那么都走到 if (INSTANCE == null) 判断时,可能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,即INSTANCE对象被创建了多次,违背了只创建一个INSTANCE对象的初衷。

为了解决饿汉模式懒汉模式各自的问题,于是出现了:双重检查锁

具体代码如下:

public class SimpleSingleton4 {private static SimpleSingleton4 INSTANCE;private SimpleSingleton4() {}public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {synchronized (SimpleSingleton4.class) {if (INSTANCE == null) {INSTANCE = new SimpleSingleton4();}}}return INSTANCE;}
}

需要在synchronized前后两次判空。

但我要告诉你的是:这段代码有漏洞的。

有什么问题?

public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {//1synchronized (SimpleSingleton4.class) {//2if (INSTANCE == null) {//3INSTANCE = new SimpleSingleton4();//4}}}return INSTANCE;//5
}

getInstance方法的这段代码,我是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行。

但是java虚拟机实际上会做一些优化,对一些代码指令进行重排。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下:

public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {//1if (INSTANCE == null) {//3synchronized (SimpleSingleton4.class) {//2INSTANCE = new SimpleSingleton4();//4}}}return INSTANCE;//5
}

原来如此,那有什么办法可以解决呢?

答:可以在定义INSTANCE是加上volatile关键字。具体代码如下:

public class SimpleSingleton7 {private volatile static SimpleSingleton7 INSTANCE;private SimpleSingleton7() {}public static SimpleSingleton7 getInstance() {if (INSTANCE == null) {synchronized (SimpleSingleton7.class) {if (INSTANCE == null) {INSTANCE = new SimpleSingleton7();}}}return INSTANCE;}
}

volatile关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止指令重排

双重检查锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

此外,如果你想了解更多单例模式的细节问题,可以看看我的另一篇文章《单例模式,真不简单》

3. volatile的原子性

从前面我们已经知道volatile,是一个非常不错的关键字,它能保证变量在多个线程中的可见性,它也能禁止指令重排,但是不能保证原子性

使用volatile关键字禁止指令重排,前面已经说过了,这里就不聊了。

可见性主要体现在:一个线程对某个变量修改了,另一个线程每次都能获取到该变量的最新值。

先一起看看反例:

public class VolatileTest extends Thread {private  boolean stopFlag = false;public boolean isStopFlag() {return stopFlag;}@Overridepublic void run() {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}stopFlag = true;System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);}public static void main(String[] args) {VolatileTest vt = new VolatileTest();vt.start();while (true) {if (vt.isStopFlag()) {System.out.println("stop");break;}}}
}

上面这段代码中,VolatileTest是一个Thread类的子类,它的成员变量stopFlag默认是false,在它的run方法中修改成了true。

然后在main方法的主线程中,用vt.isStopFlag()方法判断,如果它的值是true时,则打印stop关键字。

那么,如何才能让stopFlag的值修改了,在主线程中通过vt.isStopFlag()方法,能够获取最新的值呢?

正例如下:

public class VolatileTest extends Thread {private volatile boolean stopFlag = false;public boolean isStopFlag() {return stopFlag;}@Overridepublic void run() {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}stopFlag = true;System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);}public static void main(String[] args) {VolatileTest vt = new VolatileTest();vt.start();while (true) {if (vt.isStopFlag()) {System.out.println("stop");break;}}}
}

volatile关键字修饰stopFlag即可。

下面重点说说volatile的原子性问题。

使用多线程给count加1,代码如下:

public class VolatileTest {public volatile int count = 0;public void add() {count++;}public static void main(String[] args) {final VolatileTest test = new VolatileTest();for (int i = 0; i < 20; i++) {new Thread() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {test.add();}};}.start();}while (Thread.activeCount() > 2) {//保证前面的线程都执行完Thread.yield();}System.out.println(test.count);}
}

执行结果每次都不一样,但可以肯定的是count值每次都小于20000,比如:19999。

这个例子中count是成员变量,虽说被定义成了volatile的,但由于add方法中的count++是非原子操作。在多线程环境中,count++的数据可能会出现问题。

由此可见,volatile不能保证原子性

那么,如何解决这个问题呢?

答:使用synchronized关键字。

改造后的代码如下:

public class VolatileTest {public int count = 0;public synchronized void add() {count++;}public static void main(String[] args) {final VolatileTest test = new VolatileTest();for (int i = 0; i < 20; i++) {new Thread() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {test.add();}};}.start();}while (Thread.activeCount() > 2) {//保证前面的线程都执行完Thread.yield();}System.out.println(test.count);}
}

4. 死锁

死锁可能是大家都不希望遇到的问题,因为一旦程序出现了死锁,如果没有外力的作用,程序将会一直处于资源竞争的假死状态中。

死锁代码如下:

public class DeadLockTest {public static String OBJECT_1 = "OBJECT_1";public static String OBJECT_2 = "OBJECT_2";public static void main(String[] args) {LockA lockA = new LockA();new Thread(lockA).start();LockB lockB = new LockB();new Thread(lockB).start();}}class LockA implements Runnable {@Overridepublic void run() {synchronized (DeadLockTest.OBJECT_1) {try {Thread.sleep(500);synchronized (DeadLockTest.OBJECT_2) {System.out.println("LockA");}} catch (InterruptedException e) {e.printStackTrace();}}}
}class LockB implements Runnable {@Overridepublic void run() {synchronized (DeadLockTest.OBJECT_2) {try {Thread.sleep(500);synchronized (DeadLockTest.OBJECT_1) {System.out.println("LockB");}} catch (InterruptedException e) {e.printStackTrace();}}}
}

一个线程在获取OBJECT_1锁时,没有释放锁,又去申请OBJECT_2锁。而刚好此时,另一个线程获取到了OBJECT_2锁,也没有释放锁,去申请OBJECT_1锁。由于OBJECT_1和OBJECT_2锁都没有释放,两个线程将一起请求下去,陷入死循环,即出现死锁的情况。

那么如果避免死锁问题呢?

4.1 缩小锁的范围

出现死锁的情况,有可能是像上面那样,锁范围太大了导致的。

那么解决办法就是缩小锁的范围

具体代码如下:

class LockA implements Runnable {@Overridepublic void run() {synchronized (DeadLockTest.OBJECT_1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (DeadLockTest.OBJECT_2) {System.out.println("LockA");}}
}class LockB implements Runnable {@Overridepublic void run() {synchronized (DeadLockTest.OBJECT_2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (DeadLockTest.OBJECT_1) {System.out.println("LockB");}}
}

在获取OBJECT_1锁的代码块中,不包含获取OBJECT_2锁的代码。同时在获取OBJECT_2锁的代码块中,也不包含获取OBJECT_1锁的代码。

4.2 保证锁的顺序

出现死锁的情况说白了是,一个线程获取锁的顺序是:OBJECT_1和OBJECT_2。而另一个线程获取锁的顺序刚好相反为:OBJECT_2和OBJECT_1。

那么,如果我们能保证每次获取锁的顺序都相同,就不会出现死锁问题。

具体代码如下:

class LockA implements Runnable {@Overridepublic void run() {synchronized (DeadLockTest.OBJECT_1) {try {Thread.sleep(500);synchronized (DeadLockTest.OBJECT_2) {System.out.println("LockA");}} catch (InterruptedException e) {e.printStackTrace();}}}
}class LockB implements Runnable {@Overridepublic void run() {synchronized (DeadLockTest.OBJECT_1) {try {Thread.sleep(500);synchronized (DeadLockTest.OBJECT_2) {System.out.println("LockB");}} catch (InterruptedException e) {e.printStackTrace();}}}
}

两个线程,每个线程都是先获取OBJECT_1锁,再获取OBJECT_2锁。

5. 没释放锁

在java中除了使用synchronized关键字,给我们所需要的代码块加锁之外,还能通过Lock关键字加锁。

使用synchronized关键字加锁后,如果程序执行完毕,或者程序出现异常时,会自动释放锁。

但如果使用Lock关键字加锁后,需要开发人员在代码中手动释放锁。

例如:

public class LockTest {private final ReentrantLock rLock = new ReentrantLock();public void fun() {rLock.lock();try {System.out.println("fun");} finally {rLock.unlock();}}
}

代码中先创建一个ReentrantLock类的实例对象rLock,调用它的lock方法加锁。然后执行业务代码,最后再finally代码块中调用unlock方法。

但如果你没有在finally代码块中,调用unlock方法手动释放锁,线程持有的锁将不会得到释放。

6. HashMap导致内存溢出

HashMap在实际的工作场景中,使用频率还是挺高的,比如:接收参数,缓存数据,汇总数据等等。

但如果你在多线程的环境中使用HashMap,可能会导致非常严重的后果。

@Service
public class HashMapService {private Map<Long, Object> hashMap = new HashMap<>();public void add(User user) {hashMap.put(user.getId(), user.getName());}
}

在HashMapService类中定义了一个HashMap的成员变量,在add方法中往HashMap中添加数据。在controller层的接口中调用add方法,会使用tomcat的线程池去处理请求,就相当于在多线程的场景下调用add方法。

在jdk1.7中,HashMap使用的数据结构是:数组+链表。如果在多线程的情况下,不断往HashMap中添加数据,它会调用resize方法进行扩容。该方法在复制元素到新数组时,采用的头插法,在某些情况下,会导致链表会出现死循环。

死循环最终结果会导致:内存溢出

此外,如果HashMap中数据非常多,会导致链表很长。当查找某个元素时,需要遍历某个链表,查询效率不太高。

为此,jdk1.8之后,将HashMap的数据结构改成了:数组+链表+红黑树

如果同一个数组元素中的数据项小于8个,则还是用链表保存数据。如果大于8个,则自动转换成红黑树。

为什么要用红黑树?

答:链表的时间复杂度是O(n),而红黑树的时间复杂度是O(logn),红黑树的复杂度是优于链表的。

既然这样,为什么不直接使用红黑树?

答:树节点所占存储空间是链表节点的两倍,节点少的时候,尽管在时间复杂度上,红黑树比链表稍微好一些。但是由于红黑树所占空间比较大,HashMap综合考虑之后,认为节点数量少的时候用占存储空间更多的红黑树不划算。

jdk1.8中HashMap就不会出现死循环?

答:错,它在多线程环境中依然会出现死循环。在扩容的过程中,在链表转换为树的时候,for循环一直无法跳出,从而导致死循环。

那么,如果想多线程环境中使用HashMap该怎么办呢?

答:使用ConcurrentHashMap

7. 使用默认线程池

我们都知道jdk1.5之后,提供了ThreadPoolExecutor类,用它可以自定义线程池

线程池的好处有很多,比如:

  1. 降低资源消耗:避免了频繁的创建线程和销毁线程,可以直接复用已有线程。而我们都知道,创建线程是非常耗时的操作。

  2. 提供速度:任务过来之后,因为线程已存在,可以拿来直接使用。

  3. 提高线程的可管理性:线程是非常宝贵的资源,如果创建过多的线程,不仅会消耗系统资源,甚至会影响系统的稳定。使用线程池,可以非常方便的创建、管理和监控线程。

当然jdk为了我们使用更便捷,专门提供了:Executors类,给我们快速创建线程池。

该类中包含了很多静态方法:

  • newCachedThreadPool:创建一个可缓冲的线程,如果线程池大小超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool:创建一个固定大小的线程池,如果任务数量超过线程池大小,则将多余的任务放到队列中。

  • newScheduledThreadPool:创建一个固定大小,并且能执行定时周期任务的线程池。

  • newSingleThreadExecutor:创建只有一个线程的线程池,保证所有的任务安装顺序执行。

在高并发的场景下,如果大家使用这些静态方法创建线程池,会有一些问题。

那么,我们一起看看有哪些问题?

  • newFixedThreadPool:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

  • newSingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

  • newCachedThreadPool:允许创建的线程数是Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

那我们该怎办呢?

优先推荐使用ThreadPoolExecutor类,我们自定义线程池。

具体代码如下:

ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数10, //maximumPoolSize 线程池中最大线程数60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue(500), //队列new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略

顺便说一下,如果是一些低并发场景,使用Executors类创建线程池也未尝不可,也不能完全一棍子打死。在这些低并发场景下,很难出现OOM问题,所以我们需要根据实际业务场景选择。

8. @Async注解的陷阱

之前在java并发编程中实现异步功能,一般是需要使用线程或者线程池

线程池的底层也是用的线程。

而实现一个线程,要么继承Thread类,要么实现Runnable接口,然后在run方法中写具体的业务逻辑代码。

开发spring的大神们,为了简化这类异步操作,已经帮我们把异步功能封装好了。spring中提供了@Async注解,我们可以通过它即可开启异步功能,使用起来非常方便。

具体做法如下:

1.在springboot的启动类上面加上@EnableAsync注解。

@EnableAsync
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

2.在需要执行异步调用的业务方法加上@Async注解。

@Service
public class CategoryService {@Asyncpublic void add(Category category) {//添加分类}
}

3.在controller方法中调用这个业务方法。

@RestController
@RequestMapping("/category")
public class CategoryController {@Autowiredprivate CategoryService categoryService;@PostMapping("/add")public void add(@RequestBody category) {categoryService.add(category);}
}

这样就能开启异步功能了。

是不是很easy?

但有个坏消息是:用@Async注解开启的异步功能,会调用AsyncExecutionAspectSupport类的doSubmit方法。

默认情况会走else逻辑。

而else的逻辑最终会调用doExecute方法:

protected void doExecute(Runnable task) {Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));thread.start();
}

我去,这不是每次都会创建一个新线程吗?

没错,使用@Async注解开启的异步功能,默认情况下,每次都会创建一个新线程。

如果在高并发的场景下,可能会产生大量的线程,从而导致OOM问题。

建议大家在@Async注解开启的异步功能时,请别忘了定义一个线程池

9. 自旋锁浪费cpu资源

在并发编程中,自旋锁想必大家都已经耳熟能详了。

自旋锁有个非常经典的使用场景就是:CAS(即比较和交换),它是一种无锁化思想(说白了用了一个死循环),用来解决高并发场景下,更新数据的问题。

而atomic包下的很多类,比如:AtomicInteger、AtomicLong、AtomicBoolean等,都是用CAS实现的。

我们以AtomicInteger类为例,它的incrementAndGet没有每次都给变量加1。

public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

它的底层就是用的自旋锁实现的:

public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;
}

在do...while死循环中,不停进行数据的比较和交换,如果一直失败,则一直循环重试。

如果在高并发的情况下,compareAndSwapInt会很大概率失败,因此导致了此处cpu不断的自旋,这样会严重浪费cpu资源。

那么,如果解决这个问题呢?

答:使用LockSupport类的parkNanos方法。

具体代码如下:

private boolean compareAndSwapInt2(Object var1, long var2, int var4, int var5) {if(this.compareAndSwapInt(var1,var2,var4, var5)) {return true;} else {LockSupport.parkNanos(10);return false;}}

当cas失败之后,调用LockSupport类的parkNanos方法休眠一下,相当于调用了Thread.Sleep方法。这样能够有效的减少频繁自旋导致cpu资源过度浪费的问题。

10. ThreadLocal用完没清空

在java中保证线程安全的技术有很多,可以使用synchroized、Lock等关键字给代码块加锁。

但是它们有个共同的特点,就是加锁会对代码的性能有一定的损耗。

其实,在jdk中还提供了另外一种思想即:用空间换时间

没错,使用ThreadLocal类就是对这种思想的一种具体体现。

ThreadLocal为每个使用变量的线程提供了一个独立的变量副本,这样每一个线程都能独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal的用法大致是这样的:

  1. 先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。

public class CurrentUser {private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();public static void set(UserInfo userInfo) {THREA_LOCAL.set(userInfo);}public static UserInfo get() {THREA_LOCAL.get();}public static void remove() {THREA_LOCAL.remove();}
}
  1. 在业务代码中调用CurrentUser类。

public void doSamething(UserDto userDto) {UserInfo userInfo = convert(userDto);CurrentUser.set(userInfo);...//业务代码UserInfo userInfo = CurrentUser.get();...
}

在业务代码的第一行,将userInfo对象设置到CurrentUser,这样在业务代码中,就能通过CurrentUser.get()获取到刚刚设置的userInfo对象。特别是对业务代码调用层级比较深的情况,这种用法非常有用,可以减少很多不必要传参。

但在高并发的场景下,这段代码有问题,只往ThreadLocal存数据,数据用完之后并没有及时清理。

ThreadLocal即使使用了WeakReference(弱引用)也可能会存在内存泄露问题,因为 entry对象中只把key(即threadLocal对象)设置成了弱引用,但是value值没有。

那么,如何解决这个问题呢?

public void doSamething(UserDto userDto) {UserInfo userInfo = convert(userDto);try{CurrentUser.set(userInfo);...//业务代码UserInfo userInfo = CurrentUser.get();...} finally {CurrentUser.remove();}
}

需要在finally代码块中,调用remove方法清理没用的数据。

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

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

相关文章

CAP理论与MongoDB一致性、可用性的一些思考

大约在五六年前&#xff0c;第一次接触到了当时已经是hot topic的NoSql。不过那个时候学的用的都是mysql&#xff0c;Nosql对于我而言还是新事物&#xff0c;并没有真正使用&#xff0c;只是不明觉厉。但是印象深刻的是这么一张图片&#xff08;后来google到图片来自这里&#…

【开源】一个WPF开发的XML记事本

今天推荐一个WPF开源项目&#xff1a;XmlNotepad[1]。此仓库由 WPF开发者[2] 推荐&#xff0c;站长简单翻译分享。介绍XML Notepad[3] 是一个 Windows 程序&#xff0c;它为浏览和编辑 XML 文档提供了一个简单直观的用户界面。有四种安装方式&#xff1a;ClickOnce 安装程序[4]…

log4j2 mybatis 显示 sql 和 结果集

为什么80%的码农都做不了架构师&#xff1f;>>> 首先说明版本&#xff1a; log4j 版本是 第2版beta9&#xff0c;引用jar包 log4j-api-2.0-beta9.jar log4j-core-2.0-beta9.jar mybatis 是 mybatis-3.2.3 log4j2 配置 如下&#xff0c;命名为 log4j2.xml&#xff0…

深入解析volatile关键字

目录 1. 初步认识volatile 2. volatile的特性一&#xff1a;保证可见性 2.1 volatile关键字是如何保证可见性的&#xff1f; 2.2 从JMM来看可见性 2.3 从硬件层面了解可见性的本质 2.3.1 CPU高速缓存 2.3.2 缓存一致性 2.3.3 MESI 协议 2.3.4 缓存一致性小结 3. vola…

Nginx基础配置

一、主配置文件结构main block&#xff1b;#全局块配置全局生效event{#事件驱动相关配置 }http{#http/https协议相关配置段 server { ... }&#xff1a;#每个server用于定义一个虚拟主机&#xff1b; server { ... server_name root alias location [OPERATOR] URL { ... if CO…

上周面试回来后写的Java面试总结,想进BAT必看

上周陪同之前一起工作的同事去面试&#xff08;乔治&#xff0c;小袁&#xff0c;鹏飞&#xff08;面试人&#xff09;&#xff09;&#xff0c;第一站是去深圳&#xff0c;第二站上海&#xff0c;第三站杭州。面试什么公司我在这里就不多说了&#xff0c;你们知道是一线公司就…

从贝叶斯方法谈到贝叶斯网络

从贝叶斯方法谈到贝叶斯网络0 引言其实。介绍贝叶斯定理、贝叶斯方法、贝叶斯判断的资料、书籍不少&#xff0c;比方《数理统计学简史》&#xff0c;以及《统计决策论及贝叶斯分析 James O.Berger著》等等&#xff0c;然介绍贝叶斯网络的中文资料则非常少。中文书籍总共也没几本…

k8s 读书笔记 - kubectl 命令行工具用法详解

kubectl 在 k8s 集群中作为 客户端 CLI 工具&#xff0c;可以让用户使用 kubectl 工具执行命令行&#xff0c;并通过使用 k8s API 与 k8s 集群的控制面&#xff08;kube-controller-manager&#xff09;进行通信。kubectl 语法格式kubectl 命令行的语法格式如下&#xff1a;kub…

初级必备:单例模式的7个问题

故事 实话实说&#xff0c;关于单例模式&#xff0c;网上有N多个版本。你估计也看过很多版本。但看完了又能怎样&#xff1f;我技术群里的一位小伙伴&#xff0c;上周面试&#xff0c;就因为一个单例模式&#xff0c;然后叫他回去等通知了。 下面是这位同学被问到的问题&…

Spring AOP源码解析——AOP动态代理原理和实现方式

2019独角兽企业重金招聘Python工程师标准>>> Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架&#xff0c;同时也是轻量级的IoC和AOP的容器框架&#xff0c;主要是针对JavaBean的生命周期进行管理的轻量级容器&#xff0c;可以单独使用&#xff0…

使用.NET构建简单的高性能Redis(三)

译者注该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。首先这个"Redis"是非常简单的实现&#xff0c;但是他在优化这个简单"Redis"路程很有趣&#xff0c;也能给我们在从事性能优化工作时带来一些启…

海尔联手软银机器人,进军服务机器人领域

海尔此次将正式全面进入到服务机器人。 据悉&#xff0c;3月6日海尔公布了未来对于家用机器人的最新战略&#xff0c;同时与软银展开战略合作&#xff0c;将软银的人形机器人引入中国市场&#xff0c;正式进军服务机器人领域。 在本次发布会上&#xff0c;海尔与软银将联手从软…

.NET 7 中的 EndpointFilter

ASP.NET 7 中的 EndpointFilterIntro.NET 6 开始微软引入了 Minimal API&#xff0c;但是相比成熟的控制器模型&#xff0c;还是有很多的不足&#xff0c;.NET 7 针对于 Minimal API 也做了一些改进来让 Minimal API 功能更加丰富&#xff0c;其中 Filter 就是其中的一个更新从…

越来越火的图数据库到底能做什么?

作者 | 俞方桦 随着大数据时代的到来&#xff0c;传统的关系型数据库由于其在数据建模和存储方面的限制&#xff0c;变得越来越难以满足大量频繁变化的需求。关系型数据库&#xff0c;尽管其名称中有“关系”这个词&#xff0c;却并不擅长处理复杂关系的查询和分析。另外&…

piwik抓取用户交互行为

2019独角兽企业重金招聘Python工程师标准>>> https://github.com/matomo-org/matomo-sdk-ios/tree/version-3 http://piwik.org 首先下载demo&#xff0c;把文件拖到项目中去&#xff0c;在AppDelegate.m文件填写piwik服务器的URL和编号&#xff1b; 例如&#xff1…

k8s 读书笔记 - kubernetes 基本概念和术语(下)

DevOps前言上一篇文章 中&#xff0c;我们介绍了 k8s 中的 Master、Node、Pod、Label、RC & RS、Deployment、HPA & VPA、DaemonSet 这些资源对象信息&#xff0c;接下来我们继续介绍 k8s 中常用的资源对象。StatefulSet在 k8s 系统中&#xff0c;Pod 的管理对象 RC、D…

java数据类型后加三个点...

2019独角兽企业重金招聘Python工程师标准>>> 从Java5开始&#xff0c;Java语言对方法参数支持一种新写法&#xff0c;varargs&#xff08;可变长度参数列表&#xff09;&#xff0c;其语法就是类型后跟...&#xff0c;表示此处接受的参数为0到多个Object类型的对象&…

手把手教你用 Jenkins 自动部署 SpringBoot!

1. 什么是 CI/CD 1.1 CI&#xff08;Continuous Integration&#xff09; 1.2 CD&#xff08;Continuous Delivery/Continuous Deployment&#xff09; 2. 什么是 Jenkins 3. 准备工作 3.1 整体规划 3.2 准备代码 3.3 准备服务器 4. 搭建 Jenkins 5. 安装插件 6. 配置 …

bondat蠕虫传播与对抗

转载来自&#xff1a;http://www.mottoin.com/109730.html &#xff08;1&#xff09;可移动磁盘传播手段&#xff1a;隐藏U盘文件&#xff0c;创建快捷方式指向病毒bat文件。Bondat蠕虫主要通过可移动磁盘传播&#xff0c;并借助可移动磁盘中的文件隐蔽自身。Bondat蠕虫会检索…

vim 编译 Python 代码提示配置

2019独角兽企业重金招聘Python工程师标准>>> .vim 和.vimrc 拷贝到根目录 注意根目录下默认是没有.vim的&#xff0c;所以拷贝.vim 没问题&#xff0c;但是拷贝.vimrc 之前需要把原来的.vimrc备份 两个文件下载&#xff1a;http://pan.baidu.com/s/1eRRhakM 转载于:…