前文只是介绍了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是线程不安全的,因此在并发写入的时候,内部状态出现了错误。 但是它相对快一点,单线程的时候可以使用。
集合的并发后面再说