1) Java中什么是竞态条件? 举个例子
竞态条件会导致程序在并发情况下出现一些bug。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了, 那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理
竞态条件(Race Condition):计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
最常见的竞态条件为:
一,先检测后执行。执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
对于main线程,如果文件a不存在,则创建文件a,但是在判断文件a不存在之后,Task线程创建了文件a,这时候先前的判断结果已经失效,(main线程的执行依赖了一个错误的判断结果)此时文件a已经存在了,但是main线程还是会继续创建文件a,导致Task线程创建的文件a被覆盖、文件中的内容丢失等等问题。
多线程环境中对同一个文件的操作要加锁。
和大多数并发错误一样,竞态条件不总是会产生问题,还需要不恰当的执行时序
2)Java中有哪些实现并发编程的方法
1. synchronized关键字
2. 使用继承自Object类的wait、notify、notifyAll方法
3. 使用线程安全的API和集合类:
- 使用Vector、HashTable等线程安全的集合类
- 使用Concurrent包中提供的ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等弱一致性的集合类
- 在Collections类中有多个静态方法,它们可以获取通过同步方法封装非同步集合而得到的集合,如List list = Collection.synchronizedList(new ArrayList())。
4. 使用原子变量、volatile变量等
5. 使用Concurrent包中提供的信号量Semaphore、闭锁Latch、栅栏Barrier、Callable&Future、阻塞队列BlockingQueue等.
6. 手动使用Lock实现基于锁的并发控制
7. 手动使用Condition或AQS实现基于条件队列的并发控制
8. 使用CAS实现非阻塞的并发控制
3)进程间通信的几种方式
1. 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3. 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
4. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
6. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
7. 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
4)ConcurrentHashMap
ConcurrentHashMap是线程安全的HashMap,内部采用分段锁来实现,默认初始容量为16,装载因子为0.75f,分段16,每个段的HashEntry<K,V>[]大小为2。键值都不能为null。每次扩容为原来容量的2倍,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。在获取size操作的时候,不是直接把所有segment的count相加就可以可到整个ConcurrentHashMap大小,也不是在统计size的时候把所有的segment的put, remove, clean方法全部锁住,这种方法太低效。在累加count操作过程中,之前累加过的count发生变化的几率非常小,所有ConcurrentHashMap的做法是先尝试2(RETRIES_BEFORE_LOCK)次通过不锁住Segment的方式统计各个Segment大小,如果统计的过程中,容器的count发生了变化,再采用加锁的方式来统计所有的Segment的大小。
5)ABA问题
ABA问题发生在类似这样的场景:线程1转变使用CAS将变量A的值替换为C,在此时,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍为A,所以CAS成功。但实际上这时的现场已经和最初的不同了。大多数情况下ABA问题不会产生什么影响。如果有特殊情况下由于ABA问题导致,可用采用AtomicStampedReference来解决,原理:乐观锁+version。可以参考下面的案例来了解其中的不同
1 public class ABAQuestion 2 { 3 private static AtomicInteger atomicInt = new AtomicInteger(100); 4 private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100,0); 5 6 public static void main(String[] args) throws InterruptedException 7 { 8 Thread thread1 = new Thread(new Runnable(){ 9 @Override 10 public void run() 11 { 12 atomicInt.compareAndSet(100, 101); 13 atomicInt.compareAndSet(101, 100); 14 } 15 }); 16 17 Thread thread2 = new Thread(new Runnable(){ 18 @Override 19 public void run() 20 { 21 try 22 { 23 TimeUnit.SECONDS.sleep(1); 24 } 25 catch (InterruptedException e) 26 { 27 e.printStackTrace(); 28 } 29 boolean c3 = atomicInt.compareAndSet(100, 101); 30 System.out.println(c3); 31 } 32 }); 33 34 thread1.start(); 35 thread2.start(); 36 thread1.join(); 37 thread2.join(); 38 39 Thread thread3 = new Thread(new Runnable(){ 40 @Override 41 public void run() 42 { 43 try 44 { 45 TimeUnit.SECONDS.sleep(1); 46 } 47 catch (InterruptedException e) 48 { 49 e.printStackTrace(); 50 } 51 atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1); 52 atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1); 53 } 54 }); 55 56 Thread thread4 = new Thread(new Runnable(){ 57 @Override 58 public void run() 59 { 60 int stamp = atomicStampedRef.getStamp(); 61 try 62 { 63 TimeUnit.SECONDS.sleep(2); 64 } 65 catch (InterruptedException e) 66 { 67 e.printStackTrace(); 68 } 69 boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1); 70 System.out.println(c3); 71 } 72 }); 73 thread3.start(); 74 thread4.start(); 75 } 76 }