【JAVA并发】共享资源访问

什么是共享资源

先了解下JAVA程序在运行时内存的分布,由以下部分组成

  1. :所有线程共享一个堆;存放的都是new 出来的对象;
  2. 方法区:所有线程共享一个方法区;里面存放的内容有点杂,可以认为是除堆和栈中的其它东西(如类信息,静态变量,常量,代码等);
  3. 程序计数器:也叫PC,存放下一条指令所在单元的地址的地方;
  4. JAVA栈:每个线程都有一个自己的JAVA栈;存放的一般是方法的局部变量,方法出口信息等;
  5. 本地方法栈:与JAVA栈类似,区别是使用的对象不一样,本地方法栈是给Native方法使用的,JAVA栈是给JAVA方法使用的;

注意上面标红的堆和方法区的数据,是多个线程的共享的资源,会出现多个线程访问共享资源的情况;如多个线程访问同一个对象的成员变量,多个线程访问静态全局变量等...

多个线程不正确的访问共享资源

举个例子,多个线程访问同一个对象的情况,这里被访问的对象为共享资源,代码如下:

定义一个抽象类IntGenerator,类中定义一个next方法用于返回整型值,再定义一个canceled成员属性用于判断该对象是否被取消,注意canceled是boolean类型,关于canceled的操作(赋值和返回操作)都是原子性的,且canceled变量被设置为volatile,保证线程间的可见性;

package concurrency;

public abstract class IntGenerator {
    private volatile boolean canceled = false;

    public abstract int next();

    public void cancel() {
        canceled = true;
    }

    public boolean isCanceled() {
        return canceled;
    }
}

再定义一个具体偶数生产者EvenGenerator,继承IntGenerator类,实现next方法(两个自增操作),注意该操作不是原子性;

package concurrency;

public class EvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;

    public  int next() {
        ++currentEvenValue; // Danger point here!
        ++currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args) {
        EvenChecker.test(new EvenGenerator());
    }
}

定义EvenChecker类,实现Runnable接口,并编写一个test方法,产生10个线程访问同一个EvenGenerator对象,每个线程都调用next方法获取返回值,并判断是否是偶数,如果不是偶数,终止线程返回;:

package concurrency;

import java.util.concurrent.*;

public class EvenChecker implements Runnable {
    private IntGenerator generator;
    private final int id;

    public EvenChecker(IntGenerator g, int ident) {
        generator = g;
        id = ident;
    }

    public void run() {
        while (!generator.isCanceled()) {
            int val = generator.next();
            if (val % 2 != 0) {
                System.out.println(val + " not even!");
                generator.cancel(); // Cancels all EvenCheckers
            }
        }
    }

    public static void test(IntGenerator gp, int count) {
        System.out.println("Press Control-C to exit");
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < count; i++)
            exec.execute(new EvenChecker(gp, i));
        exec.shutdown();
    }

    public static void test(IntGenerator gp) {
        test(gp, 10);
    }
}

运行程序,发现这个程序很快就终止了,因为同一个资源在未加任何保护措施的情况下被多个线程不正确的访问了;

注意,currentEvenValue变量自增需要多个步骤,在这个过程中该线程很有可能会被挂起,此时其它线程访问该变量就会导致结果异常(出现奇数);

有兴趣的可以在next方法前面加个锁,就不会出现异常结果了;

解决共享资源竞争

加锁,synchronized关键字

如下代码,对上面提到的next方法加锁:

    public synchronized int next() {
        ++currentEvenValue; // Danger point here!
        ++currentEvenValue;
        return currentEvenValue;
    }

注意该锁为对象锁,当对象被加锁时,该对象上的其它synchronized方法只有等前一个加锁的方法调用结束释放锁后才开始执行,简单的说,所有的synchronized方法共享同一个对象锁;

还有一种锁叫类锁,一般用在对静态方法加锁,或者synchronized(xxx.class)代码段上;

使用Lock对象

如下写法,显示的调用lock和unlock方法;

package concurrency;

import java.util.concurrent.locks.*;

public class MutexEvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;
    private Lock lock = new ReentrantLock();

    public int next() {
        lock.lock();
        try {
            ++currentEvenValue;
            Thread.yield(); // Cause failure faster
            ++currentEvenValue;
            return currentEvenValue;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        EvenChecker.test(new MutexEvenGenerator());
    }
}

显示的调用lock和unlock方法比内建的synchronized锁更加灵活,允许你尝试去获取锁,或尝试去获取锁一段时间,而不是一直等待获取锁;在尝试获取锁的时候,如果其它线程已经获取这个锁,则可以决定去做一些其它事情,以下代码为尝试获取锁的例子,

package concurrency;

import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class AttemptLocking {
    private ReentrantLock lock = new ReentrantLock();

    public void untimed() {
        boolean captured = lock.tryLock();
        try {
            System.out.println("tryLock(): " + captured);
        } finally {
            if (captured)
                lock.unlock();
        }
    }

    public void timed() {
        boolean captured = false;
        try {
            captured = lock.tryLock(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);
        } finally {
            if (captured)
                lock.unlock();
        }
    }

    public static void main(String[] args) {
        final AttemptLocking al = new AttemptLocking();
        al.untimed(); // True -- lock is available
        al.timed(); // True -- lock is available
        // Now create a separate task to grab the lock:
        new Thread() {
            {
                setDaemon(true);
            }

            public void run() {
                al.lock.lock();
                System.out.println("acquired");
            }
        }.start();
        //Thread.yield(); // Give the 2nd task a chance
        try {
            TimeUnit.MILLISECONDS.sleep(55);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        al.untimed(); // False -- lock grabbed by task
        al.timed(); // False -- lock grabbed by task
    }
} 

在该例子中,前面两次尝试获取锁都可以成功,因为没有其他线程占用该锁,第三第四次获取锁失败,因为中间起了个线程,该线程获取锁后一直未释放;

原子性和可见性

原子性

原子操作是不能被线程中断机制中断的操作,如简单的读取和写入除long和double之外的基本数据类型操作;

如果想让long和double读取和写入操作变成原子性,可以使用volatile关键字;

原子操作看似很安全,但也需要同步,举个例子如下:

package concurrency;

import java.util.concurrent.*;

public class AtomicityTest implements Runnable {
    private int i = 0;

    public int getValue() {
        return i;
    }

    private synchronized void evenIncrement() {
        i++;
        i++;
    }

    public void run() {
        while (true)
            evenIncrement();
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while (true) {
            int val = at.getValue();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

虽然evenIncrement方法加了锁,return i操作也是原子性的,但是通过getValue方法读数据的时候还是产生了奇数,因为getValue方法没有加锁,使其可以访问处于不稳定状态的i变量;

另外,JAVA中的自增操作不是原子性的,其中涉及一个读操作和写操作;

可见性

volatile关键字可以确保被修饰的域在不同线程之间的可见性,在对volatile域上的修改会被立即写到主存中,然后所有读取操作在主存中进行,保证不同线程的可见性;

注意同步也会导致数据向主存中刷新,因此如果一个域已经由synchronized关键字加锁,则不必设置为volatile;

另外,以下两种情况volatile是无法正常工作的,需要使用synchronized:

一个域的值依赖于它之前的值,如递增操作;

一个域的值受限于其它域的值;

总之,使用volatile唯一安全的情况是类中只有一个可变的域;

举个例子,SerialNumberGenerator 类中有一个获取唯一id的方法,代码如下:

package concurrency;

public class SerialNumberGenerator {
    private static volatile int serialNumber = 0;

    public static int nextSerialNumber() {
        return serialNumber++; // Not thread-safe
    }
}

在多线程情况下,调用该类的nextSerialNumber方法,会发现获取的值存在相同的情况,即使serialNumber变量加了volatile来修饰,但是volatile不能对递增不是原子性操作这一事实产生影响;

原子类

在java.util.concurrent.atomic包下有AtomicInteger、AtomicBoolean等特殊的原子性变量类,它们提供了compareAndSet操作,即CAS原子操作,是机器级别上的原子操作;

所谓CAS操作是一种乐观锁技术,是指在操作数据之前先判断数据是否修改过(通过预期值与内存值比较是否相等来判断),只有当数据没被修改过的时候,才写入数据;注意CAS操作存在ABA问题,即A被修改为B又被修改为A,导致无法判断A是否变化过,解决该问题可以通过引入版本号等手段;

下面给出一个使用原子类的例子,该例子未使用synchronized加锁(悲观锁),但同样能够正确运行:

package concurrency;

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class AtomicIntegerTest implements Runnable {
    private AtomicInteger i = new AtomicInteger(0);

    public int getValue() {
        return i.get();
    }

    private void evenIncrement() {
        i.addAndGet(2);
    }

    public void run() {
        while (true)
            evenIncrement();
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicIntegerTest ait = new AtomicIntegerTest();
        exec.execute(ait);
        while (true) {
            int val = ait.getValue();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

上面代码之所以不出现问题,是因为i.addAndGet(2)操作是一个原子操作;

同步控制块

可以用synchronized(syncObject){}对某个代码段进行保护,防止多个线程同时访问该块区域,被保护的这段代码称为同步控制块;

某个线程想要进入同步代码块,需先获得syncObject对象的锁;

使用同步代码块比使用整个方法进行同步控制性能要好;

还可以用Lock对象创建同步控制块(lock和unlock方法);

线程本地存储ThreadLocal

防止多线程访问共享资源产生异常结果的另外一种方式是消除资源的共享,可以使用线程本地存储ThreadLocal达到这样的效果,即为每个线程创建一个共享资源的存储,从而消除多个线程对该资源的共享访问;

可以通过get和set方法访问该对象的内容,以下代码为线程本地存储的例子,从输出结果可以看出,每个线程对value的访问相互独立,互不干扰,因为它们访问的本来就不是同一个资源;

package concurrency;

import java.util.concurrent.*;
import java.util.*;

class Accessor implements Runnable {
    private final int id;

    public Accessor(int idn) {
        id = idn;
    }

    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            ThreadLocalVariableHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }

    public String toString() {
        return "#" + id + ": " + ThreadLocalVariableHolder.get();
    }
}

public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        private Random rand = new Random(47);

        protected synchronized Integer initialValue() {
            return rand.nextInt(10000);
        }
    };

    public static void increment() {
        value.set(value.get() + 1);
    }

    public static int get() {
        return value.get();
    }

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++)
            exec.execute(new Accessor(i));
        TimeUnit.SECONDS.sleep(3); // Run for a while
        exec.shutdownNow(); // All Accessors will quit
    }
} 
...
#1: 4838
#3: 12329
#1: 4839
#3: 12330
#1: 4840
#3: 12331
#1: 4841
#2: 5936
#3: 12332
#0: 14259
#4: 4979
...

参考资料:JAVA编程思想--4

时间: 2024-11-05 11:33:26

【JAVA并发】共享资源访问的相关文章

JAVA跨域资源访问CORSFilter

当一个资源从与该资源本身所在的服务器不同的域或端口不同的域或不同的端口请求一个资源时,资源会发起一个跨域 HTTP 请求. 出于安全考虑,浏览器会限制从脚本内发起的跨域HTTP请求.跨域资源共享机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行.浏览器支持在 API 容器中使用 CORS,以降低跨域 HTTP 请求所带来的风险. 针对于JAVA开发而言,为了更好的做业务分层,经常会将前后端代码分离开来,发布在不同的服务器上,此时,便会遇到跨域的问题. 跨域有很多种解决方

java web 静态资源访问配置三种方式

方案一:Tomcat的defaultServlet来处理静态文件 要放在DispatcherServlet的前面, 让 defaultServlet先拦截请求,这样请求就不会进入Spring了 方案二: 在spring3.0.4以后版本提供了mvc:resource /images/**映射到ResourceHttpRequestHandler进行处理,location指定静态资源的位置.可以是web application根目录下.jar包里面,这样可以把静态资源压缩到jar包中.cache-

[Java并发编程之美]第2章 并发编程的其他基础知识 补充知识

synchronized与volatile关键字 一. synchronized synchronized是Java语法中的一个内置锁的实现.synchronized关键字解决了代码块或者方法上的同步问题,同一时间,只有一个线程能够通过并执行.保证线程安全:内存可见性和原子性提供了并发场景的一个共享资源访问的解决方案. 当我们说synchronized锁住的是一个JVM对象时,真正发挥作用的是对象头上所指向的monitor对象(监视器机制:Java锁的底层实现). synchronized有两种

java并发 之构建线程安全程序 (2)

我们知道线程安全的根本原因在于: 多个线程访问一个共享资源,并且对该共享资源进行非原子性修改. 在一个大型的程序中要找出哪些线程访问了同一个资源是非常复杂且容易出错的.所以在开发线程安全程序时要将共享资源进行封装,控制线程对其的访问. 在java中,面向对象提供了封装和访问内部资源的方式,通过面相对象,不仅可以编写出结构优雅.可维护性高的类,而且有助于编写出线程安全的类. 在java中通过面向对象技术封装和控制共享资源访问. 使用面向对象不仅使得便于封装和控制共享资源,而且也易于构建多个共享资源

java 并发 之 并发安全性问题(1)

多线程编程在提升程序性能的同时也会带来一些安全性问题,产生一下不可预知的结果.其原因在于多线程中每个线程中的操作顺序是不可预测的,对于各个线程共享的资源操作,很可能相互影响.例如统计一个页面的访问量,对一个int类型的值进行累加,每个线程接受到用户请求后就会对该int类型累加.这里就会出现安全性问题,原因在于对数据进行累加的过程并非是一个原子操作,他需要先读取现有的值,然后进行累加,最后重新设置该值.如果在a线程读取现有值后,该值被其他线程更新,那么a线程最后将设置一个错误的值. 出现安全性问题

java访问局域网共享资源

本地要读取存在另一台计算机上的文件,两台主机在同一局域网中,互相拷贝费时费力.只需将要访问的资源设置成资源共享,就可以在局域网其他主机读取该文件. 首先,将要访问文件所在文件夹设置为"共享文件夹". 接下来就可以访问了: public class Testtelnet { public static void main(String[] args) { // TODO Auto-generated method stub File file = new File("\\\\R

java并发编程学习:用 Semaphore (信号量)控制并发资源

并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章. 今天先来学习Semaphore(信号量),字面上看,根本不知道这东西是干啥的,借用 并发工具类(三)控制并发线程数的Semaphore一文中的交通红绿信号灯的例子来理解一下: 一条4车道的主干道,假设100米长,每辆车假设占用的长度为10米(考虑到前后车距),也就是说这条道上满负载运行的话,最多只能容纳4*(100/10)=40辆车,如果有120辆车要通过的

java线程共享受限资源 解决资源竞争 thinking in java4 21.3

java线程共享受限资源 解决资源竞争  详细介绍请参阅:thinking in java4 21.3 thinking in java 4免费下载:http://download.csdn.net/detail/liangrui1988/7580155 package org.rui.thread.res; /** * 不正确的访问 资源 * @author lenovo * */ public abstract class IntGenerator { private volatile bo

Java多线程、主线程等待所有子线程执行完毕、共享资源

1.Java创建与启动线程 Java提供两种方式创建和启动线程:1.直接Thread类,2.实现Runable接口. 1.1  继承Thread类 public class myThread extends Thread { public void run(){ for(int i=0;i<5;i++){ System.out.println(this.getName()+":"+i); } } public static void main(String[] args){ //