解决并发问题:
对多线程的理解
多线程主要是解决共享变量的问题时需要考虑锁机制,其他无共享变量时,多线程都是安全的,因此在多线程中,对共享变量的定义时特别需要注意。
1.synchronized,Atomic ,volatile,ReentrantLock
- 线程对某个变量的操作可以简化成下面的步骤:
2.执行代码,对数据进行各种操作和计算
3.把操作后的变量值重新写回主内存中
对于synchronized(悲观锁),主要有两方面的作用,(1)锁机制确保拿到锁的线程才能执行该段代码;(2)内存数据的同步,在线程进入synchronized块之前,会把工作存内存中的所有内容映射到主内存上,然后把工作内存清空再从主存储器上拷贝最新的值。而在线程退出synchronized块时,同样会把工作内存中的值映射到主内存,但此时并不会清空工作内存。这样一来就可以强制其按照上面的顺序运行,以保证线程在执行完代码块后,工作内存中的值和主内存中的值是一致的,保证了数据的一致性!
注意:synchronized是对对象加锁,即一个对象中有多个方法加锁,同一时刻只有获得该对象锁的方法能够运行。
对于volatile,由于每个线程间的各自维持自己的内存,不会同步到公用内存,因此对于需要及时同步的变量,需要定义成volatile,注意:volatile只是保证可见性,不能确保原子性。其作用包括如下:其一是,用volatile修饰的变量的读取和写入都是直接操作内存,以保证被其它线程读取到值都是最新的,或者称之为确保内存的可见性;其二是,保证变量的读取和写入操作都是原子操作,就是上面long和double的读取所遇到的问题,注意这里提到的原子性只是针对变量的读取和写入,并不包括对变量的复杂操作,比如i++就无法使用volatile来确保这个操作是原子操作。
我理解的原子操作就是该线程在操作共享内存时,别的线程是不可对该共享内存进行读写的,也就是说一旦某个处理器开始操作某个内存地址,处理器保证在其他处理器或总线代理访问内存地址之前完成这个操作,可以通过锁总线的方式实现。
volatile的使用规则:
a、对变量的写入操作不依赖于变量当前的值,或者你能确保只有单个线程更新变量的值
b、该变量不会与其他状态变量一起纳入不变性条件中
使用实例
public class Counter {
private volatile long count;
private long prevCount;
public Counter() {}
public Counter(int count) { this.count = count; }
public synchronized long inc() {
return ++count;
}
public synchronized long inc(long i) {
count = count + i;
return count;
}
public synchronized long dec() {
return --count;
}
public synchronized long dec(long i) {
count = count - i;
return count;
}
/**
* reset counter to 0
*/
public synchronized void clear() {
count = 0;
prevCount = 0;
}
public long getCount() {
return count;
}
public long getCountChange(){
long tempCount = count;
long change = tempCount-prevCount;
prevCount = tempCount;
return change;
}
}
2. ExecutorService threadPool = Executors.newFixedThreadPool(10),开辟10个线程,队列是用LinkedBlockQueue作为队列,当新来的任务没有进程可用是时,就放入队列,等有线程空闲时会去捞取任务执行
3.ThreadLocal:如多线程访问SimpleDateFormat,为每个线程new一个SimpleDateFormat,当某个线程第一次运行时,minuteThreadLocal.get()会调用initialvValue()函数,实例化一个副本,若再次运行将不再调用initialvValue()方法.从而保证了每个形成只new了一个实例,解决了变量共享存在的问题。
private final static ThreadLocal<SimpleDateFormat> minuteThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMddHHmm"); }; };
4.concurrentHashMap,分段加锁机制,对于多线程来说比HashMap高效(整体加锁)
5.concurrentLinkHashMap:采用了最近最少使用策略,可以用来缓解平凡访问数据的压力
6.CopyOnWriteArrayList,线程安全,适合读多写少,因为改数组对写操作加锁,而读操作不加锁;采用的底层数组复制机制来开辟数组的。
public boolean add(E var1) { ReentrantLock var2 = this.lock; var2.lock(); boolean var6; try { Object[] var3 = this.getArray(); int var4 = var3.length; Object[] var5 = Arrays.copyOf(var3, var4 + 1); var5[var4] = var1; this.setArray(var5); var6 = true; } finally { var2.unlock(); } return var6; }
7.线程的同步问题Future
8.对于多线程的计数问题尽量用AtomicInteger代替加锁策略
锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
首先毫无疑问,在没有锁的机制下需要借助volatile原语,保证线程间的数据是可见的(共享的),这样获取变量值的时候才能直接读取。
public final int get() {
return value;
}
然后来看看++i是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而compareAndSet利用JNI来完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
总结:其实就是通过比较来确定是否加1操作成功,从而避免了i++这类操作在多线程中非原子操作带来的问题,着也是乐观锁的解决策略。
对于一般计数问题是没有问题的,但对于ABA问题还是存在问题,当一个线程处理后回复原始数据,这个过程会出现问题,可以加一个版本号解决
9.读写加锁问题
- 如果只是读操作,没有写操作,则可以不用加锁,此种情形下,建议变量加上final关键字;
- 如果有写操作,但是变量的写操作跟当前的值无关联,且与其他的变量也无关联,则可考虑变量加上volatile关键字,同时写操作方法通过synchronized加锁;
- 如果有写操作,且写操作依赖变量的当前值(如:i++),则getXXX和写操作方法都要通过synchronized加锁。
10 Spring通过单实例化Bean简化多线程问题 (自己定义的bean应该时无状态对象)
由于Spring的事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施(也即Connection实例),再结合IoC和AOP实现高级声明式事务的功能,所以Spring的事务天然地和线程有着千丝万缕的联系。
我们知道Web容器本身就是多线程的,Web容器为一个HTTP请求创建一个独立的线程(实际上大多数Web容器采用共享线程池),所以由此请求所牵涉到的Spring容器中的Bean也是运行于多线程的环境下。在绝大多数情况下,Spring的Bean都是单实例的(singleton),单实例Bean的最大好处是线程无关性,不存在多线程并发访问的问题,也就是线程安全的。
一个类能够以单实例的方式运行的前提是“无状态”:即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO必须持有一个Connection,而Connection即是状态化的对象。所以传统的DAO不能做成单实例的,每次要用时都必须创建一个新的实例。传统的Service由于内部包含了若干个有状态的DAO成员变量,所以其本身也是有状态的。
但是在Spring中,DAO和Service都以单实例的方式存在。Spring是通过ThreadLocal将有状态的变量(如Connection等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring不遗余力地将有状态的对象无状态化,就是要达到单实例化Bean的目的。
由于Spring已经通过ThreadLocal的设施将Bean无状态化,所以Spring中单实例Bean对线程安全问题拥有了一种天生的免疫能力。不但单实例的Service可以成功运行于多线程环境中,Service本身还可以自由地启动独立线程以执行其他的Service。
11 信号量
用来限流作用
12 LinkedBlockingQueue
问题1.它的加锁是如何做到读写分离的
因为每次插入数据的时候,总是插在链表的尾部,而读数据总是在链表的头部,从而使得读写不会在同一个节点。
问题2.如何做到当没数据时等待有数据再处理
当链表为空时,LinkedBlockingQueue中用到了await,当count一旦为0便阻塞,直到放数据操作唤醒读操作。锁可以理解为原子操作。