前言:
如果没有明确的同步来管理共享数据,一个线程可能会修改其他线程正在使用的数据,产生意外的结果;
编写线程安全的代码,本质上就是管理对状态的访问,而且通常是共享的、可变的状态。
状态:通俗的说,一个对象的状态就是它的数据,存储在状态变量中,比如实例属性或静态属性。
共享:是指一个变量可以被多个线程访问;
可变:是指变量的值在其生命周期内可以改变;
无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步机制来协调线程对该变量的访问。Java提供了synchronized关键字,对具体一个对象实现线程独占,完成所谓的原子操作。
一.线程安全
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是 “线程安全” 的。
二.原子性
1.何谓Atomic
Atomic一词跟原子有点关系,曾被认为是不能被进一步分割的最小粒子,计算机中的Atomic是指不能分割成若干部分的意思。如果一段代码被认为是Atomic,则表示这段代码在执行过程中,是不能被中断的。
2.原子操作:
原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断;
原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
3.提供原子操作的类:
java.util.concurrent.atomic包中提供了许多关于原子操作类。
public class Test implements Runnable {
// 创建具有初始值 0 的 AtomicInteger
private AtomicInteger atInteger = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10; i++) {
/*
* 以原子方式将当前值加 1; 也就是说当一个线程执行原子操作时,是不能被中断的,直到执行完毕.所以该方式是线程安全的.
*/
atInteger.getAndIncrement();
System.out.println(Thread.currentThread().getName() + "..."
+ atInteger.get());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + atInteger.get());
}
public static void main(String[] args) {
Test test = new Test();
Thread th1 = new Thread(test);
Thread th2 = new Thread(test);
th1.start();
th2.start();
}
}
三.锁
1.内部锁
Java提供了强制原子性的内置锁机制:
synchronized块, 一个synchronized块有两部分:锁对象的引用,以及这个锁保护的代码块;
synchronized方法是对跨越了整个方法体的synchronized块的简短描述,至于synchronized方法的锁,就是该方法所在对象本身。(静态的synchronized方法从Class对象上获取锁)。
语法:
synchronized(lock){
//访问或修改被锁保护的共享状态
}
执行线程进入synchronized块之前会自动获得锁,无论通过正常控制路径退出,还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。
内部锁在Java中扮演了互斥锁的角色,意味着最多只有一个线程可以拥有该锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。
同一时间,只能有一个线程可以运行特定锁保护的代码块,因此,由同一个锁保护的synchronized块会各自原子地执行,不会相互干扰。
注意:原子性的含义与它在事务性应用中相同—— 一组语句作为单独的,不可分割的单元运行。
2.重进入
当一个线程请求其它线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的。因此线程在试图获得它自己占有的锁时,请求会成功。重进入意味着所有请求是基于“每个线程”,而不是基于“每次调用”的。
public class ReEntry implements Runnable
{
public synchronized void firstMth()
{
/*
* 验证内部锁是否可重入:
* firstMth()与lastMth()都是synchronized类型的,都会在处理前试图获得ReEntry的锁,
* 如果内部锁不是可重入的,lastMth()的调用者就永远无法得到ReEntry的锁,因为锁已经被占有,
* 导致线程永远地延迟,等待一个永远无法获得的锁。 —— 重进入帮助我们避免了这种死锁
*/
lastMth();
System.out.println("enter firstMth...");
}
public synchronized void lastMth()
{
System.out.println("enter LastMth...");
}
@Override
public void run()
{
firstMth();
}
public static void main(String[] args)
{
Thread th = new Thread(new ReEntry());
th.start();
}
}
重进入的实现是为每一个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,计数递增;每次占有线程退出同步块,计数器值将递减。直到计数器达到0时,锁被释放。
3.用锁保护状态
一种常见的锁规则是在对象内部封装所有可变状态,通过对象的内部锁来同步任何访问可变状态的代码路径,保护它在并发访问中的安全。很多线程安全类都是这个模式。
例如java.util.Vector类。这种情况下,对象状态中的一切变量都被对象内部锁保护。如果添加新的代码路方法或代码路径而忘记使用锁,这种锁协议也很容易被破坏。
并不是所有的数据都需要锁保护——只有那些被多个线程访问的可变数据。
4.活跃度与性能
场景:
现在假设我们把Servlet中的service()声明为synchronized,即同步方法。因此每次只能有一个线程执行它。这违背了Servlet框架的使用初衷——Servlet可以同时处理多个请求——并且当负载过高时会引起客户的不满。如果Servlet正忙于处理一个复杂运算,那么在它可以处理一个新的请求开始前,其他用户必须等待,直到当前请求完成。在多CPU系统中,即时负载很高,仍然会有处理器处于空闲。
以上这种web应用的运行方式描述为“弱并发”的一种表现:限制并发调用数量的,并非是可用的处理器资源,而恰恰是应用程序自身的结构。
通过缩小synchronized块的范围来维护线程安全性,可以很容易提升Servlet的并发性。
注意:应谨慎的控制synchronized块,不可以将一个原子操作分解到多个synchronized块中。不过你应该尽量从synchronized块中分离出耗时的且不影响共享状态的操作。—— 这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。
四. 线程安全例子
public class NotThreadSafe implements Runnable
{
private int value = 0;
@Override
public void run()
{
System.out.println("before——"+Thread.currentThread().getName()+"——"+value);
try
{
Thread.sleep(100L);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
value++;
System.out.println("before——"+Thread.currentThread().getName()+"——"+value);
}
public static void main(String[] args) throws InterruptedException
{
NotThreadSafe notSafe = new NotThreadSafe();
Thread[] ths = new Thread[1000];
/*
* 非线程安全的自增操作:
* 在没有同步的情况下,创建1000个线程执行自增操作value++,很有可能会遗失更新,使统计结果不准确
*/
for (int i = 0; i < 1000; i++)
{
ths[i]=new Thread(notSafe);
ths[i].start();
}
for (int i = 0; i < ths.length; i++)
{
ths[i].join();
}
System.out.println("统计结果:"+notSafe.value);
}
}
运行结果:996(理想情况下应该是1000,所以这里遗失更新了)
分析:
假如value的初始值为9,现在有两个线程(A、B)执行该自增操作,线程A、B可能会交替运行(也就是并发),所以线程A、B可能会同时读取到value的值为9,然后同时加1,最后都将value的值设置为10(理想的结果应为:11,可是现在结果为:10)这就会造成其中一次自增操作遗失了。