悲观锁和乐观锁。我们大致可以把锁分为两类:会被修改,所以每次拿到数据的时候都会加锁,这样当其他线程要修改数据的时候,就会阻塞,直到拿到锁。比如MySQL数据库中的表锁、行锁、读锁、写锁等,Java中的synchronized和ReentrantLock等。乐观锁总是假设最好的情况。每次获取数据时,都假设其他线程不会修改,所以不会加锁。但是在修改数据的时候,需要判断这段时间是否有其他线程。数据已被修改。如果没有修改过,则正常修改。如果已经修改过,则本次修改会失败。常见的乐观锁有版本号控制、CAS算法等。悲观锁应用案例如下:publicclassLockDemo{staticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{ListthreadList=newArrayList<>();for(inti=0;i<50;i++){Threadthread=newThread(()->{for(intj=0;j<1000;++j){count++;}});thread.start();threadList.add(线程);}//等待所有线程完成for(Threadthread:threadList){thread.join();}System.out.println(计数);}}本程序一共开了50个线程,线程是对共享变量count进行++操作的,所以如果没有线程安全问题,最后的结果应该是50000,但是必须有一个线程该程序存在安全问题,运行结果为:48634如果要解决线程安全问题,可以使用synchronized关键字:publicclassLockDemo{staticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{ListthreadList=newArrayList<>();对于(inti=0;i<50;i++){Threadthread=newThread(()->{//使用synchronized关键字解决线程安全问题synchronized(LockDemo.class){for(intj=0;j<1000;++j){count++;}}});thread.start();threadList.add(线程);}for(Threadthread:threadList){thread.join();}System.out.println(计数);}}使用synchronized键修改count变量的话进行换行,这样当一个线程在执行++操作时,其他线程不能同时执行++,只能等待前一个线程执行1000次后才执行继续执行,这样可以保证最后的结果是50000使用ReentrantLock也可以解决线程安全问题:publicclassLockDemo{staticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{ListthreadList=newArrayList<>();锁lock=newReentrantLock();for(inti=0;i<50;i++){Threadthread=newThread(()->{//使用ReentrantLock关键字解决线程安全问题lock.lock();try{for(intj=0;j<1000;++j){count++;}}最后{lock.unlock();}});thread.start();threadList.add(线程);}for(Threadthread:threadList){thread.join();}System.out.println(计数);}}这两种锁机制都是悲观锁的具体实现。不管其他线程是否会同时修改,直接加锁,保证原子操作。乐观锁的应用由于线程的调度对操作系统来说是极其耗费资源的,所以我们应该尽量避免在不断阻塞和唤醒的线程之间切换,从而产生乐观锁。在数据库表中,我们经常会设置一个version字段,这就是乐观锁的体现。假设一个数据表的数据内容如下:+----+-----+-----------+------+|编号|姓名|密码|版本|+----+-----+---------+-------+|1|zs|123456|1|+----+-----+------------+------+它如何避免线程安全问题?假设此时A、B两个线程要修改这条数据,就会执行如下sql语句:selectversionfrome_userwherename='zs';更新e_usersetpassword='admin',version=version+1wherename='zs'andversion=1;首先两个线程都查到zs用户的版本号为1,然后线程A先进行更新操作,此时用户密码改为admin,版本号加1。然后线程B执行更新操作。这时候版本号已经是2了,所以更新肯定是失败了。因此,线程B失败。只能重新获取版本号,然后更新。这就是乐观锁,我们没有对程序和数据库进行任何加锁操作,但是仍然可以保证线程安全。CAS还是以初始加法程序为例。在Java中,我们也可以用一种特殊的方式来实现:publicclassLockDemo{staticAtomicIntegercount=newAtomicInteger(0);publicstaticvoidmain(String[]args)throwsInterruptedException{ListthreadList=newArrayList<>();for(inti=0;i<50;i++){Threadthread=newThread(()->{for(intj=0;j<1000;++j){//使用AtomicInteger解决线程安全问题count.incrementAndGet();}});thread.start();threadList.add(线程);}for(Threadthread:threadList){thread.join();}System.out.println(计数);}}为什么要用AtomicInteger类来解决线程安全问题呢?我们看一下源码:publicfinalintincrementAndGet(){returnunsafe.getAndAddInt(this,valueOffset,1)+1;}count在调用incrementAndGet()方法时,实际上是调用了不安全类:publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){int变量5;做{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));returnvar5;}getAndAddInt()方法有一个循环,关键代码在这里,我们假设此时线程A进入了这个方法,此时var1是一个AtomicInteger对象(初值为0),而值var2是12(这是一个内存偏移量,我们不用关心),var4的值是1(准备给计数加1)首先,可以通过获取主存中的数据值AtomicInteger对象和内存偏移量:var5=this.getIntVolatile(var1,var2);当var5的值为0时,程序会判断:!this.compareAndSwapInt(var1,var2,var5,var5+var4)compareAndSwapInt()是一个本地方法,它的作用是比较交换,即判断var1的值是否与从主存中取出的var5的值相同,此时肯定是相同的,所以将var5+var4的值赋值给var1,返回true,为true取反为false,所以循环结束,final方法返回1。这是一个正常的运行过程。但是,当并发发生时,处理情况就不同了。假设此时线程A执行了getAndAddInt()方法:publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;做{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));returnvar5;}线程A此时获取var1的值为0(var1为共享变量AtomicInteger)。当线程A即将执行时,线程B先执行。此时线程B获取到var1的值为0,var5的值为0,比较成功。这时候,var1的值就变成了1;此时轮到线程A执行,它获取到var5的值为1,此时var1的值不等于var5的值。这次加1的操作会失败,重新进入循环。这时,var1和var5的值发生了变化。此时var5的值也是1,比较成功。因此,var1的值由1变为2,如果其他线程在获取var5之前修改了主存中var1的值,那么本次操作将再次失败,程序将重新进入循环。这就是使用自旋方法来实现乐观锁。因为不加锁,节省了线程调度的资源,也避免了程序一直自旋的情况。手写一个自旋锁publicclassLockDemo{privateAtomicReferenceatomicReference=newAtomicReference<>();publicvoidlock(){//获取当前线程对象Threadthread=Thread.currentThread();//自旋等待while(!atomicReference.compareAndSet(null,thread)){}}publicvoidunlock(){//获取当前线程对象Threadthread=Thread.currentThread();atomicReference.compareAndSet(thread,null);}静态整数计数=0;publicstaticvoidmain(String[]args)throwsInterruptedException{LockDemolockDemo=newLockDemo();列表<线程>threadList=newArrayList<>();for(inti=0;i<50;i++){Threadthread=newThread(()->{lockDemo.lock();for(intj=0;j<1000;j++){count++;}lockDemo.开锁();});线。开始();threadList.add(线程);}//等待线程完成for(Threadthread:threadList){thread.join();}System.out.println(计数);}}利用CAS的原理可以很容易的实现一个自旋锁,首先AtomicReference中的初始值必须为null,这样第一个线程调用lock()方法后才会成功将当前线程的对象放入AtomicReference中,而此时如果另一个线程调用lock()方法,则会因为线程对象与AtomicReference中的对象不同,陷入循环等待,直到第一个线程执行完++操作,调用unlock()方法,则线程会将AtomicReference值设置为null。这时候其他线程就可以跳出循环了。通过CAS机制,我们可以在不加锁的情况下模拟加锁的效果,但是它的缺点也很明显:循环等待占用CPU资源,只保证一个变量的原子操作会产生ABA有问题请点击关注公众号,利用碎片时间学习,每天进步一点点。