Java:并发问题及加锁
什么是并发和并行
并发:单个CPU在做多个线程的任务。简单理解:1位服务员(CPU),同一时间只能服务1位客人(线程),但可以服务完这位后,去服务下一位,服务多位客人即多个任务。
并行:多个CPU在做多个线程的任务。简单理解:多位服务员(CPU),同一时间可以服务多位客人(线程)
Java多线程
Java是支持多线程的,而我通常喜欢把线程比喻成工人。一个进程的执行,允许有多个工人在工作,比如进程是建一座大厦,有的工人搬砖,有的搬水泥,有的打地基等等...
小知识:
- JVM中程序计数器,虚拟机栈是线程独有,各一份。而方法区和堆则是线程共享
- 单核CPU:同一时间只能执行一个线程的任务。即同一时间,只会有一个工人在工作,其他工人都是歇着的
多核CPU:同一时间可以执行多个线程的任务(如4核CPU,每一核同一时间处理1个线程,加起来同一时间最多能处理4个线程)。即同一时间,可以有多个工人在工作,这些工人分散在多个进程里,1个进程可能有1个工人在工作,也可能有多个工人在做(进程和线程并不是包含关系,并不是说一个进程就固定几个线程一成不变) - 以单核CPU为例,如果只有1个线程完成N个任务所消耗的时间,一般要比多线程完成N个任务所消耗的时间要短。因为:线程的切换(可以理解为工人轮换)的时间频繁且耗时
- 平时开发好像没有创建和调用过线程,是不是代表应用的运行就没有线程去执行呢?错,框架一般封装了线程池,不然应用怎么可能正常运行。只是说你有额外的异步需求,并且想自己搞个线程去执行,就可以创建新的线程实例去执行
线程安全问题
- 同一资源被消费了两次。举例:
现在账户有100块。线程A和线程B同时做取钱的任务,且都是取60块钱。同一时间进入判断:
if(取款金额 <= 账户现有金额){ //60<100成立,两个线程都进入判断
//取60
....
}
导致账户变成-20,这不符合业务逻辑了
PS:并发问题 = 线程安全问题。并发一般都是多线程,多线程是导致线程安全问题的前提。并发越高,出现线程安全问题的概率会越大
并发导致的根本原因
线程1在执行A任务的时候,线程2也参与了A任务。且A任务中包含共享资源
解决思路
线程1在执行A任务的时候,其他线程不要参与进来!
解决方法
加锁!
synchronized同步锁
同步代码块
synchronized (锁对象){
//同步代码
}
- 代码块一般写在方法内
- 锁对象可以是任意的类对象。但是!在这里要保证所有线程共享同一把锁,即同一个对象。
- 大家经常可以看到synchronized (this)的写法,是由于this指代当前调用的对象。而在spring中,组件一般是单例的,所以可以使用this。有一种写法可以保证锁是唯一的,即取当前类的类对象,如synchronized(User.class){}
同步方法
普通同步方法
public synchronized void A() {}
其实同步方法很好理解,可以理解成同步代码块的代码抽成1个方法出来,方法再声明synchronized就是同步方法了。作用是一样的,同一时间只能有1个线程能够访问方法
值得讨论的是,锁并没有声明。实际上普通同步方法的锁默认是this,大家在实际开发中,要确定普通同步方法的类会有几个实例,如果是单例则没问题,是多例就有问题了
静态同步方法
public static synchronized void A() {
说结论,静态同步方法锁是当前class对象,只有1个,因此锁一定是唯一的。但有个小问题,静态方法内部是不允许去调非静态数据的
lock同步锁
lock是JDK1.5出的一个接口,作用和synchronized一模一样,没有区别。但是用法不同,先上例子:
private final static ReentrantLock lock = new ReentrantLock();//ReentrantLock为Lock的实现类,也是常用的实现类
public static String GetRandom() {
try {
lock.lock();//加锁
....同步代码
} catch (Exception e) {
} finally {
lock.unlock();//释放锁
}
}
lock和synchronized的区别
- synchronized同步代码块范围明确,可读性强,锁是自动释放;但需要考虑锁对象,考虑不慎可能易导致线程安全问题。
- lock无需考虑锁对象。但可读性会比较差,且需要手动去释放锁。
结论:虽然个人还是比较倾向于synchronized,但是实际开发中lock用的更多点。建议的话:优先考虑lock,再是synchronized
其他重要知识
- 线程通信不用理解那么高大上,其实就是多个线程共同完成同一件事。比如要打印1-100,由线程A,B共同完成,线程A打印偶数,线程B打印奇数,这就是线程通信。