Moosphan / Android-Daily-Interview

:pushpin:每工作日更新一道 Android 面试题,小聚成河,大聚成江,共勉之~

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

2019-07-31:谈一谈java线程常见的几种锁?

MoJieBlog opened this issue · comments

比如:

  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock
  • AtomicInteger

当然也可以是其他锁

4种Java线程锁(线程同步)

  1. synchronized
    在Java中synchronized关键字被常用于维护数据一致性。
    synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的。
    synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。
    2.ReentrantLock(ReentrantLock引入两个概念:公平锁与非公平锁。)
    可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
    ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
    Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。
    此处AtomicInteger是一系列相同类的代表之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不同。

比如:

  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock
  • AtomicInteger

当然也可以是其他锁

还有一个 unsafe 类;
cas 是乐观锁的一种实现形式, 在多核 CPU 机器上会有比较好的性能;

首先,Java为什么会出现锁的概念,锁是用来干什么的?这里我给大家解释一下, 是保证 线程同步与安全的.

synchronized

synchronized 有什么用?大家看一下这段代码

public class Synchronized2Demo implements TestDemo {

    private int x = 0;

    private void count() {
        x++;
    }

    @Override
    public void runTest() {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1_000_000; i++) {
                    count();
                }
                System.out.println("final x from 1: " + x);
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1_000_000; i++) {
                    count();
                }
                System.out.println("final x from 2: " + x);
            }
        }.start();
    }
}

synchronized的互斥访问

  当添加 synchronized 的时候,多个线程不能同时访问同一个方法,synchronized 的意义是保证方法或代码块的内部资源的互斥访问,同一时间用用一个monitor监视代码最多一个代码访问,怎么解释这句话呢?

运行上段代码,我们不难发现:

final x from 1: 1000000
final x from 2: 1314570

Fuck! 竟然结果不是200 0000,而且每次结果都不一样,这是为什么呢?

  因为:x++ 不是原子操作, 不是一行代码执行的,他是可以分解的代码,可以分离成这样

int temp = i;
i + 1 = temp;

你看,这是两段代码吧,加 synchronized 修饰 就可以完美解决这个问题.

synchronized 的本质:

  1. 保证方法的内部或内部代码块内部资源(数据)的互斥访问,即同一时间,由同一个 Monitor 监视代码,最多只能一个线程在访问

  2. 保证线程之间对监视资源的数据同步.即,任何线程获取 Monitor 后第一时间,会将共享数据的缓存复制到自己的缓存中;任何线程在释放 Monitor 的第一时间,会将缓存的数据复制到共享内存中

Lock / ReentrantReadWriteLock

  同样是加锁机制 ,使用方式更加灵活,同时也更加麻烦一些

 
Lock lock = new ReentrantLock();
{
   // -------伪代码-------
}
lock.lock(); try {
x++;
} finally {
    lock.unlock();
}

  finally 作用是保证在方法提前结束或出现Exception的时候,依然能正常释放锁.

  一般不会直接使用 Lock,而是直接使用更加复杂的锁,如: ReadWriteLock:

public class ReadWriteLockDemo implements TestDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    private int x = 0;

    @Override
    public void runTest() {
        print(15);
        count();
    }


    public void count() {
        writeLock.lock();

        try {
            x++;
        } finally {
            writeLock.unlock();
        }


    }

    public void print(int time) {
        readLock.lock();
        try {
            for (int i = 0; i < time; i++) {
                System.out.println(x + "");
            }

            System.out.println("over");
        } finally {
            readLock.unlock();
        }

    }
}
volatile
  1. 保证加了 volatile 关键字的字段的操作具有同步性,以及对 long 和 double 操作的原子性.因此 volatile 可以看做简化版的 synchronized
  2. volatile 只对基本数据类型(byte ,short ,long ,char,int ,float ,double,boolean)的赋值操作和对象引用赋值有效,你修改 User.name 是不能保证同步的
  3. volatile 依然解决 不了 ++ 的原子性问题
java.util.concurrent.atomic
  1. AtomicInteger , AtomicInteger等类,作用和 volatile 基本一致,可以看做通用版本的 volatile
  
AtomicInteger atomicInteger = new AtomicInteger(0);

{
// -------伪代码--------
}

 atomicInteger.getAndIncrement();

 解决了 ++ 原子性 问题,非常好用.

悲观锁

不管数据怎么样我都会加锁,这个就是悲观锁

乐观锁

我认为,别人不会读数据我倾向于别人不会动数据,所以不对代码块加锁,这就是乐观锁

线程安全问题的本质:

  在多线程访问共同资源时,在某一个线程对资源进行写操作中途(写入已经开始了,但还没结束),其他线程对这个写了一半资源进行读操作,或基于这个写了一半的资源进行了写操作,导致出现数据错误

锁机制的本质:

  通过共享资源进行访问限制,让同一时间只有一个线程可以访问资源,保证数据的准确性

总结:

  无论是线程安全问题,还是针对线程衍生的锁机制,它的核心在于共享资源,而不是某个方法或几行代码


当然还有死锁问题,这里我就不一一和大家阐述了.

最后大家觉的我的内容对大家有帮助,欢迎通过以下渠道阅读我的文章或github star +1

github CSDN 掘金
MicroKibaco Kibaco 小木箱