锁是最常用的同步方法之一。
4.1 提高锁性能
锁的竞争必然会导致程序的整体性能下降。在编写程序时,应尽量减小锁的竞争。
4.1.1 减少锁持有时间
在程序开发中,应尽可能的减少某个锁的占有时间,以减少线程间互斥的可能。
例:
public synchronized void syncMethod{
othercode1();
mutextMethod();
othercode2();
}
在syncMethod()方法中,假设只有mutexMethod()方法是需要同步的,这样编写就会增加线程对锁的持有时间。
因这样写
public void syncMethod2(){
othercode1();
synchronized(this){
mutextMethod();
}
othercode2();
}
4.1.2 减小锁粒度
减小锁粒度也是一种削弱多线程锁竞争的有效手短。常用的使用场景就是ConcurrentHashMap类,它被分成16个小段的HashMap。在该类中添加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁。并完成put方法操作。
put方法():
public V put(K key,V value){
Segment s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask; //获得对应的段序号
if((s =(Segment(K,V)UNSAFE.getObject)
(segment,(j >>> SSHIFT)+SBASE)) == null)
s = ensureSegment(j); //数据的插入
return s.put(key,hash,value,false);
}
减小所粒度带来的问题:当系统需要取得全局锁时,其消耗的资源会比较多。
因此只有在获取全局信息的方法调用不频繁时,这种减小所粒度方法才能真正意义上提高系统的吞吐量。
4.1.3 用读写锁代替独占锁
在读多写少的场合,使用读写锁代替独占锁,有益于系统性能的提高。
4.1.4 锁分离
将锁思想进一步延伸,就是锁分离。依据应用程序的功能特点,使用类似的分离思想,也可以像读写锁那样进行分离。
一个典型的案例就是LinkedBlockingQueue的实现,其中take()和put()分别实现了从队列中取得数据和向队列中增加数据的功能。两的方法虽然都是对当前列表进行了修改操作,但分别作用于队列的前端和尾端,理论上说,并不冲突。
如果使用独占锁,则要求两个方法进行时都要获取当前的独占锁,那么take和put就不能真正的并发。
因此JDK中,用两种不同的锁分离了take和put方法。
4.2 java虚拟机的锁优化
4.2.1 锁偏向
锁偏向是一种针对加锁操作的优化手段。其思想是:如果一个线程获得了锁,那么锁就进入了偏向模式。当这个线程再次请求锁时,无须在做任何同步操作。这样就可以节省大量有关锁申请的操作。
使用场合:在几乎没有锁竞争的场合,有较好的优化效果。
在竞争激烈的场合,由于是不同线程来竞争相同的锁,所以效果不佳。
4.2.2 轻量级锁
如果偏向锁失败,虚拟机不会立即挂起线程,会使用一种轻量级锁的优化手段。
实现:将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。
如果轻量级锁加锁失败,当前线程的锁请求就会膨胀为重量级锁。
4.2.3 自旋锁
锁膨胀后,为了避免线程真实的在操作系统层面挂起,虚拟机会让当前线程做几个空循环,如果可以得到锁,顺利进入临界区;如果还是得不到锁,将会在操作系统层面真正的挂起。
4.2.4 锁消除
java虚拟机在JIT编译时,通过运行上下文的扫描,去除不可能存在共享资源竞争的锁。
4.3 ThreadLocal
除了控制资源的访问,还可以通过增加资源来保证对象的安全。
例如,让100个人去填写信息表,如果只有一只笔,那么控制资源就是控制大家不去哄强这只笔
,而增加资源就是准备100只笔让每个人都有笔。
4.3.1 Threadlocal的简单使用
package chapter4;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalDemo {
//使用ThreadLocal为每一个线程创造一个SimpleDateformat
static ThreadLocal tl=new ThreadLocal();
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i){this.i=i;}
public void run() {
try {
if(tl.get()==null){
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t=tl.get().parse("2019-12-12 20:38:"+i%60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es=Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
}
4.3.2 ThreadLocal的实现原理
ThreadLocal如何保证这些对象只被当前线程访问?
ThreadLocal的set方法和get方法
public void set(T value){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map != null)
map.set(this,value);
else
createMap(t,value);
}
在set时,首先获取当前对象线程对象,然后通过getMap()方法拿到线程的ThreadLocalMap,并将值存到ThreadLocal中。设置到ThreadLocal中的数据,写入ThreadLocalMap中,key为当前对象,value为我们需要的值。
public T get(){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map != null){
ThreadLocalMap.Entry e = map.getEntry(this);
if(e != null){
return (T)e.value;
}
return setInitalValue();
}
get方法先取得当前线程的ThreadLocalMap对象,然后将自己作为key取得内部的实际数据。
这些变量是维护在ThreadLocal的内部实现的,这意味着只有线程不退出,对象的引用将一直存在。
当线程退出时,Thread类会进行一些清理工作,其中包括清理ThreadLocalMap。
在使用线程池是当前线程未必就会退出(例如固定大小的线程池,线程总是存在)。如果存在,将一些大的对象设置到ThreadLocal中,就可能会使系统出现内存泄漏的可能(设置ThreadLocal对象,不清理,在使用几次后,这个对象也不再有用了,但它却无法回收)。
如果希望对象及时被回收,可以使用ThreadLocal.remove()方法将这个变量移除,就和关闭数据库连接一样。
在对象垃圾回收中,我们会写出类似obj = null 的代码。如果这样就更容易被垃圾回收器发信息,从而加速回收。同理,对于ThreadLocal变量,将其手动设置为null,对应的所有线程的局部变量都有可能被回收。
案例:
package chapter4;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalDemo_Gc {
static volatile ThreadLocal tl = new ThreadLocal() {
protected void finalize() throws Throwable {
System.out.println(this.toString() + " is gc");
}
};
static volatile CountDownLatch cd = new CountDownLatch(10000);
public static class ParseDate implements Runnable {
int i = 0;
public ParseDate(int i) {
this.i = i;
}
public void run() {
try {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {
protected void finalize() throws Throwable {
System.out.println(this.toString() + " is gc");
}
});
System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
}
Date t = tl.get().parse("2019-12-12 21:15:" + i % 60);
} catch (ParseException e) {
e.printStackTrace();
} finally {
cd.countDown();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10000; i++) {
es.execute(new ParseDate(i));
}
cd.await();
System.out.println("mission complete!!");
tl = null;
System.gc();
System.out.println("first GC complete!!");
//在设置ThreadLocal的时候,会清除ThreadLocalMap中的无效对象
tl = new ThreadLocal();
cd = new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
es.execute(new ParseDate(i));
}
cd.await();
Thread.sleep(1000);
System.gc();
System.out.println("second GC complete!!");
}
}
从运行结果中可以看出,首先线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行一次GC,可以看到ThreadLocal对象被回收了,接着第二次创建,然后GC,虽然没有remove()这些对象,但系统有可能回收它们。
4.3.3对性能的帮助
为每一个线程分配一个独立的对象对系统性能可能是有帮助的,但帮助于否取决于共享对象的内部逻辑。
案例:多线程下产生随机数的性能问题
package chapter4;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class RndMultiThread {
public static final int GEN_COUNT = 10000000;
public static final int THREAD_COUNT = 4;
static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
public static Random rnd = new Random(123);
public static ThreadLocal tRnd = new ThreadLocal() {
@Override
protected Random initialValue() {
return new Random(123);
}
};
public static class RndTask implements Callable {
private int mode = 0;
public RndTask(int mode) {
this.mode = mode;
}
public Random getRandom() {
if (mode == 0) {
return rnd; //返回一个
} else if (mode == 1) {
return tRnd.get(); //ThreadLocal
} else {
return null;
}
}
@Override
public Long call() {
long b = System.currentTimeMillis();
for (long i = 0; i < GEN_COUNT; i++) {
getRandom().nextInt();
}
long e = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");
return e - b;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Future[] futs = new Future[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
futs[i] = exe.submit(new RndTask(0));
}
long totaltime = 0;
for (int i = 0; i < THREAD_COUNT; i++) {
totaltime += futs[i].get();
}
System.out.println("多线程访问同一个Random实例:" + totaltime + "ms");
//ThreadLocal的情况
for (int i = 0; i < THREAD_COUNT; i++) {
futs[i] = exe.submit(new RndTask(1));
}
totaltime = 0;
for (int i = 0; i < THREAD_COUNT; i++) {
totaltime += futs[i].get();
}
System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms");
exe.shutdown();
}
}
可见在ThreadLoca模式下,系统用时仅为436ms。
4.4 无锁
无锁是一种乐观的策略,它会假设对资源的访问时没有冲突的,所有的线程都可以在没有等待的情况下持续执行。如果遇到冲突,就采用比较交换的技术来鉴别线程冲突,一旦遇到冲突,就重复当前操作直到没有冲突为止。
4.4.1 比较交换
CAS算法过程:包含三个参数CAS(V,E,N),其中V表示要更新的变量,E表示预期值,N表示新值。仅当V等于E时,才会将V的值设为N,如果V和E的值不同,说明已经有其他线程做了更新,则当前线程什么也不做。最后,CAS返回当前V的真实值,CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程使用CAS操作同一变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,允许再次尝试,也允许放弃操作。
简单说:就是CAS需要你给出一个期望值,也就是你认为变量现在应该是什么样子的。如果变量不是期望值那样,说明已经被别人修改过了。就重新读取,再次尝试修改。
4.4.2 无锁的线程安全整数:AtomicInteger
JDK中的atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。
其中最常用的是AtomicInteger,可以把他看成线程安全的Interger。
AtomicInteger的简单使用:
package chapter4;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
static AtomicInteger i=new AtomicInteger();
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
i.incrementAndGet(); //AtomicInteger的自增操作
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(i);
}
}
运行结果为10000,没有错误,是线程安全的,如果线程不是安全的,输出值应该小于10000
于AtomicInterger类似的类还有:AtomicLong、AtomicBoolean,以及AtomicReference用于表示对象引用。
4.4.3 java中的指针:Unsafe类
在方法compareAndSet中
public final boolean compareAndSet (int expect,int update){
return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
}
可以看到unsafe变量,它时Unsafe类型,这个类封装了一些类似指针的操作,java中认为指针是不安全的,因此应用程序无法使用这个类,只有jdk内部可以使用。
4.4.4 无锁的对象引用:AtomicReference
AtomicReference与AtomicInteger类似。
AtomicReference逻辑上有不足之处:
1.可能一个值被修改了两次修改成立原值,可能线程认为它并没有修改,当然这种情况发生的概率很小,即使发生,也不会产生计算错误。
2.但可能有这样的场景,我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关。这时,CAS可能就会出错。
案例:一家商店为了刺激消费,决定为卡里余额小于20元的客户一次性赠送20元。
package chapter4;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceDemo {
static AtomicReference money=new AtomicReference();
public static void main(String[] args) {
money.set(19);
//模拟多个线程同时更新后台数据库,为用户充值
for(int i = 0 ; i < 3 ; i++) {
new Thread() {
public void run() {
while(true){
while(true){
Integer m=money.get();
if(m<20){
if(money.compareAndSet(m, m+20)){
System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");
break;
}
}else{
//System.out.println("余额大于20元,无需充值");
break ;
}
}
}
}
}.start();
}
//用户消费线程,模拟消费行为
new Thread() {
public void run() {
for(int i=0;i<100;i++){
while(true){
Integer m=money.get();
if(m>10){
System.out.println("大于10元");
if(money.compareAndSet(m, m-10)){
System.out.println("成功消费10元,余额:"+money.get());
break;
}
}else{
System.out.println("没有足够的金额");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}
用若干个后台线程,扫描数据,并为满足条件的客户充值。
主函数模拟一个账户,获得赠送的同时,正好消费了20元,使得消费后的金额等于赠于前的金额。
可以看到运行结果,这个账户被进行了多次充值。
虽然这种情况出现概率不大,但也会造成系统bug,jdk中提供了AtomicStampedReference来解决这个问题。
4.4.5 带时间戳的对象引用 AtomicStampedReference
上一小节中的问题主要是对象在修改的过程中,对象丢失了状态信息。
AtomicStampedReference不仅维护对象值,还维护一个时间戳,对应数值被修改时,必须更新时间戳,因此,就能有效的防止上述问题的发生。
package chapter4;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceDemo {
static AtomicStampedReference money=new AtomicStampedReference(19,0);
public static void main(String[] args) {
//模拟多个线程同时更新后台数据库,为用户充值
for(int i = 0 ; i < 3 ; i++) {
final int timestamp=money.getStamp();
new Thread() {
public void run() {
while(true){
while(true){
Integer m=money.getReference();
if(m<20){
if(money.compareAndSet(m, m+20,timestamp,timestamp+1)){
System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元");
break;
}
}else{
//System.out.println("余额大于20元,无需充值");
break ;
}
}
}
}
}.start();
}
//用户消费线程,模拟消费行为
new Thread() {
public void run() {
for(int i=0;i<100;i++){
while(true){
int timestamp=money.getStamp();
Integer m=money.getReference();
if(m>10){
System.out.println("大于10元");
if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){
System.out.println("成功消费10元,余额:"+money.getReference());
break;
}
}else{
System.out.println("没有足够的金额");
break;
}
}
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
}.start();
}
}
可以看到账户只被赠送了一次。
4.4.6 数组的无锁 AtomicIntergerArray
案例:
package chapter4;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k=0;k<10000;k++)
arr.getAndIncrement(k%arr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts=new Thread[10];
for(int k=0;k<10;k++){
ts[k]=new Thread(new AddThread());
}
for(int k=0;k<10;k++){ts[k].start();}
for(int k=0;k<10;k++){ts[k].join();}
System.out.println(arr);
}
}
可见是线程安全的。
4.4.7 普通变量的原子操作:AtomicIntergerFieldUpdate
有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求。在原子包中有一个实用的工具类,AtomicIntegerFieldUpdate。它可以在不改动(或极少改动)原有代码的基础上,让普通的变量也享受CAS操作带来的线程安全性。
案例:模拟投票选举
package chapter4;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate{
int id;
volatile int score;
}
public final static AtomicIntegerFieldUpdater scoreUpdater
= AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
//检查Updater是否工作正确
public static AtomicInteger allScore=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Candidate stu=new Candidate();
Thread[] t=new Thread[10000];
for(int i = 0 ; i < 10000 ; i++) {
t[i]=new Thread() {
public void run() {
if(Math.random()>0.4){
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for(int i = 0 ; i < 10000 ; i++) { t[i].join();}
System.out.println("score="+stu.score);
System.out.println("allScore="+allScore);
}
}
可以看到score与allscore相等,说明Updater正常工作,是保证了线程安全的。