绝对线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替工作,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象时绝对线程安全的。
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
java.util.Vector是一个线程安全的容器,它的add()、get()和size()这类方法都是被synchronized修饰的。
看一下下面的代码:
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
});
removeThread.start();
printThread.start();
while (Thread.activeCount() > 20);
}
}
运行期间会抛ArrayIndexOutOfBoundsException这个错误。
相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的。对于一些特定顺序的连续调用,就可能需要在调用段使用额外的同步手段来保证调用的正确性。
线程安全的实现方式
互斥同步
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个或一些线程使用。
互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。
因此互斥是因,同步是果;互斥是方法,同步是目的。
非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。
基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程竞争使用共享数据,那操作就成功了;如果共享数据有竞争,产生了冲突,就采取其他的补偿措施(最初的措施就是不断地重试,直到成功为止)。
这种乐观的并发策略的许多实现都不要把线程挂起,因此这种同步操作称为非阻塞同步。
锁优化
自旋锁:让请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,只需要让线程执行一个忙循环,这项技术就是所谓的自旋锁。
锁消除:对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁粗化:如果虚拟机检测到有一连串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。
轻量级锁:使用操作系统互斥量来实现的传统锁是“重量级的”。在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
偏向锁:消除数据在无竞争情况下的同步,进一步提高性能。
参考
- 深入理解Java虚拟机[书籍]