Java synchronization and synchronized lock basics

1. Thread synchronization and locks

 The concept of synchronization and locks is mainly to solve the problem of multi-threaded shared resources. When multiple threads compete for access to shared resources , resulting in inconsistent operating results, thread synchronization and lock mechanisms are usually used to eliminate the differences caused by this multi-threaded competitive access. Example:

public class ThreadCompetitionTest {

	static int count=0;
        public static void main(String[] args) throws InterruptedException {
        long start=System.currentTimeMillis();
        Thread t = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <5000000 ; i++) {
                    count++;
                }
                System.out.println("Custom thread: Completion of calculation..., time-consuming"+(System.currentTimeMillis()-start));
            }
        };
        t.start();
        for (int i = 0; i <5000000 ; i++) {
            count++;
        }
        System.out.println("Main thread: Completion of calculation.... time-consuming"+(System.currentTimeMillis()-start));
        t.join();
        System.out.println("count:"+count);
    }

}

 The results of one of the runs of the sample code are as follows (the results of each run are almost different)

 

Main thread: Computation completed...., time consuming 11
Custom thread: Computation complete..., took 13
count:9973996

 The running result of the sample code is not a fixed 10 million, but is different every time. The main reason for this phenomenon is that the two threads modify the shared resource count variable at the same time, and they count++are not one 原子操作. Each auto-increment is actually divided into 3 steps:

 

  1. Get the current value of the count variable

  2. Increment the current value by 1

  3. Store the value after adding 1 to the count variable

This can be a problem because threads execute in parallel. For example, assuming that the current value of the count variable is 0, the main thread and the custom thread obtain this value at the same time, the main thread completes the auto-increment operation first, and sets the value of the count variable to 1. The custom thread then completes the auto-increment operation, because the custom thread also adds 1 to 0, and then assigns the value to the count variable, which eventually leads to two auto-increment operations, but in fact only adds 1.

 

We can solve the problem in the above code by synchronizing the code block ( synchronized block) and sum . The solution is to modify the count++ in the main thread and the custom thread to:

synchronized (ThreadCompetitionTest.class) {
       count++;
}

Of course, this is only one of the various synchronization lock solutions, here we first solve it with synchronized. No matter how many times you run this modification, the result is 10 million as expected.

 

2. Race conditions and critical sections

Multithreading is not the key to the above problem, the key is that multiple threads access the same resources (ie shared resources), and only if one or more of these threads write to these shared resources can problems cause problems , as long as the resource has not changed, it is safe for multiple threads to read the same resource.

When multiple threads compete for the same resource, if the access order of the resource is sensitive, it is said to exist 竞态条件. The area of ​​code that causes the race condition to occur is called 临界区. For example, the count++ operation in the sample code above is a critical section, which will generate a race condition.

 

It occurs when multiple threads access the same resource at the same time, and one or more of them write to the resource 竞态条件. Multiple threads reading the same resource at the same time will not cause race conditions. Therefore, sometimes we can create immutable shared objects to ensure that objects will not be modified by write operations when shared between threads, thereby eliminating竞态条件。

 

3. Thread Safety

Code that is allowed to be executed by multiple threads simultaneously is called 线程安全code. That is to say, thread-safe code is not sensitive to the access order of multiple threads. No matter what order multiple threads access, the result will not change, so thread-safe code is not included 竞态条件.

 

In general, local basic type variables and local reference type objects that do not escape (in layman's terms, refer to not being referenced by other threads) out of the current thread are thread-safe. For member variables , if two threads can update the same member of the same object at the same time, the code is not thread-safe. For a non-local reference variable , even if it is manipulated internally is a thread-safe object instance reference, the non-local reference variable itself may still be non-thread-safe, because the thread-safe object reference manipulated inside it may be is changed to repoint to another memory address.

 

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

  1. 如果一个资源的创建,使用,销毁都在同一个线程内完成,
  2. 且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。

即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了只在各自线程上下文中使用的数据库连接,相对数据库连接来说,每个连接自身确实是线程安全的,但它们所连接到的同一个数据库实例也许就不是线程安全,例如:如果这两个线程都先查询某条记录X是否存在,如果不存在就插入X记录,如果恰好两个线程同时都查询到不存在X记录,那么将导致这两个线程分别插入了一条相同的记录。

 

类似的问题可能还会发生在文件操作或其他共享资源的操作上,因此,判断线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。

 

四、Synchronized锁使用

synchronized关键字最主要有以下四种使用方式:

  1. 实例方法同步
  2. 实例方法同步块
  3. 静态方法同步
  4. 静态方法同步块

在Java中,每一个对象实例有且仅有一个锁与之相关联,所以任何一个对象实例都可以作为一个锁。当线程要执行同步方法或同步代码块的时候,必须首先获得相应的锁才能进入执行,否则将转为阻塞等待状态,这种阻塞等待除了获得了相应的锁,不能通过中断等方式唤醒从而终止继续阻塞等待。在多个线程同时准备获得同一把锁的时候,最多只能有一个线程能够成功获得这把锁(也称对象监视器),执行完毕退出同步方法或同步代码块之后,释放相应的锁。

 

所以,同步锁是掌握在当前运行线程手里的。另外,静态方法同步/块对应的锁,实际上是Class对象的一个特定实例,而实例同步方法/块的锁,是相应的特定的对象实例。例如,实例同步方法和使用this作为锁的同步代码块,其锁就是当前调用执行该方法的对象实例。

 

后记 

在查阅synchronized相关资料的时候,发现很多资料有提到“synchronized是不能被继承的” ,当我第一次看到这个结论的时候,编写了测试代码进行验证,但是却总是无法证实该观点,于是继续查证,最后在CSND找到如下解释:

 

synchronized不能被继承真相 写道
其实真相是这样的,“synchronized不能被继承”,这句话有2种不同意思,一种是比较正常的、很容易让人想到的意思;另一种是“不正常的”或者说是“java中文界”广泛认同了的意思。
楼主是第一种意思,其他人是第二种意思。所以,会出现该贴的尴尬讨论。
第一种理解方式:父类中有个synchronized方法,子类继承了父类,但子类没覆写该方法。通过子类实例来使用该方法时,按“synchronized不能被继承”,意思就为:该子类的该方法就变成了非synchronized方法。
第二种理解方式:synchronized并不属于方法定义的一部分,不能被继承。子类覆写了该方法,如果在覆写时不明确写上synchronized,那这个方法就不是synchronized。换句话说,虽然继承了,但是没把synchronized继承下来,也就意味着“synchronized不能被继承”。

我觉得“synchronized不能被继承”这句话,没把意思表述清楚。产生这种情况的原因,我推测是这样的:某前辈详细解释了以上2种意思,最后总结的时候,使用了“synchronized不能被继承”这句不太合适的话。某无知后辈转述前辈意思的时候,就直接用了“synchronized不能被继承”。结果一传十,十传百,这句话就传开了,也让初学者产生了迷惑。

在英文世界里,没有“synchronized不能被继承”的讨论。这也说明了点问题。

其实楼主的问题,可以记住这么句话:synchronized方法,一定要显示标明,它是不能隐式标明的。

中文里“一句话多种意思”的问题,真的是给学术界增加了不少麻烦!

 

原来我的理解是第一种方式,而这句话真正要说的是,当子类覆写了父类的同步方法时,synchronized必须显示的指定才会是同步方法,而不能通过从父类继承从而也成为同步方法,如果子类没有覆写父类的同步方法,通过子类实例调用该方法,其实使用的还是父类的方法,因此该方法仍然是synchronized。

 

 

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=327103721&siteId=291194637