前文只是介绍了volitale关键字能够关闭重排序,缓存寄存器等优化来防止可见性发生问题。
但是并发编程更多的是对发布(放到可以供其他线程访问的区域,比如static 集合等)的对象进行多线程处理,这里讨论下如何能够安全的发布这些对象。
通常有如下的手段:
栈限制
局部变量,也就是方法内的变量,因为线程私有栈,因此这个变量也是私有的,不会存在并发问题。 当然这个不是解决线程共享对象的,实际上我们写的代码中大多数的对象都是这样使用的,因此没有故意去注意并发仍然运行起来没有问题。
线程限制
使用ThreadLocal来把一个对象变成线程内共享。 也不是解决线程间共享的,其很适用于保存线程内很多地方都会用到的大对象。
package com.prince.concurrent; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }原理上比较简单。 在虚拟机上维护了一个map,可以根据当前线程id得到另外一个map,然后根据当前ThreadLocal实例来得到hold的对象。
线程安全的对象
这种对象内部使用了锁或者其他的机制,保证了每个方法都是安全的。 JVM在性能和安全上的考虑对很多类都提供了两个版本的实现,安全的和不安全的。比如
AtomicInteger 和Integer
StringBuffer 和StringBuilder
Vector和ArrayList
HashTable和HashMap等等。
AtomicInteger 和Integer
class CounterNoSafe{ private Integer count = 0; public void addOne(){ count++; } public Integer getCount(){ return count; } } class CounterSafe{ private AtomicInteger count = new AtomicInteger(0); public void addOne(){ count.getAndIncrement(); } public Integer getCount(){ return count.get(); } }
private void testAtomicInteger() throws InterruptedException{ final CounterNoSafe counter1 = new CounterNoSafe(); new Thread(){ public void run() { for (int i = 0; i < 1000; i++) { counter1.addOne(); } }; }.start(); for (int i = 0; i < 1000; i++) { counter1.addOne(); } TimeUnit.SECONDS.sleep(2); System.out.println(counter1.getCount()); final CounterSafe counter2 = new CounterSafe(); new Thread(){ public void run() { for (int i = 0; i < 1000; i++) { counter2.addOne(); } }; }.start(); for (int i = 0; i < 1000; i++) { counter2.addOne(); } TimeUnit.SECONDS.sleep(2); System.out.println(counter2.getCount()); }将会打印 1327 2000.
可见不安全的计数器是有问题的。
AtomicInteger并不是通过锁的机制来解决并发问题的,而是使用如下的代码:
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } }这样可以获得更好的性能, 之后还会讨论
StringBuffer和StringBuilder
private void testStringBufferAndBuilder() throws InterruptedException { final StringBuffer sbf = new StringBuffer(); new Thread(){ public void run() { for (int i = 0; i < 10000; i++) { sbf.append(1); } }; }.start(); for (int i = 0; i < 10000; i++) { sbf.append(1); } TimeUnit.SECONDS.sleep(2); System.out.println(sbf.length()); final StringBuilder sbd = new StringBuilder(); new Thread(){ public void run() { for (int i = 0; i < 10000; i++) { sbd.append(1); } }; }.start(); for (int i = 0; i < 10000; i++) { sbd.append(1); } TimeUnit.SECONDS.sleep(2); System.out.println(sbd.length()); }
将会打印2000和17836
这个是因为StringBuilder是线程不安全的,因此在并发写入的时候,内部状态出现了错误。 但是它相对快一点,单线程的时候可以使用。
集合的并发后面再说