线程安全与锁优化

提纲

1. 锁的基本概念和实现

线程安全

面向过程的编程思想:把数据和过程分别作为独立的部分来考虑,数据代表问题空间中的客体,程序代码则用于处理数据。

面向对象的编程思想:站在现实世界的角度去抽象和解决问题,把数据和行为都看做是对象的一部分。

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个结果是线程安全的。

示例1--多线程共同操作一个对象时引发的线程安全问题

package chapter8;

import java.util.ArrayList;
import java.util.List;

public class ThreadUnsafe {
	public static List<Integer> numberList = new ArrayList<Integer>();
	public static class AddToList implements Runnable{
		int startnum;
		
		public AddToList(int startnum) {
			super();
			this.startnum = startnum;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			int count =0;
			while(count<1000000){
				numberList.add(startnum);
				startnum+=2;
				count++;
			}
		}
		
	}
	public static void main(String[] args) {
		Thread t1 = new Thread(new AddToList(0));
		Thread t2 = new Thread(new AddToList(1));
		t1.start();
		t2.start();
	}
}

输出结果:

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 366
	at java.util.ArrayList.add(ArrayList.java:441)
	at chapter8.ThreadUnsafe$AddToList.run(ThreadUnsafe.java:21)
	at java.lang.Thread.run(Thread.java:745)、

原因--arraylist底层(详情:JDK1.8源码解析-ArrayList):

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

vector底层:

/**
     * Sets the size of this vector. If the new size is greater than the
     * current size, new {@code null} items are added to the end of
     * the vector. If the new size is less than the current size, all
     * components at index {@code newSize} and greater are discarded.
     *
     * @param  newSize   the new size of this vector
     * @throws ArrayIndexOutOfBoundsException if the new size is negative
     */
    public synchronized void setSize(int newSize) {
        modCount++;
        if (newSize > elementCount) {
            ensureCapacityHelper(newSize);
        } else {
            for (int i = newSize ; i < elementCount ; i++) {
                elementData[i] = null;
            }
        }
        elementCount = newSize;
    }

因此可将arraylist更换为vector解决线程安全问题。

对象头和锁

jvm的对象有对象头、实例数据、和对齐补充构成,对象头中有一个markword的部分是实现锁的关键。以32位系统为例,普通对象的对象头(开启偏向锁)如下所示:

hash:25------------>|age:4  biased_lock:1  lock:2

它有25位表示对象的hash值,4位比特表示对象的年龄,1位比特表示对象是否为偏向锁,2位比特表示锁的信息

对于偏向锁的对象,它的格式如下:

[JavaThread* | epoch | age | 1 | 01]

前23位表示持有的偏向锁的线程,后续2位比特表示偏向锁的时间戳,4位表示对象年龄,后一位为1表示偏向锁。最后两位表示可偏向未锁定。

当对象处于轻量级锁定时,其mark word如下:

[ptr                |00] locked

ptr指向存放在获得线程栈中的该对象的真实对象头。

当对象处于重量级锁定时,其markword如下:

[ptr                |10] monitor

ptr指向monitor

当对象处于普通的未锁定的(关闭偏向锁)状态时,其格式如下:

[header          | 0 |10] unlocked

前29位表示对象的hash值、年龄等信息。倒数第三位为0,最后两位为01,表示未锁定。

2.锁的实现与优化

偏向锁(对象头)

在无竞争的情况下把整个同步都消除掉。

这个锁会偏向于第一个获得它的线程,如果接下来该所没有被其它线程获取,则持有偏向锁的线程将永远不需要进行同步。即持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行同步操作。当有另外一个线程获取这个锁时,偏向模式结束。

示例2--偏向锁的应用

package chapter8;

import java.util.List;
import java.util.Vector;
/**
 * 
 * @author yinweicheng
 *jvm1:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx512m -Xms512m
 *jvm2:-XX:-UseBiasedLocking -client -Xmx512m -Xms512m
 */
public class Biased {
	public static List<Integer> numberList = new Vector<Integer>();

	public static void main(String[] args) {
		long begin = System.currentTimeMillis();
		int count = 0;
		int startnum = 0;
		while (count < 10000000) {
			numberList.add(startnum);
			startnum += 2;
			count++;
		}
		long end = System.currentTimeMillis();
		System.out.println(end - begin);
	}
}

使用偏向锁输出结果:

299

不适用时输出结果:

433

轻量级锁(对象头)

如果获取偏向锁失败,即该对象已偏向,但是持有的锁被另外一个线程尝试获取时,则该对象撤销偏向。若被当前线程锁定则恢复到轻量级锁定的状态,未锁定则进入未锁定不可偏向的状态。当线程获取到轻量级锁时,另一个尝试获取当前锁的线程则进入自旋状态,避免阻塞来提高性能。

轻量级锁通过BasicObjectLock实现,这个对象包含一个BasicLock对象和一个持有该对象的java指针组成。BasicObjectLock对象放置在java栈的栈帧中。

在无竞争的情况下,使用CAS操作完成加锁和解锁,消除同步使用的互斥量。(BasicLock)先复制当前对象头到锁记录中,当栈帧到达该对象时,使该对象指向锁记录(BasicLock)中的对象头并修改所标志位完成加锁。如果存在锁竞争,除了互斥量的开销外,还额外的发生了CAS操作,因此有竞争的情况下,轻量级锁比传统的重量级锁慢。

锁膨胀

当轻量级锁获取失败,即另一个尝试获取当前锁的线程自旋一定次数后,这个线程就会被阻塞,轻量级锁就会膨胀为重量级锁。

锁消除

虚拟机即时编译器在运行时,对一些代码上要求同步,但实际被检测到不可能存在共享数据竞争的锁进行消除。

如字符串相加,可以将第一次锁与最后一次保留,其它锁进行消除

示例3--锁消除

package chapter8;

/**
 * 
 * @author yinweicheng
 *  jvm1:-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
 *         -Xcomp -XX:-BackgroundCompilation -XX:BiasedLockingStartupDelay=0
 *  jvm2:-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks -Xcomp
 *         -XX:-BackgroundCompilation -XX:BiasedLockingStartupDelay=0
 */
public class LockEliminate {

	private static final int CIRCLE = 2000000;

	public static String createStringBuffer(String s1, String s2) {
		StringBuffer sb = new StringBuffer();
		sb.append(s1);
		sb.append(s2);
		return sb.toString();
	}

	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		for (int i = 0; i < CIRCLE; i++) {
			createStringBuffer("JVM", "Diagnosis");
		}
		long bufferCost = System.currentTimeMillis() - start;
		System.out.println("createStringBuffer:" + bufferCost + "ms");
	}
}

输出结果--关闭锁消除:

createStringBuffer:141ms

输出结果--开启锁消除

createStringBuffer:119ms

自旋锁与自适应锁

自旋锁:为了避免线程挂起和恢复带来的时间消耗,可以让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自适应锁:意味着自旋的时间不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

参数:使用自旋锁-XX:+UseSpinning

      设定自旋次数-XX:PreBlockSpin

锁在应用层的优化

1.减少锁的持有时间。只在必要时进行同步,将同步方法改为同步块,或者设置设置同步标记,通过判断进行同步。

2.减少锁粒度。如ConcurrentHashMap的分段锁,将全局锁变为分段锁,减少锁粒度。

3.锁分离:同样减少锁粒度,如读写锁,或LinkedBlockingQueue的实现(即take与put操作的锁分离)

 public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
    }

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
        } finally {
            lock.unlock();
        }
    }

4.锁粗化

如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁(锁粗化),将会把加锁的同步范围扩展到整个操作序列的外部。

3.CAS操作

原理

包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当要更新的变量V等于期望值E时,,才会将V的值赋值为N。如果不同说明其它线程对其进行了更新,当前线程什么也不做。最后返回当前V的真实值。

原子操作

jdk提供可java.util.concurent.atomic实现cas操作。

LongAddr(jdk1.8)

由于cas操作执行的是循环等待,多次尝试的操作,故失败次数会影响性能。因此可以运用减小锁粒度的思想,将共享数据分成多个cell,每个cell独自维护内部的值,当前对象的值有所有cell累计合成。这样提高了并行度。

示例4---锁、原子操作、原子分段操作的比较

package chapter8;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class Atomic {
	//设置最大线程数
	private static final int MAX_THREADS = 3;
	//设置任务数量
	private static final int TASK_COUNT = 3;
	//设置目标值
	private static final int TARGET_COUNT = 10000000;
	//分别使用原子分段、原子、锁进行多线程运行
	private AtomicLong acount = new AtomicLong(0L);
	private LongAdder lacount = new LongAdder();
	private long count = 0L;

	static CountDownLatch cdlsync = new CountDownLatch(TASK_COUNT);
	static CountDownLatch cdlatomic = new CountDownLatch(TASK_COUNT);
	static CountDownLatch cdladdr = new CountDownLatch(TASK_COUNT);

	protected synchronized long inc() {
		return ++count;
	}

	protected synchronized long getCount() {
		return count;
	}

	public class SyncThread implements Runnable {
		protected long starttime;
		Atomic out;

		public SyncThread(Atomic o, long starttime) {
			super();
			this.out = o;
			this.starttime = starttime;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			long v = out.getCount();
			while (v < TARGET_COUNT) {
				v = out.inc();
			}
			long endtime = System.currentTimeMillis();
			System.out.println("SyncThread spend;" + (endtime - starttime)
					+ "ms" + "v=" + v);
			cdlsync.countDown();
		}
	}

	public void testSync() throws InterruptedException {
		ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
		long starttime = System.currentTimeMillis();
		SyncThread sync = new SyncThread(this, starttime);
		for (int i = 0; i < TASK_COUNT; i++) {
			exe.submit(sync);
		}
		cdlsync.await();//同步操作,进行等待
		exe.shutdown();
	}
	
	public class AtomicThread implements Runnable{
		protected long starttime;

		public AtomicThread( long starttime) {
			super();
			this.starttime = starttime;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			long v = acount.get();
			while (v < TARGET_COUNT) {
				v = acount.incrementAndGet();
			}
			long endtime = System.currentTimeMillis();
			System.out.println("atomicThread spend;" + (endtime - starttime)
					+ "ms" + "v=" + v);
			cdlatomic.countDown();
		}
	}
	public void testAtomic() throws InterruptedException {
		ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
		long starttime = System.currentTimeMillis();
		AtomicThread atomic = new AtomicThread(starttime);
		for (int i = 0; i < TASK_COUNT; i++) {
			exe.submit(atomic);
		}
		exe.shutdown();//原子操作时不用进行等待
	}
	
	public class LongAddrThread implements Runnable{
		protected long starttime;

		public LongAddrThread( long starttime) {
			super();
			this.starttime = starttime;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			long v = lacount.sum();
			while (v < TARGET_COUNT) {
				lacount.increment();
				v = lacount.sum();
			}
			long endtime = System.currentTimeMillis();
			System.out.println("LongAdderThread spend;" + (endtime - starttime)
					+ "ms" + "v=" + v);
			cdlatomic.countDown();
		}
	}
	public void testAtomicLong() throws InterruptedException {
		ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
		long starttime = System.currentTimeMillis();
		LongAddrThread atomic = new LongAddrThread(starttime);
		for (int i = 0; i < TASK_COUNT; i++) {
			exe.submit(atomic);
		}
		exe.shutdown();//原子操作时不用进行等待
	}
	public static void main(String[] args) throws InterruptedException {
		Atomic a = new Atomic();
		a.testAtomicLong();
		a.testAtomic();
		a.testSync();
	}
}

输出结果;

atomicThread spend;156msv=10000000
atomicThread spend;160msv=10000001
atomicThread spend;161msv=10000002
LongAdderThread spend;362msv=10000002
LongAdderThread spend;362msv=10000000
LongAdderThread spend;362msv=10000001
SyncThread spend;604msv=10000002
SyncThread spend;604msv=10000001

4.Java语言中的线程安全

java语言中各种操作共享的数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1. 不可变:不可变的对象一定是线程安全的。

   基本数据类型使用final修饰可以保证它不可变。

   把对象中带有状态的变量都声明为final,这样在构造函数之后,它就是不可变的。

2. 绝对线程安全:不管运行时环境如何,调用者都不需要任何额外的同步措施。

3. 相对线程安全:保证这个对象单独的操作是线程安全的。大部分的线程安全类都属于这种类型,例如Vector、hashTable、Collections的synchronizedCollection()方法包装的集合等

4. 线程兼容:对象本身并不是安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。大部分的类是线程兼容的,如ArrayList和HashMap等。

5. 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。会造成死锁,如Thread类的suspend()和resume()方法

线程安全的实现方法

1. 互斥同步:同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用,而互斥则是同步的一种手段,临界区、互斥量、和信号量都是主要的互斥实现方式。

   java中最基本的互斥同步手段就是synchronized关键字,synchronized经过编译之后,分别形成monitorenter和monitorexit这两个字节码指令。

   首先,synchronized是对同一条线程来说是可重入的。其次,同步块在已进入的线程执行完之前,会阻塞后面的线程进入。而阻塞和唤醒线程都需要进行用户态到核心态的转换,所以synchronized是一个重量级锁。

   还可以使用concurrent中的重入锁(ReentrantLock)实现同步。ReentrantLock的高级功能:

   (1). 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可已选择放弃等待,改为处理其他事情。

   (2). 公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。可以通过带boolean值的构造函数实现公平锁。

   (3). 锁绑定多个条件:指一个ReentrantLock对象可以同时绑定多个Condition对象。而synchronized中,锁对象的wait()和notify()或notifyAll()可以实现一个隐含条件。

2. 非阻塞同步:基于冲突检测的乐观并发策略,就是先进行操作,如果没有其它线程征用共享数据,那操作成功;吐过共享数据争用,产生了冲突,那就再采取其他的补偿措施。这种乐观的并发策略很多实现都不需要把线程挂起,称为非阻塞同步。

  通过硬件保证操作和冲突检测这两个步骤具备原子性,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:

   (1)测试并设置

   (2)获取并增加

   (3)交换

   (4)比较并交换(CAS)

   (5)加载链接/条件存储(LL/SC)

CAS操作可通过unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。由于unsafe类限制了只有启动类加载器加载,所以可通过JUC包里的整数原子类,通过compareAndSet()方法和getAndIncrement()等方法使用CAS操作。

CAS操作的缺点:只能检查值是否相等,不能检测出是否被改变过(先+1再-1)。

3.无同步方案:有些代码天生就是安全的,包括:

  (1)可重入代码:不依赖存储在堆上数据和公用的系统资源、用到的状态量都由参数传入、不调用非可重入的方法。如果一个方法,它的返回结果是可预测的,只要输入了相同的数据,就能返回相同的结果,那他就满足可重入性的要求。

  (2)线程本地存储 :保证共享数据在同一线程中运行。可以通过java.lang.ThreadLocal类实现线程本地存储的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值