您的位置:首页 > 编程语言 > Java开发

Java 线程安全

2016-05-05 15:40 519 查看

I. 线程安全

在使用线程时,如果每个线程所执行的任务中,涉及的变量仅仅是线程内部变量或该变量仅有该线程读写,那么此线程是安全的.

但如果多个线程同时读写同一个变量的话,发生的状况往往是变量最后的值和预期不同. 这是因为多个线程在同时执行时,每个线程都会对这个变量进行操作.

这里可以用一个生活中的例子来说明:在订火车票时,某班列车只剩下最后一张票了,但有两人刚好同时看到并预定了这张票,同时因为服务器采用的是多线程,每个用户独享一个单独的线程,那么这两个人将会同时订到这张票,但这显然是不符合生活经验的.

下面的例子便说明了这一点.

public class UnsafeThread implements Runnable {
public int count = 0;

@Override
public void run() {

try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}

count++;
}

public int getCount() {
return count;
}

public static void main(String [] args) {
ExecutorService executor = Executors.newCachedThreadPool();
UnsafeThread thread = new UnsafeThread();
for (int i = 0; i < 1000; i++) {
executor.execute(thread);
}
executor.shutdown();

System.out.println("count ==> " + thread.getCount());

}
}

output://
count ==> 926


在不使用线程的情况下,count 的值应为1000;但因为多个线程同时对count 进行操作,最后的结果只有926.

在顺序编程中,对一个变量执行两次++,在不考虑特殊情况下得到的值一定是2;但如果是用多线程进行操作:线程A 增加了count一次,现在count 等于2;而同时线程B 的任务也是增加count 的值,但线程B 不知道线程A 增加了这个count 值,所以在线程B 执行任务的时候,它得到的count 值为1.

通过上述例子,可以看出,因为线程自身的特殊性,在使用并发时多个线程,如果没有特殊的机制来确保变量的正常操作,那么线程将不会被广泛采用.

II. Synchronised 关键字

对方法使用synchronised 关键字,可以避免上述的情况.

这里可以用排队的例子来说明. 在原来的情况中,线程们就像是没有排队的顾客一样,全都挤向了前台;而收银员,也就是被操作的变量,因为顾客太多而被弄得昏头转向,不免出了差错. 现在加上了syncrhonised,相当于在前台加上了栏杆(摆放在前台用来规范队列的东西是什么…),强制顾客们整齐地排成了一条队伍,一次只有一个顾客点餐.

public class SynchronizedThread implements Runnable {
private int count = 0;

@Override
public synchronized void run() {
count++;
}

public int getCount() {
return count;
}

public static void main(String [] args) {
ExecutorService executor = Executors.newCachedThreadPool();
SynchronizedThread thread = new SynchronizedThread();
for (int i = 0; i < 1000; i++) {
executor.execute(thread);
}
executor.shutdown();

System.out.println("count ==> " + thread.getCount());

}

}

output://
count ==> 1000


run()
方法前加上了synchronised,这样在访问count 变量时,一次只有一个而不是多个线程了.

同时要注意的是,被访问的变量一定要设置为private,不然尽管通过方法操作变量的线程被规范了,直接访问变量的线程却可以任意修改.

III. Lock

Lock 可以理解为显示地使用synchronised 机制.

尽管Lock 在语法上没有synchronised 优雅,但在执行一些复杂操作,那么Lock 将具有synchronised 所不具备的灵活性.

public class LockedThread implements Runnable {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
boolean captured = lock.tryLock();

try {
count++;
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (captured) lock.unlock();
}
}

public int getCount() {
return count;
}

public static void main(String [] args) {
ExecutorService executor = Executors.newCachedThreadPool();
SynchronizedThread thread = new SynchronizedThread();
for (int i = 0; i < 1000; i++) {
executor.execute(thread);
}
executor.shutdown();

System.out.println("count ==> " + thread.getCount());

}

}

output://
count ==> 1000


获得lock 需要调用方法
tryLock()
,同时在任务执行完后,需要调用
unlock()
来释放锁. 所以推荐方法是在获取锁后,将执行代码放入一个try 模块中,并在finally 模块中释放锁.

IV. Volatile 关键字

原子性(atomic)是除了使用synchronised 关键字和Lock 方法外另一个确保线程安全的方法,它指的是任务操作不可中断性. 但是Java 中的操作大多不是原子性的,并且使用原子性的尝试往往会失败. 因此,不要轻易尝试原子性,除非你已是并发专家.

volatile 关键字便是实现原子性的方法之一. 将一个变量设为volatile 后,每一个线程在改变该变量时,所有的线程都可以看到这个改变.

实例为将本章最开始的例子中的count 加上volatile 关键字.

public class UnsafeThread implements Runnable {
private volatile int count = 0;

@Override
public void run() {

try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}

count++;
}

public int getCount() {
return count;
}

public static void main(String [] args) {
ExecutorService executor = Executors.newCachedThreadPool();
UnsafeThread thread = new UnsafeThread();
for (int i = 0; i < 1000; i++) {
executor.execute(thread);
}
executor.shutdown();

System.out.println("count ==> " + thread.getCount());

}
}

output://
count ==> 767


此段解释可能稍有错误

但是从输出可以看出,即使加上了volatile 关键字,count 仍未到达1000. 这和JVM 的运行机制有关. 每个线程在运行时,其实是独自拥有着一个线程栈,并从总栈中复制所需要的变量;而volatile的作用则是强制线程在改变变量后,将这个新的变量推送到总栈中. 但由于这种推送的不及时性,所以导致count的结果依旧不是我们想要的结果.

这个例子也侧面说明了,Java 自身不支持原子性,因此如果除非你是并发专家,不然不要依赖于原子性.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: