【Java并发】共享资源:Lock 、synchronized、Atomic、volatile、local storage线程本地存储

Brian’s Rule of Synchronization——If you are writing a variable that might next be read by another thread, or reading a variable that might have last been written by another thread, you must use synchronization, and further, both the reader and the writer must synchronize using the same monitor lock.

Improperly accessing resources

import java.util.concurrent.*;
//this class is the share resources
abstract class IntGenerator{
    private volatile boolean canceled =false;
    public abstract int next();
//    allow this to be canceled
    public void canceled(){ canceled=true;}
    public boolean isCanceled() {return canceled;}
}
public class ShareResource implements Runnable{
    private IntGenerator generator;
    private final int id;
    public ShareResource(IntGenerator g , int ident){
        generator=g;
        id=ident;
    }
    @Override
    public void run() {
        while (!generator.isCanceled()){
            int val=generator.next();
            if(val %2 !=0){
                System.out.println(Thread.currentThread().getName()+" "+val +" not even");
                generator.canceled();
            }
        }
    }

    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 ShareResource(gp,i));
        }
        exec.shutdown();
    }
    public static void test(IntGenerator gp){
        test(gp,4);
    }
    public static void main(String[] args) {
        ShareResource.test(new EventGenerator());

    }
}
class EventGenerator extends IntGenerator{
    private int currentEvenValue = 0;
    @Override
    public int next() {
        ++currentEvenValue;   //danger point here
        ++currentEvenValue;
        return currentEvenValue;
    }
}

test(gb ,4) 产生4个线程对数据进行操作。IntGenerator是抽象类,EventGenerator继承于IntGenerator,在next() 函数中对局部变量currentEvenValue自增两次,在两次的间隙是可打断的,产生race condition ,当val %2 !=0 时线程出现data race bug。此时volatile boolean canceled 被设为true 程序停止运行。

请注意,在此示例中,可以取消的类不是Runnable。 相反,所有依赖于IntGenerator对象的ShareResource任务都会对其进行测试,以查看其是否已取消。 这样,共享公共资源(IntGenerator)的任务会监视该资源以使信号终止。
注意,如果generator.isCanceled()true,则run() 返回,告诉ShareResource.test() 中的执行程序任务已完成。 任何ShareResource任务都可以在其关联的IntGenerator上调用cancel(),这将导致使用该IntGenerator的所有其他ShareResource正常关闭

Resolving shared resources contention

Preventing this kind of collision is simply a matter of putting a lock on a resource when one task is using it. The first task that accesses a resource must lock it, and then the other tasks cannot access that resource until it is unlocked, at which time another task locks and uses it, and so on.

This is ordinarily accomplished by putting a clause around a piece of code that only allows one task at a time to pass through that piece of code. Because this clause produces mutual exclusion, a common name for such a mechanism is mutex.

Synchronizing

Synchronized关键字深度解析
将示例中的next()函数改为synchronized next() 或者在函数内 synchronized(this){ code } 即可

Lock objects

The Lock object must be explicitly created, locked and unlocked; thus, it produces less elegant code than the built-in form. However, it is more flexible for solving certain types of problems.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexEvenGenerator extends IntGenerator{
   private int currentEvenValue =0;
   private Lock lock = new ReentrantLock();
   public int next(){
       lock.lock();
       try {
           ++currentEvenValue;
           Thread.yield();
           ++currentEvenValue;
           return currentEvenValue;
       }finally {
           lock.unlock();
       }
   }

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

When you are using Lock objects, it is important to internalize the idiom shown here: Right after the call to lock( ), you must place a try-finally statement with unlock( ) in the finally clause—this is the only way to guarantee that the lock is always released. Note that the return statement must occur inside the try clause to ensure that the unlock( ) doesn’t happen too early and expose the data to a second task.

  • synchronized同步失败时,无法释放资源,lock使用unlock可释放资源,清理状态。
  • synchronized无法在尝试获取锁时失败,或者持锁一段时间后释放。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

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
        al.untimed(); // False -- lock grabbed by task
        al.timed(); // False -- lock grabbed by task
    }
}

A ReentrantLock allows you to try and fail to acquire the lock, so that if someone else already has the lock, you can decide to go off and do something else rather than waiting until it is free,

Atomicity and volatile

“原子操作不需要被同步” 除非你是并发专家,否则尽量少用原子操作。

Atomicity applies to “simple operations” on primitive types except for longs and doubles…However, the JVM is allowed to perform reads and writes of 64- bit quantities (long and double variables) as two separate 32-bit operations, raising the possibility that a context switch could happen in the middle of a read or write…you do get atomicity (for simple assignments and returns) if you use the volatile keyword when defining a long or double variable

原子操作可对除了longsdoubles 的原始类型变量进行简单操作,因为他们是64位,而jvm允许将64位数据分为两个32位操作,这在读或者写操作中造成上下文切换的概率增大,导致同步出错。使用volatile关键字可以使以上两个变量原子化。

Changes made by one task, even if they’re atomic in the sense of not being interruptible, might not be visible to other tasks (the changes might be temporarily stored in a local processor cache, for example), so different tasks will have a different view of the application’s state. The synchronization mechanism, on the other hand, forces changes by one task on a multiprocessor system to be visible across the application. Without synchronization, it’s indeterminate when changes become visible.

同步机制迫使任务在多处理器系统下被其他应用可见。在没有同步情况下,无法确定改变后的变量是否可见。

The volatile keyword also ensures visibility across the application. If you declare a field to be volatile, this means that as soon as a write occurs for that field, all reads will see the change. This is true even if local caches are involved—volatile fields are immediately written through to main memory, and reads occur from main memory.
It’s important to understand that atomicity and volatility are distinct concepts. An atomic operation on a non-volatile field will not necessarily be flushed to main memory, and so another task that reads that field will not necessarily see the new value. If multiple tasks are accessing a field, that field should be volatile; otherwise, the field should only be accessed via synchronization. Synchronization also causes flushing to main memory, so if a field is completely guarded by synchronized methods or blocks, it is not necessary to make it volatile.

volatile关键字保证了跨应用的可见性,它将数据立刻写进主存,读取数据也从主存读取。

Distinct concepts

  • 在非volatile区域的进行的原子操作不会立马写进主存,因此其他任务读取的数据不是最新的。
  • 若多任务对一个共享区域访问,这个区域需要volatile,否则要通过同步访问。同步机制也可立即写进主存。
  • 一个区域若被synchronized 方法或则block控制,则不需要volatile
    在同一个任务内的数据不需要volatile
  • volatile的值来自于他自身之前的值(i++)或者受限于其他区域的值时,volatile不起作用
  • 如果将变量定义为volatile,它会告诉编译器不要进行任何优化,这些优化将删除读写操作,从而使字段与线程中的本地数据保持完全同步。 实际上,读取和写入直接进入内存,并且不被缓存,volatile也限制了编译器在优化过程中对访问的重新排序。 但是,volatile不会影响增量不是原子操作的事实。

It’s typically only safe to use volatile instead of synchronized if the class has only one mutable field. Again, your first choice should be to use the synchronized keyword

Java increment is not atomic and involves both a read and a write, so there’s room for threading problems even in such a simple operation

Basically, you should make a field volatile if that field could be simultaneously accessed by multiple tasks, and at least one of those accesses is a write. For example, a field that is used as a flag to stop a task must be declared volatile; otherwise, that flag could be cached in a register, and when you make changes to the flag from outside the task, the cached value wouldn’t be changed and the task wouldn’t know it should stop.

Atomic class

Java SE5 introduces special atomic variable classes such as Atomiclnteger, AtomicLong, AtomicReference, etc. that provide an atomic conditional update operation of the form:
boolean compareAndSet(expectedValue, updateValue);

展示AtomicInterger部分源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }

    /**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }

    /**
     * Sets to the given value.
     *
     * @param newValue the new value
     */
    public final void set(int newValue) {
        value = newValue;
    }

    /**
     * Eventually sets to the given value.
     *
     * @param newValue the new value
     * @since 1.6
     */
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    /**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 ...

Critical sections

Sometimes, you only want to prevent multiple thread access to part of the code inside a method instead of the entire method. The section of code you want to isolate this way is called a critical section and is created using the synchronized keyword

笔记:Java高并发 Synchronized关键字深度解析

Thread local storage

ThreadLocal存储在static区。ThreadLocal<T> 对象为每个线程创建本地存储。
尽管只有一个ThreadLocalVariableHolder对象,每个线程对自己的数据进行操作互不干扰。

package conTest;
import java.lang.ThreadLocal;
import java.util.Random;
import java.util.concurrent.*;

class Accessor implements Runnable{
    private final int id;
    public Accessor(int idn){
        id=idn;
    }
    @Override
    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 accessor will quit

    }
}

ThreadLocal objects are usually stored as static fields. When you create a ThreadLocal object, you are only able to access the contents of the object using the get( ) and set( ) methods. The get( ) method returns a copy of the object that is associated with that thread, and set( ) inserts its argument into the object stored for that thread, returning the old object that was in storage. The increment( ) and get( ) methods demonstrate this in ThreadLocalVariableHolder. Notice that increment( ) and get( ) are not synchronized, because ThreadLocal guarantees that no race condition can occur.

发布了27 篇原创文章 · 获赞 1 · 访问量 695

猜你喜欢

转载自blog.csdn.net/SUKI547/article/details/101272460