This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

synchronized

Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but makes two kinds of errors possible: thread interference and memory consistency errors. The tool needed to prevent these errors is synchronization.

在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作) synchronized 的另外一个重要的作用,synchronized 可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到

The Java programming language provides two basic synchronization idioms: 

synchronized methods and synchronized statements.

we are talking about synchronization idioms

synchronization idioms

To make a method synchronized, simply add the synchronized keyword to its declaration:

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

making these methods synchronized has two effects

  •  methods on the same object to interleave is impossible, other thread must blocked until executing thread finish
  • when a synchronized method exits, Object lock auto release(we will talk later), any subsequent invocation of a synchronized method for the same object must get lock before execute which  automatically establishes a happens-before relationship. This guarantees that changes to the state of the object are visible to all threads.

Note: constructors cannot be synchronized, That’s doesn’t make sense, only thread creates an Object have access to it while it is being constructed

Note: When constructing an object that will be shared between threads, be very careful that a reference to the object does not “leak” prematurely.

if we maintain an List in one thread. which hold all instances, the constructor will look like

 //...new List
 instances.add(this);

If you do that, other thread can call the instances (this) before the construction finish. Which will cause problem.

all reads or writes to that object’s variables are done through synchronized methods with one exception final field, which can not modify after constructed, can be safely read through non-synchronized methods

Intrinsic Locks

we already know sunchronization is to avoid mem-consist problem , How does that work?

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock (an entity simply as a “monitor.”) with two job

  • enforcing exclusive access to an object’s state
  • establishing happens-before relationships that are essential to visibility.

Every object has an intrinsic lock associated with it. when one thread need exclusive and consistent access to an Object, have to acquire lock and release when finish There is no chance to get the same lock when other thread own that. The other thread will block when it attempts to acquire the lock.

The lock release occurs even if the return was caused by an uncaught exception.

we also have static sync method, which is associated with a class, not an object.

public class MyClass {
    private static int staticField = 0;  // 静态字段
    private int instanceField = 0;       // 实例字段

    // 静态方法,访问静态字段
    public static synchronized void incrementStaticField() {
        staticField++;
    }

    // 实例方法,访问实例字段
    public synchronized void incrementInstanceField() {
        instanceField++;
    }
}

the thread acquires the intrinsic lock for the Class object associated with the class, which is different from any instance lock.

Synchronized Statements

Another way to create synchronized code is with synchronized statements. Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock:

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

In that case, addName has to sync change with lastName and nameCount, but also needs to avoid synchronizing invocations of other objects’ methods. (this may cause a deadlock, will talk later)

 Without synchronized statements, there would have to be a separate, unsynchronized method for the sole purpose of invoking nameList.add.

Synchronized statements are also useful for improving concurrency with fine-grained synchronization.

for example, we have two field c1 and c2, they never used together and we should keep all field sync, there’s no reason to prevent an update of c1 from being interleaved with an update of c2, since they wont rely on others.

Instead of using synchronized methods or otherwise using the lock associated with this, we create two objects solely to provide locks.

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

In that case, two variable can update  interleaved.

these two objects considered “locks” not two instences

Reentrant Synchronization

Recall that a thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns.

Allowing a thread to acquire the same lock more than once enables reentrant synchronization. This describes a situation where synchronized code, directly or indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock.

Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block. (dead lock we will talk later)

next atomic

synchronized 关键字最主要有以下 3 种应用方式:

  • 同步方法,为当前对象加锁,进入同步代码前要获得当前对象的锁;

  • 同步静态方法,为当前类加锁,进入同步代码前要获得当前类的锁;

public class AccountingSyncClass implements Runnable {
    static int i = 0;
    /**
     * 同步静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase() {
        i++;
    }
    // 非静态,访问时锁不一样不会发生互斥
    public synchronized void increase4Obj() {
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new新实例
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
/**
 * 输出结果:
 * 2000000
 */
  • 同步代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public class AccountingSync2 implements Runnable {
    static AccountingSync2 instance = new AccountingSync2(); // 饿汉单例模式

    static int i=0;

    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

这里的锁指的是 Java 内置的隐式锁 monitor 也是 synchronized 封装好的实现 每个对象都有一个对象锁,不同的对象,他们的锁不会互相影响。

synchronized 与 happens before

[[JMM内存模型]]

1 - atomic

In programming, an atomic action is one that effectively happens all at once.

In programming, an atomic action is one that effectively happens all at once.

it either happens completely, or it doesn’t happen at all.

some method you can consider as atomic

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).

  • Reads and writes are atomic for all variables declared volatile

Atomic actions cannot be interleaved, so they can be used without fear of thread interference.

volatile idiom

Reads and writes are atomic for all variables declared volatile (include double and long)

In most 32-bit and 64-bit processors, a long or double variable requires two separate 32-bit reads or writes to access the full 64-bit value.

As a result, the operation of reading or writing a long or double is not guaranteed to be atomic at the hardware level.

Using volatile can reduce the risk of mem-consist, because any write to volatile var will establishes a happen-before relationship with subsequent reads of that same variable

This means that changes to a volatile variable are always visible to other threads.

Using simple atomic variable access is more efficient than accessing these variables through synchronized code

2 - Memory Consistency Errors

Memory consistency errors occur when different threads have inconsistent views of what should be the same data.
What is a Memory Consistency Errors?

one of Thread most 3 errors

different threads have inconsistent views of what should be the same data

image.png

The causes of memory consistency errors are complex, Fortunately, we don’t have to need a detailed understanding of these causes.

we need a strategy for avoiding them.

The key to avoid MC errors is understanding Happen-before relationship

Happen-before

a relationship guarantee memory writes by one specific statement are visible to another specific statement

for our example, Change in Thread A will visible to Thread B

image.png

the value will lost, because there’s no guarantee that thread A’s change to i will be visible to thread B

To create a happen-before relation, we can use synchronization

We already see happen-before relationship

  • Thread.start() when a statement invoke start(), every statement happen-before the statement also has same relation with new thread’s statement

    means that the effects of creating new thread are visible

  • Thread.join all the statements executed by the terminated thread have a happens-before relationship with all the statements following the successful join

    the effect of terminated thread now visiable to the thread performed join

3 - ReadWriteLock

A ReadWriteLock maintains a pair of associated locks, one for read-only operations and one for writing.

4 - ReentrantLock重入锁

java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁

重入性

重入性指当线程需要再次获取同一把锁时, 不会因为自身而造成死锁, 锁的本质是作用于代码块或方法,而不是线程的整个执行上下文。即使线程已经持有锁,进入新的同步方法或代码块时,仍然需要执行获取锁的操作,确保锁的计数正确。

image.png

所以支持重入性应该解决下列问题

  • 由于获得多次相同的锁, 需要计数以释放相同次数
  • 相同线程再次获取锁应当直接成功, 防止死锁

为什么需要ReentrantLock

Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁

尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

ReentrantLock使用

public class Counter {
	//和关键字不同, 需要获得一个重入锁对象
    private final Lock lock = new ReentrantLock();
    private int count;
	
    public void add(int n) {
		//代码块加锁
        lock.lock();
        try {
            count += n;
        } finally {
	        //在finally中解锁
            lock.unlock();
        }
    }
}