如何实现“高效并发”,首先需要保证并发的正确性,然后在此基础上来实现高效。
定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
按照线程安全的“安全程度”由强至弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
最简单就是把对象中带有状态的变量都声明为final
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。例如java.util.Vector是一个线程安全的容器,即使所有方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段。
import java.util.Vector; public class Main { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args) throws Throwable{ while(true){ for (int i=0; i<10; i++){ vector.add(i); } Thread removeThread = new Thread(new Runnable(){ public void run() { for (int i=0; i<vector.size(); i++){ vector.remove(i); } } }); Thread printThread = new Thread(new Runnable(){ 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); } } }日志:
它只保证某个方法是同步,并不能保证所有方法同步。有可能一线程在执行remove()方法,刚好有一个线程在get()方法,获取的元素刚好是remove 的元素。
修改一下
import java.util.Vector; public class Main { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args) throws Throwable{ while(true){ for (int i=0; i<10; i++){ vector.add(i); } Thread removeThread = new Thread(new Runnable(){ public void run() { synchronized (vector) { for (int i=0; i<vector.size(); i++){ vector.remove(i); } } } }); Thread printThread = new Thread(new Runnable(){ public void run() { synchronized (vector) { for (int i=0; i<vector.size(); i++){ System.out.println(vector.get(i)); } } } }); removeThread.start(); printThread.start(); //不要同时产生过多的线程,否则会导致操作系统假死 while (Thread.activeCount()>20); } } }对象单独的操作是线程安全的,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。如上例子
本身不是线程安全,但是可以通过同步手段实现线程安全的线程的java类,例如ArrayList HashMap等
不管采用什么同步措施都不能保证线程安全。例如 Thread类 suspend() 和resume()方法 这里已经被jdk废弃了。
保证共享数据在同一时刻只有一条(或者一些,使用信号量)线程使用 (类似停车场)
临界区(Critical Section)、互斥量(Mutex)、和信息量(Semaphore)都是主要的互斥实现方式。互斥是因,同步是果,互斥是方法,同步是目的
最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令,这个两个字节码需要一个reference类型的参数来指明要锁定和解锁的对象,获取对象的锁,锁计数器加1,反之减一,当计数器为0时,锁就被释放了。
重入锁(ReentrantLock)来实现同步。代码写法上有点区别,一个表现为API层面的互斥锁(lock())和unlock()方法配合try/finally语句块来完成,一个表现为原生语法层面的互斥锁,不过ReentrantLock比synchronized增加了一些高级功能,主要有以下:等待可中断、可实现公平锁,以及锁可以绑定多个条件。
没有线程竞争资源,如果有刚好有线程占用,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重试,直到试成功为止)(坚持不懈直到成功),它不需要线程挂起,对于硬件如何实现设置
测试并设置(Test-and-Set)获取并增加(Fetch-and-Increment)交换(Swap)比较并交换(Compare-and-Swap CAS)加载链接/条件存储(Load-Linked/Store-Conditional LL/SC) import java.util.concurrent.atomic.AtomicInteger; public class AtomicTest { public static AtomicInteger race = new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); System.out.println(race); } private static final int THREADS_COUNT= 20; public static void main(String[] args) { Thread[] threads= new Thread[THREADS_COUNT]; for (int i=0; i<THREADS_COUNT; i++){ threads[i] = new Thread(new Runnable(){ public void run() { for(int i=0; i<10000; i++){ increase(); } } }); threads[i].start(); } } } 以上就是AtomicInteger CAS 操作,运行结果为:200000
但是会出现ABA问题,虽然一线程访问前是A,访问后还是A,但是在此期间可能有线程改为B,然后又改成了A, 为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”。
不需要同步操作
技术: 适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)
-XX:+UseSpinning 参数开启,自旋就是一直在运行(等待线程释放锁,这样避免唤醒和休眠的性能损耗),只是在空转,自旋次数的默认是10次,如果自旋10次没有等待线程释放锁就挂起线程,用户使用参数-XX:PreBlockSpin设置自旋次数。自适应自旋实事求是,根据实际情况进行自旋。
不能存在竞争的数据进行锁的消除
就是同一个线程对于一个对象进行重复加锁与解锁(在没有线程竞争的情况下)。锁粗化,就是在开始加一次锁就行了。
相对传统同步(重量级锁)来说,它是通过对象头部有标记信息(Mark Word) 来标记当前的锁情况,主要分为五种
未锁定(01)、轻量级锁(00)、重量级锁(10)、GC标记(11) 、可偏向(01)
类似于:刚开始是道德约束(对应轻量级锁)、如果道德约束不行,就法律约束(重量级锁),刚开始获取这个对象资源采用轻量级锁,当有线程来竞争,而当前线程占用,这时候就改变标记为10(重量级锁),你必须等着我执行完才能执行。
启用了偏向锁-XX:+UseBiaseLocking,就是第一个线程获取执行,如果有线程来竞争,就取消标记,改为轻量级锁,一直到重量级锁。