不正确的访问资源
我们先看一个例子:
class addOne implements Runnable{
private static int value=0;
@Override
public void run() {
//System.out.println(Thread.currentThread());
try {
Thread.sleep(150);
value+=1;
System.out.println(value);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class ConcurrentTest4 {
public static void main(String args[]){
ExecutorService exec = Executors.newCachedThreadPool();
AddOne a = new AddOne();
for(int i=0;i<10;i++){
exec.execute(a);
}
exec.shutdown();
}
}
输出结果:
3
3
3
3
4
5
6
3
3
我们可以看出,静态常量Value本来应该是每执行一次任务加1才对,输出结果即使不是递增那也应该没有相同的数才对,然而在多线程中共享资源Value时操作Value+=1不是原子操作,在编译器中他不是不可拆解的,他可以分为加1和赋值2个操作。当第一个线程进入运行到加1时,有可能第二个线程进入捕获Value值的时候第一个线程还没有进入赋值操作,所以他捕获到的仍是0,然后加一输出1,而第一个线程同样赋值输出1。当任务不能保证原子性时,共享资源就会同时被多个线程操作,就会混乱。比如你本来银行卡有1000,现在取走100元但还余额还未减去100的时候有人同时存了100,并且余额变为1100,那取钱的操作接着下去就会用1100去减100,变成1000,而不是900。所以多线程共享资源时一定要慎重,尤其时项目中那些任务耗时较长时就一定会出错。
解决共享资源竞争
1)同步锁synchronized
可以用sysnchonized修饰一个代码块,代码块中被封装成一个原子,一个线程执行完代码块中的代码才能有可能让别的线程进入代码块。代码块就像一个厕所,排在前面的人抢先进入后反锁住门,等他上完厕所打开门才会让第二个人上。将上面的run()方法改为下列:
public void run() {
synchronized(this){
value+=1;
System.out.println(Thread.currentThread()+" "+ value );
}
}
输出结果:
Thread[pool-1-thread-1,5,main] 1
Thread[pool-1-thread-10,5,main] 2
Thread[pool-1-thread-7,5,main] 3
Thread[pool-1-thread-9,5,main] 4
Thread[pool-1-thread-8,5,main] 5
Thread[pool-1-thread-6,5,main] 6
Thread[pool-1-thread-5,5,main] 7
Thread[pool-1-thread-4,5,main] 8
Thread[pool-1-thread-3,5,main] 9
Thread[pool-1-thread-2,5,main] 10
sysnchronized代码块中的(this)可以指定对象,给对象加锁(obj)。当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁(lock)。当()中是类的class对象时,表示锁住了整个类,该类所有的对象都是同一把所(ClassName.class)。另外sysnchronized可以修饰方法,使Synchronized作用于整个方法。该方法就可以锁住,只能同时被一个线程访问,并且整个方法具有原子性。
当sysnchronized修饰静态方法时,所有Rnnable对象都遵从同步机制,比如将实例改为:
class AddOne implements Runnable{
private static int value = 0;
@Override
public void run() {
a();
}
synchronized static void a(){
value+=1;
System.out.println(Thread.currentThread()+" "+ value );
}
}
public class ConcurrentTest4 {
public static void main(String args[]) throws InterruptedException{
ExecutorService exec = Executors.newCachedThreadPool();
//AddOne a = new AddOne();
for(int i=0;i<10;i++){
exec.execute(new AddOne());
}
exec.shutdown();
}
}
输出结果:
Thread[pool-1-thread-4,5,main] 1
Thread[pool-1-thread-8,5,main] 2
Thread[pool-1-thread-1,5,main] 3
Thread[pool-1-thread-9,5,main] 4
Thread[pool-1-thread-10,5,main] 5
Thread[pool-1-thread-7,5,main] 6
Thread[pool-1-thread-3,5,main] 7
Thread[pool-1-thread-6,5,main] 8
Thread[pool-1-thread-5,5,main] 9
Thread[pool-1-thread-2,5,main] 10
注意上述有两处改动:第一:run()方法调用synchronzed修饰的静态方法a();第二:exec.execute()中不再是对象a,而是初始化了10个对象。run()中运行的是类AddOne的静态方法a(),每一个对象指向的都是同一个方法。
2)使用显示的Lock对象
Lock对象必须被显示的创建,锁定,释放。将上述的AddOne类修改为:
class AddOne implements Runnable{
private static int value = 0;
private Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
value+=1;
System.out.println(Thread.currentThread()+" "+ value );
lock.unlock();
}
//synchronized static void a(){}
}
同样可以实现类似与synchronized的效果,虽然代码看起来没有使用synchronized来的优雅,但是在某些情况下却更加灵活。当时用Lock时上述的将lock()的调用放在finally自居中带有unlock()的try-finally语句的惯用法会显得非常重要。finally中的放置unlock确保不会过早释放锁,从而将数据暴露给第二个任务。synchronized中当任务失败会抛出一个异常,却没有机会去做任何清理工作,以维护系统的良好状态。当你尝试获取锁且获取锁失败,或者尝试获取锁一段时间,然后放弃他时就要用到Lock。同时Lock可以更加细致的去控制所得获取与释放。
原子性与易变性
原子性
原子操作时不能被线程机制中断的操作,一旦操作开始那么他一定可以在可能发生“上下文切换”之前(即线程之间的切换)执行完毕。对于读取和写入出long和double之外的基本类型都是原子操作,比如int i = 0。JVM将long和double变量(64位)的读取与写入当作两个分离的32位操作执行,因此只有用关键字volatile修饰该变量使简单的读取写入为原子操作。
volatile关键字保证可见性
当一个任务(比如一个共享的普通变量)被修改时,会暂时的存储在本地处理器的高速缓存中,其他线程的读取或写入就会在这个高速缓存中,当程序运行结束将高速缓存中的数据刷新到主存当中。上当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。