Concurrency of java programming ideas (shared resources)

Concurrency of java programming ideas (shared resources)

Pay attention to my official account: Android development bragging. Learn from each other

With concurrency, we can do many things at the same time, but the problem of two or more threads interfering with each other also exists. If this conflict is not prevented, two threads may access a bank account at the same time, print to the same printer, and change the same value.

Share resource

A single thread can only do one thing at a time. Because there is only one entity, you never have to worry about two people parking in the same place. But multiple threads will access a resource at the same time.

Incorrect access to resources

Let's first do an experiment and multiple tasks. One task generates an even number, and other tasks check the validity of the even number.

public abstract class IntGenerator {
	// volatile  
	private volatile boolean canceled = false;
	public abstract int next();
	public void cancel(){
		canceled = true;
	}
	//
	public boolean isCanceled() {
		return canceled;
	}
}

 

Any IntGenerator can be tested with the following EvenChecker class:

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

	protected EvenChecker(IntGenerator generator, int id) {
		super();
		this.generator = generator;
		this.id = id;
	}

	@Override
	public void run() {
		while (!generator.isCanceled()) {
			int val = generator.next();
			if (val %2 !=0) {
				System.out.println(" ");
				generator.cancel();
			}

		}

	}

	public static void test(IntGenerator gp,int count) {
		ExecutorService service = Executors.newCachedThreadPool();
		for (int i = 0; i < count; i++) {
			service.execute(new EvenChecker(gp, i));
		}
		service.shutdown();
	}

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

}
 

The example above, generator.cancel()the revocation is not the task itself, but the object can be revoked if IntGenerator conditions. You must carefully consider all possible ways for a concurrent system to fail, for example, one task cannot depend on another task. Because the order in which tasks are closed cannot be guaranteed. Here, by making the task dependent on non-task objects, we can eliminate potential race conditions.

EvenChecker always reads and tests the return value of IntGenerator. If the return value of isCanceled() is true, run() returns, which will inform the Executor in test() that the task is completed. Any EvenChecker task can call cancel() on the IntGenerator associated with it, which will cause all other EvenChecker using the IntGenerator to be closed.

The first IntGenerator has a next() method that can generate a series of even values:

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

	@Override
	public int next() {
		++currentEvenValue;
		++currentEvenValue;

		return currentEvenValue;
	}

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

 

Results of the:

1537 
1541 
1539 

 

One task may call the next() method after another task performs the first increment operation, but not before the second increment operation. This will put this value in an inappropriate state. To prove that this is possible, the text() method creates a set of EvenChecker objects to continuously read and output the same EvenGenerator, and check whether each value is even. If not, report an error and terminate.

This program will eventually fail and terminate, because each EvenChecker task can still access the information in EvenGenerator when it is in an inappropriate state. However, depending on the operating system and implementation details, this problem may not be detected after multiple cycles. It is important to note that the incrementing program itself also requires multiple steps, and tasks may be suspended during the incrementing process. In other words, increment is not an atomic operation in Java. Therefore, if the task is not protected, even a single increment is not safe.

Solve competition for shared resources

The previous example shows a basic problem with threads: you never know when a thread is running. For concurrent operations, you need some way to prevent two tasks from accessing the same resources, at least not in the critical phase. The way to prevent this conflict is to lock the resource when it is used by a task. The first task to access a certain resource must lock this resource so that other tasks cannot access it until it is unlocked. When it is unlocked, another task can lock and use it, and so on.

Basically, all concurrency modes use serialized access to shared resources when solving the problem of thread conflicts. This means that only one task is allowed to access shared resources at a given moment. Usually this is achieved by adding a lock statement in front of the code, which allows only one task to run this code within a period of time. Because the lock statement produces a mutually exclusive effect, this mechanism is called a mutex.

In addition, when a lock is unlocked, we cannot determine the next task that uses the lock, because the thread scheduling mechanism is not deterministic. You can use yield() and setPriorit() to provide suggestions to the thread scheduler.

Java provides built-in support for preventing resource conflicts in the form of providing the keyword synchronized. When the task wants to execute the code fragment protected by the synchronized keyword, it will check whether the lock is available, then acquire the lock, execute the code, and release the lock. Shared resources generally exist in memory fragments as objects, which can be files, input and output ports. To control access to shared resources, you must first package it into an object. Then mark all methods that need to access this resource as synchronized.

Here is how to declare the synchronized method:

synchronized void f(){};
synchronized void g(){};
 

All objects automatically contain a single lock (monitor). When calling any of its synchronized methods on an object, the object is locked. At this time, other synchronized methods on this object can only be called after the previous method is called and the lock is released. For a specific object, all its synchronized methods share the same lock, which can be used to prevent multiple tasks from accessing the memory coded as the object at the same time.

Note: It is very important to set the object as private when using concurrency. Otherwise, the synchronized keyword cannot prevent other tasks from directly accessing the domain, which will cause conflicts.

There is also a lock for each class, so the synchronized static method can prevent concurrent access to static data within the scope of the class.

When should I synchronize?

 
 

Synchronous control of EvenGenerator

By adding the synchronized keyword to EvenGenerator, you can prevent unwanted thread access:

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

	@Override
	public synchronized int next() {
		++currentEvenValue;
		Thread.yield();
		++currentEvenValue;

		return currentEvenValue;
	}

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

The call to Thread.yield() is inserted between the two threads to increase the possibility of odd numbers. Because mutual exclusion can prevent multiple tasks from entering the critical section at the same time, there will not be any failures above. The first task that enters next() acquires the lock, and any other tasks that try to acquire the lock will be blocked until the first task releases the lock.

Use the displayed Lock object

The Java SE5 class library also contains the displayed mutual exclusion mechanism defined in java.util.concurrent.locks. Lock objects must be explicitly created, locked, and released. Therefore, compared with the built-in lock form, the code lacks elegance. But it is more flexible when solving certain types of problems.

Now rewrite the above code with the shown Lock:

public class EvenGenerator extends IntGenerator{
	private int currentEvenValue = 0;
	//
	private Lock lock = new ReentrantLock();
	@Override
	public int next() {
		//
		lock.lock();
		try {
			++currentEvenValue;
			Thread.yield();
			++currentEvenValue;

			return currentEvenValue;
		}finally {
			//
			lock.unlock();
		}
	}

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

 

When you are using the lock object, the idiom of the example is important: the call to the unlock() method must be placed in a try-finlly statement. Note that the return statement must appear in the try clause to ensure that unlock() does not occur prematurely, exposing the data to the second task. Although there are more try-finlly clauses than synchronized keywords, the advantages of the lock shown are obvious. If something fails when using the synchronized keyword, an exception will be thrown. But we did not have the opportunity to deal with it in order to maintain the good state of the system. With the displayed lock object, you can use the finlly clause to maintain the correct state of the system. In general, we use synchronized more often, and only use the displayed lock object when we encounter special problems.

Example: Using the synchronized keyword cannot try to acquire the lock and the acquisition will fail, or try to acquire for a period of time and then give up.

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 {
	    	//2 
	      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
	  }
}

 

Results of the:

tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
acquired
 

ReentrantLock allows us to try to acquire the lock but never acquire the lock, so if someone else has already acquired the lock, then you can decide to leave and do other things instead of waiting for the lock to be released. The displayed Lock object also gives you more fine-grained control in terms of locking and releasing locks compared to the built-in synchronized lock.

Atomicity and variability

In Java threads, we often think that atomic operations do not require synchronization control. Atomic operations cannot be interrupted by the thread scheduling mechanism. Such thinking is wrong, and it is dangerous to rely on atomicity. Atomicity has achieved some more clever constructions in the Java class library. Atomicity can be applied to "simple operations" on all basic types except long and double. But jvm will execute 64-bit long and double operations as two separate 32-bit operations, which creates a context switch between read and write operations, resulting in incorrect results for different tasks The possibility. But if we use the volatile keyword, we will get atomicity (it has not worked correctly before Java SE5).

Therefore, the atomic operation can be guaranteed by the thread mechanism to be uninterruptible, but even so, this is a simplified mechanism. Sometimes seemingly safe atomic operations may actually be unsafe.

On multi-core processors, there are far more visibility issues than atomicity issues. Modifications made by one task may not be visible to other tasks. Because each task temporarily stores information in the cache. The synchronization mechanism enforces that changes made by a task in the processor must be visible. The volatile keyword ensures this visibility. If a task modifies the operation on this modified object, then other task read and write operations can see this modification. It can be seen even if the cache is used, because volatile will be written to main memory immediately. The read and write operations occur in main memory. Synchronization will also result in flushing to main memory, so if an object is protected by synchronized for so long, it is not necessary to use volatile modification. The only safe case for using volatile instead of synchronized is that there is only one variable field in the class. Our first choice should be the synchronized keyword, which is the safest way.

What is atomic operation?

Assignment and return operations to values in the field are usually atomic. But increment and decrement are not:

public class Atomicity {
	int i;
	void f(){
		i++;
	}
	void g(){
		i +=3;
	}
}
 

We look at the compiled file:

void f();
		0  aload_0 [this]
		1  dup
		2  getfield concurrency.Atomicity.i : int [17]
		5  iconst_1
		6  iadd
		7  putfield concurrency.Atomicity.i : int [17]
 //Method descriptor #8 ()V
 //Stack: 3, Locals: 1
 void g();
		0  aload_0 [this]
		1  dup
		2  getfield concurrency.Atomicity.i : int [17]
		5  iconst_3
		6  iadd
		7  putfield concurrency.Atomicity.i : int [17]
}
 

Each instruction produces a get and put, and there are some other instructions between them. Therefore, between acquisition and modification, another task may modify this field. Therefore, these operations are not atomic:

Let's see if the following example meets the above description:

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);
	      }
	    }
	  }
}

 

Test Results:

1

 

The program finds odd numbers and terminates. Although return i is an atomic operation, the lack of synchronization allows its value to be read in an unstable intermediate state. There is also a visibility problem because i is not volatile. Both getValue() and evenIncrement() must be synchronized. For basic types of read and assignment operations are considered safe atomic operations. But when the object is in an unstable state, it is still very likely to be accessed using atomic operations. The most sensible approach is to follow the rules of synchronization.

Atomic class

Java SE5 introduced special atomic variable classes such as AtomicInteger, AtomicLong, AtomicReference, etc. They provide the following forms of atomic conditional update operations:

boolean compareAndSet(expectedValue,updateValue);
 

These classes are adjusted to be used on modern processors and are machine-level atomic, so there is no need to worry when using them. They are rarely used conventionally, but they are very useful for performance tuning.

Example, rewrite the above example:

public class AtomicIntegerTest implements Runnable{
	private AtomicInteger ger = new AtomicInteger(0);
	public int getValue() {
		return ger.get();
	}
	private void eventIncrement() {
		ger.addAndGet(2);
	}

	@Override
	public void run() {
		while (true) {
			eventIncrement();
		}

	}

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

}

 

Atomic classes are designed to build classes in Java.util.concurrent, so they are only used in code under special circumstances. The above example can get good synchronization without using any locking mechanism. But usually relying on locks is a little safer for us.

Critical section

Sometimes we need to prevent multiple threads from accessing part of the code inside the method at the same time instead of preventing access to the entire method. The code separated in this way is called a critical section, and it is also modified with the synchronized keyword. The syntax is: synchronized is used to specify an object, and the lock of this object is used to synchronize the code in the brackets:

synchronized (syncObject){
	//
}
 

This is called a synchronized code block; before entering this piece of code, you must get the lock of the syncObject object. If other threads have already obtained the lock, they will have to wait until the lock is released before entering the critical section. By using the synchronization control block instead of the entire method for synchronization control, the timeliness for multiple tasks to access objects can be significantly improved.

The following example compares two synchronization control methods:

public class Pair {
	  private int x, y;
	  public Pair(int x, int y) {
	    this.x = x;
	    this.y = y;
	  }
	  public Pair() { this(0, 0); }
	  public int getX() { return x; }
	  public int getY() { return y; }
	  //

	  public void incrementX() {
		  x++;
	  }
	  public void incrementY() {
		  y++;
	  }

	  public String toString() {
	    return "x: " + x + ", y: " + y;
	  }

	  public class PairValuesNotEqualException extends RuntimeException {
	    public PairValuesNotEqualException() {
	      super("Pair values not equal: " + Pair.this);
	    }
	  }
	  //Arbitrary invariant -- both variables must be equal:
	  public void checkState() {
	    if(x != y)
	      throw new PairValuesNotEqualException();
	  }
}

 

Template class:

public abstract class PairManager {
      //
	  AtomicInteger checkCounter = new AtomicInteger(0);
	  protected Pair p = new Pair();
	  //
	  private List<Pair> storage =Collections.synchronizedList(new ArrayList<Pair>());

	  //
	  public synchronized Pair getPair() {
	    //Make a copy to keep the original safe:
	    return new Pair(p.getX(), p.getY());
	  }

	  //  50 
	  protected void store(Pair p) {
	    storage.add(p);
	    try {
	      TimeUnit.MILLISECONDS.sleep(50);
	    } catch(InterruptedException ignore) {

	    }
	  }

	  public abstract void increment();
}

 

Implementation template:

public class PairManager1 extends PairManager{

	//
	@Override
	public synchronized void increment() {
		// 
		 p.incrementX();
		 p.incrementY();
		 store(getPair());
	}

}

public class PairManager2 extends PairManager{

	@Override
	public void increment() {
		Pair temp;
		//
	    synchronized(this) {
	      p.incrementX();
	      p.incrementY();
	      temp = getPair();
	    }
	    store(temp);

	}

}

 

Create two threads:

public class PairManipulator implements Runnable {

	  private PairManager pm;
	  public PairManipulator(PairManager pm) {
	    this.pm = pm;
	  }

	  public void run() {
	    while(true)
	      pm.increment();
	  }

	  public String toString() {
	    return "Pair: " + pm.getPair() +
	      " checkCounter = " + pm.checkCounter.get();
	  }

}

public class PairChecker implements Runnable{

	  private PairManager pm;
	  public PairChecker(PairManager pm) {
	    this.pm = pm;
	  }
	  public void run() {
	    while(true) {
	      pm.checkCounter.incrementAndGet();
	      pm.getPair().checkState();
	    }
	  }

}
 

Test category:

public class CriticalSection {
	 static void testApproaches(PairManager pman1, PairManager pman2) {
	    ExecutorService exec = Executors.newCachedThreadPool();

	    PairManipulator
	      pm1 = new PairManipulator(pman1),
	      pm2 = new PairManipulator(pman2);
	    PairChecker
	      pcheck1 = new PairChecker(pman1),
	      pcheck2 = new PairChecker(pman2);

	    exec.execute(pm1);
	    exec.execute(pm2);
	    exec.execute(pcheck1);
	    exec.execute(pcheck2);
	    try {
	      TimeUnit.MILLISECONDS.sleep(500);
	    } catch(InterruptedException e) {
	      System.out.println("Sleep interrupted");
	    }
	    System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
	    System.exit(0);
	  }
	  public static void main(String[] args) {
	    PairManager
	      pman1 = new PairManager1(),
	      pman2 = new PairManager2();
	    testApproaches(pman1, pman2);
	  }
}

 

Final test results:

pm1: Pair: x: 11, y: 11 checkCounter = 2183
pm2: Pair: x: 12, y: 12 checkCounter = 24600386
 

Although the results of each run may be different, in general, PairChecker checks the frequency of PairManager1 less than PairManager2. The latter is controlled by synchronous code blocks, so the object is not locked for longer. Makes other threads more accessible.

Sync on other objects

The synchronized block must be given an object synchronized on it, and a reasonable way is to use the current object whose method is being called: synchronized(this). In this way, if the lock on the synchronized block is obtained, then the The other synchronized methods and critical sections of the object cannot be called.

Sometimes you must synchronize on another object, but if you do, you must ensure that all related tasks are synchronized on the same object.

The following example demonstrates that two tasks can enter the same object at the same time, as long as the methods on this object are synchronized on different locks:

class DualSynch {
  private Object syncObject = new Object();
  public synchronized void f() {
    for(int i = 0; i < 5; i++) {
      print("f()");
      Thread.yield();
    }
  }
  public void g() {
    synchronized(syncObject) {
      for(int i = 0; i < 5; i++) {
        print("g()");
        Thread.yield();
      }
    }
  }
}

public class SyncObject {
  public static void main(String[] args) {
    final DualSynch ds = new DualSynch();
    new Thread() {
      public void run() {
        ds.f();
      }
    }.start();
    ds.g();
  }
}
 

Results of the:

g()
f()
g()
f()...
 

Where f() is synchronized on this, and g() is a synchronized block synchronized on a syncObject. Therefore, these two synchronizations are independent of each other. It can be seen from the method call in main() that these two methods are not blocking.

Thread local storage

The second way to prevent tasks from conflicting on shared resources is to eradicate the sharing of variable memory. Thread local storage is an automated mechanism that can create different storage for each different thread of the same variable. Therefore, if you have 5 threads, then the threads will generate 5 different storage blocks locally. They allow you to associate state with threads.

Creating and managing thread local storage can be implemented by the java.lang.ThreadLocal class:

public class Accessor implements Runnable{

	private final int id;
	protected Accessor(int id) {
		super();
		this.id = id;
	}
	@Override
	public void run() {
		while (!Thread.currentThread().isInterrupted()) {
			ThreadLocalVariableHolder.increment();
			System.out.println(this);
			Thread.yield();
		}

	}
	@Override
	public String toString() {
		//TODO Auto-generated method stub
		return "#"+id+":"+ThreadLocalVariableHolder.get();
	}

}
 

Thread local storage:

public class ThreadLocalVariableHolder {
	private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
		private Random dRandom = new Random(47);
		protected synchronized Integer initialValue(){
			return dRandom.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 executorService = Executors.newCachedThreadPool();
		for (int i = 0; i < 5; i++) {
			executorService.execute(new Accessor(i));
		}
		TimeUnit.SECONDS.sleep(3);
		executorService.shutdown();
	}
}

 

Test Results:

#0:712564
#0:712565
#0:712566
#0:712567
#0:712568/...
 

ThreadLocal objects are usually treated as static storage domains. When creating a ThreadLocal method, you can only access the content through the get() and set() methods. The get() method returns the copy associated with the object, and the set() will insert the parameters into the object stored for its thread , And return the original object in storage. When you run this program, you will find that each individual thread has allocated its own storage, because each of them has to keep track of its own count value.