多线程-并发②
16. 数据并发操作可能的问题?
- 丢失的修改
- 不可重复读,读第二次,数据就不对了
- 读脏数据
- 幻影读
17. 消息等待通知wait/notify具体的应用
- 一个线程修改了一个对象的值,另外一个线程需要感知到这个变化
- Java中我们使用的对象锁以及wait/notify方法进行线程通信
- 等待方遵循的原则:
- 获取对象的锁
- 不满足条件 就调用wait()方法
- 条件满足继续执行
- 通知方原则:
- 获取对象的锁
- 改变条件, 然后notify
18. 线程池中 submit() 和 execute() 方法有什么区别?
- execute() 参数 Runnable ;
- submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable);
- execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
- submit(Callable x)有返回值,返回一个Future类的对象。
- Future对象:
- 通过get方法,获取线程返回结果
- 通过get方法,接收任务执行时候抛出的异常
- 通过isDone方法,可以判断线程是否执行完成。
19. 线程的创建方式有哪些?
- 继承Thread类实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 继承Thread类,并重写run方法
*/
public class MyThread extends Thread {
public void run() {
super.run();
System.out.println("MyThread...");
}
}
//调用
MyThread thread = new MyThread();
thread.start(); - 实现Runnable接口方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 实现Runnable接口,并重写run方法
*/
public class MyRunnable implements Runnable{
public void run() {
System.out.println("MyRunnable...");
}
}
//调用
MyRunnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start(); - 实现Callable接口方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 实现Callable接口,并重写call方法
*/
public class MyCallable implements Callable<String>{
public String call() throws Exception {
return "MyCallable...";
}
}
//创建和调用
MyCallable callable=new MyCallable();
ExecutorService eService=Executors.newSingleThreadExecutor();
Future<String> future=eService.submit(callable);
//获取返回结果
try {
String result=future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
} - 其中前两种比较常用。但是,需要有返回值需要实现Callable接口。
注意
- callable需要配合线程池使用
- callable比runnable功能复杂一些
- Callable的call方法有返回值并且可以抛异常,而Runnable的run方法就没有返回值也没有抛异常,也就是可以知道执行线程的时候除了什么错误。
- Callable运行后可以拿到一个Future对象,这个对象表示异步计算结果,可以从通过Future的get方法获取到call方法返回的结果。但要注意调用Future的get方法时,当前线程会阻塞,直到call方法返回结果。
20. CAS锁机制
- CAS(Compare and Swap 比较并交换),是一种无锁算法,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
- CAS算法涉及到三个操作数
- 需要读写的内存位置(V)
- 进行比较的预期原值(A)
- 拟写入的新值(B)
- 如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。
21. Threadlocal关键字
- 线程本地变量,可以为变量在每个线程中都创建一个副本,使每个线程都可以访问自己内部的副本变量
22. 乐观锁和悲观锁的区别?
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
- synchronized、Lock属于悲观锁。
- Lock有三种实现类:ReentrantLock、ReadLock(读锁)和WriteLock(写锁)。
- 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。
- CAS属于乐观锁。
- 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
- 悲观锁对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 乐观锁不会上锁,在更新时会判断数据有没有被修改,一般会使用“数据版本机制”或“CAS操作”来实现。
23. 事务特性
- 事务特性指的就是ACID。
- 分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
- 分别解释下:
- 原子性:原子性是指事务包含的操作要么全部成功,要么全部失败。因此事务的操作成功就必须要完全应用到数据库。
- 一致性:一致性强调的是数据是一致性的。假设用户A和用户B两者的钱加起来一共是5000,那么不管A还是B如何转账,转几次帐,事务结束后两个用户的钱加起来应该还是5000,这就是事务的一致性。
- 隔离性:当多个用户并发访问数据库时,多个并发事务是相互隔离的。事务之间不能相互干扰。
- 持久性:一个事务一旦被提交了,那么对数据库中的数据改变是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作
强化理解
- 原子性,算是事务最基本的特性了。
- 一致性,感觉像事务的目标,其他的三个特性都是为了保证数据一致性存在的。
- 隔离性,为了保证并发情况下的一致性而引入,并发状态下单靠原子性不能完全解决一致性的问题,在多个事务并发进行的情况下,即使保证了每个事务的原子性,仍然可能导致数据不一致。比如,事务1需要将100元转入帐号A:先读取帐号A的值,然后在这个值上加上100。但是,在这两个操作之间,另一个事务2将100元转入帐号A,为它增加了100元。那么最后的结果应该是A增加了200元。但事实上,事务1最终完成后,帐号A只增加了100元,因为事务1覆盖了事务2的修改结果。
- 持久性,好理解,事务一旦提交,对数据库的影响是永久的,保证所有操作都是有效。
24. 互斥锁/读写锁
- 独享锁/共享锁就是一种广义的说法,互斥锁/读写锁,就是具体的实现。
- 一次只能一个线程拥有互斥锁,其他线程只有等待
- 互斥锁在Java中的具体实现就是ReentrantLock。
- 读写锁在Java中的具体实现就是ReadWriteLock。
25. 偏向锁/轻量级锁/重量级锁
- 这三种锁是指锁状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
26. 公平锁/非公平锁
- 公平锁是指多个线程按照申请锁顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
- 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
- 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
27. 分段锁
- 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
- 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
- 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对分段加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行插入。
- 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
- 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
28. 可重入锁
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。
- 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
- 代码理解
1
2
3
4
5
6
7
8synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
} - 上面的代码就是一个可重入锁的一个特点。如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
29. 对象锁和类锁
1.java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,实际区别大
2.对象锁是用于对象实例方法,或者一个对象实例上的
3.类锁是用于类的静态方法或者一个类的class对象上的。
4.我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的
30. 死锁
- Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。即线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。
31. 独享锁/共享锁
- 独享锁是指该锁一次只能被一个线程所持有。
- 共享锁是指该锁可被多个线程所持有。
- 对于Java ReentrantLock(重入锁)而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
- 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
- 独享锁与共享锁也是通过AQS(AbstractQuenedSynchronizer抽象的队列式同步器)来实现的,通过实现不同的方法,来实现独享或者共享。
- 对于Synchronized而言,当然是独享锁。
32. 自旋锁
- 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
- 优点是减少线程上下文切换的消耗
- 缺点是循环会消耗CPU。
33. 进程和线程的区别
- 程序被载入到内存中并准备执行,它就是一个进程
- 单个进程中执行中每个任务就是一个线程
- 一个线程只能属于一个进程,但是一个进程可以拥有多个线程
34. 静态方法是否线程安全?
- 看静态方法是是引起线程安全问题要看在静态方法中是否使用了静态成员。
- 如果该静态方法不去操作一个静态成员,只在方法内部使用实例字段(instance field),不会引起安全性问题
- 如果该静态方法操作了一个静态字段,则需要静态方法中采用互斥访问的方式进行安全处理。