- 线程安全
- volatile
- Java原子性操作实现原理
- Java内存模型
- ThreadLocal的设计理念与作用
- 什么是线程池(thread pool)
- concurrenthashmap
- 线程安全实现方案
- 锁优化方案
- 类锁,对象锁
- 实现多线程的3种方法
- foreach与正常for循环效率对比
线程安全
线程安全指的是多线程环境下访问某个类,能够表现正确的行为。
volatile关键字
- 保证可见性
当共享内存的变量改变时,每个线程都能够立即读到对应的变量值。 - 禁止jvm指令重排
线程A写volatile变量,随后线程B读这个变量,这个过程实质上线程A通过内存向B发送消息。
内存语义的实现,也是禁止重排序特性:当一个线程读一个线程写时,第一步是volatile读时禁止指令重排,第二步是volatile写时禁止指令重排,第一个操作是volatile写,第二个操作是volatile读时,不重排序。 - Volatile 能保持单个简单volatile变量的读/写操作的具有原子性。但不能保证自增自减的原子性。
Java原子性操作实现原理
使用CAS实现原子性,CAS是在操作期间先比较旧值,如果旧值没有发生改变,才交换成新值,发生了变化则不交换。但是会出现几个问题:
- ABA的问题,使用版本号解决
- 循环时间过长开销大,一般采用自旋方式实现
- 只能保证一个共享变量的原子操作。
Java内存模型
ThreadLocal的设计理念与作用
ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
与同步那种机制对比(java内存模型那种):同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
什么是线程池(thread pool)
创建一个对象要获取内存资源或者其它更多资源,线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池。
Execute()用于提交不需要返回值得任务,submit()用于提交需要返回值的任务,发挥Future类型的对象。
它们的原理都是遍历线程池中的工作线程,然后逐个调用线程的Internet方法来中断线程,所以无法响应中断的任务可能永远无法终止。 ShutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或暂停的任务,并返回等待执行任务的列表。而shutdown只是将线程池设置成SHUTDOWN状态,然后中断没有正在执行任务的线程。
concurrenthashmap
concurrenthashmap与hashtable最大的区别就是在与锁的粒度以及如何锁上。concurrenthashmap是有一个segment数组,数组中每个元素的都有一个hashentry数组,数组中每个元素都有链表。
- get操作
get操作不加锁,hashentry的不可变性保障了get操作的安全性。hashentry对应的链表头是可变的,但是next指针是final的,元素是volatile的.每次改变元素会维护一个volatile count变量。 - put操作
每次put操作都要对对应的segment枷锁,然后插入到对应的hashentry的链表头里面,最后修改count值。 - delete操作
为了保证hashentry的不可变形,在找到对应的需要删除的元素时,把前面所有的元素复制一遍,然后前面的一个元素指向要删除元素的下一个。 - size()
size() 每个Segment都有一个count变量,是一个volatile变量。当调用size方法时,首先先尝试2次通过不锁住segment的方式统计各个Segment的count值得总和,如果两次值不同则将锁住整个ConcurrentHashMap然后进行计算。
线程状态图
线程安全实现方案
- 互斥实现同步
同步指的是多个线程并发访问共享数据时,保证同一时刻只有一个线程在使用.(临界区,互斥量,信号量可以实现)
synchronized关键字可实现互斥同步,同步块使得其形成monitorenter, monitorentexit通过一个ref指向锁对象, 通过锁计数器+1,-1实现知道什么时候释放锁.它会阻塞其他线程.比较重量级锁 更原生
concurrent包下的重入锁通过lock(),unlock()配合Try,catch使用,其优势在于等待的线程等久了可中断可做别的事,上述锁是非公平锁,而重入锁可实现公平锁相对的.CopyOnWriteArrayList中add方法用到式例:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
两者都是悲观锁.
2. 非阻塞同步 concurrenthashmap里面count变量,值变量利用volatile修饰就用到了CAS保证读写原子性。
用CAS原子性操作实现,是一种乐观锁,compare and swap V与旧的A相比,相同则同意更新B的值.这种操作有几个包装类,比如自加的时候利用 AtomicInteger代替 int类型,然后调用i.incrementAndGet()方法可保证安全.其中那个方法用的是无限for循环直到CAS操作到更新的值.
以上for循环是自旋锁的一种,它避免了线程切换的开销,但是占有处理器的时间,如果自旋等待时间很短,则性能很好,但是自旋等待时间比较长,则性能不好.有一种叫自适应锁的,是根据以前的状态决定自旋的次数.
3. 无同步方案
可重入代码: 不依赖于堆上的公共资源,不掉用不可重入代码,返回结果在相同参数下是一样的.
线程本地存储: 像一个web请求对应一个服务器线程就是很典型的.
以上都可以保证线程安全.
锁优化方案
自旋锁的改进,自适应锁.
锁消除: 比如StringBuffer里面的append方法,它是同步方法,但是如果在方法里面调用的话是在线程的栈帧里面,这是线程私有的所以不会被其他线程消除,这个时候可以被消除此锁.
锁粗化: 比如同一个代码块中,sb.append(a), sb.append(b), sb.append(c) 只会对此方法加一次锁,而不是进入一次加一次锁,不然加锁解锁开销太大.
锁的四种状态: 无锁,偏向锁(轻量级锁所适应的场景是线程交替执行同步块的情况,使用CAS),轻量级锁,重量级锁.
以上是从高到低排序的,会膨胀到上一级. 其中:
偏向锁: 偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
偏向锁使用场景:始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁, 在这个过程中会导致很多额外的开销,不是这种情况应该禁止.
类锁,对象锁
类锁和对象锁不是同1个东西,一个是类的Class对象的锁,1个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。
当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
同步方法只能锁定当前对象或class对象, 而同步方法块可以使用其他对象、当前对象及当前对象的class作为锁
实现多线程的3种方法
- 继承Tread类,重写run函数
- 实现Runnable接口 实际上 Thread 类也是实现了 Runnable 接口
- 实现Callable接口 可以返回结果(通过 Future),也可以抛出异常,需要实现的是 call() 方法。
foreach与正常for循环效率对比
如果只是读数据,优先选择foreach,因为效率高,而且代码简单,方便。 使用的是迭代器实现的
如果要写数据,即替换指定索引位置处的对象,就只能选择for了。