Home >Java >javaTutorial >Java thread (2) - Detailed explanation of thread synchronization
In order to speed up the running speed of the code, we adopt a multi-threading method. Parallel execution does make the code more efficient, but the problem that comes with it is that there are many threads running at the same time in the program. If they modify an object at the same time, it is likely to cause corruption. At this time we A synchronization mechanism is needed to manage these threads.
I remember that there is a picture in the operating system that impressed me deeply. What is drawn above is a block of processes. Several threads are divided into these processes. All these threads point to the resources of the process in a unified manner. The same is true in Java. Resources are shared between threads instead of each thread having an independent resource. In this kind of sharing situation, it is very likely that multiple threads are accessing a resource at the same time. This phenomenon is called race condition.
In a banking system, each thread manages an account, and these threads may perform transfer operations.
When a thread performs an operation, it first stores the account balance in the register. In the second step, it reduces the number in the register by the amount of money to be transferred. In the third step, it writes the result back to the balance.
The problem is that when this thread completes steps 1 and 2, another thread wakes up and modifies the account balance value of the first thread, but the first thread does not know about it at this time. After the first thread waits for the second thread to finish executing, it continues with its third step: writing the result back to the balance. At this time, it wipes out the operation of the second thread, so the total amount of money in the entire system will definitely be sent as an error.
This is an undesirable situation when Java race conditions occur.
The above example tells us that if our operation is not an atomic operation, being interrupted is It will definitely happen, even if the probability is really very small sometimes, it cannot be ruled out. We can't turn our code into atomic operations like in the operating system, what we can do is lock our code to ensure security. In a concurrent program, if we want to access data, we must first set a lock on our code. While we are using the lock, the resources involved in our code will seem to be "locked". It cannot be accessed by other threads until we open this lock.
In java, the synchronized keyword and the ReentrantLock class both have this lock function. Let's first discuss the functions of ReentrantLcok here.
In this class, two constructors are provided, one is the default constructor, there is nothing to say, and the other is with fairness Constructor for the strategy. This fair strategy is, first of all, much slower than normal locks, and secondly, it is not really fair in some cases. And if we really need a fair strategy for no special reason, try not to study this strategy.
ReentrantLock myLock = new ReentrantLock(); //创建对象 myLock.lock(); //获取锁try{...} finally{ myLock.unlock(); //释放锁 }
Be sure to remember to release the lock in finally! ! We said before that unchecked errors can lead to thread termination. An inexplicable termination will stop the program from running downward. If the release is not placed in finally, the lock will never be released. This principle is the same as when we use package .close()
in our daily framework. Speaking of close, it is worth mentioning that when we use a lock, we cannot use a "try statement with resources" because the lock is not closed with close. If you don't know what a try statement with resources is, just pretend I didn't say it.
If you want to use a lock in a recursive or cyclic program, then feel free to use it. The ReentrantLock lock is reentrant. It maintains a count to record the number of calls every time lock() is called. Unlock must be used to release each lock call.
Usually, threads find a problem after locking and entering the critical section. The resources they need are in other objects. are used or do not meet the conditions for their execution. At this time, we need to use a condition object to manage these threads that have obtained a lock but cannot do useful work.
if(a>b){ a.set(b-1);}
The above is a very simple conditional judgment, but we cannot write it like this in concurrent programs. The problem is that if another thread is awakened just after this thread has made the judgment, and the other thread makes a less than b after the operation (the condition in the if statement is no longer correct).
那么这个时候我们可能想到,我们把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但是如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句,这时,我们突然发现,if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确,这真的是非常尴尬,我们自己的锁把我们自己困住了,我们出不去,别人进不来。
为了解决这种情况,我们用ReentrantLock类中的newCondition方法来获取一个条件对象。
Condition cd = myLock.newCondition();
获取了Condition对象之后,我们就应该来研究这个对象有什么方法和作用了。先不急于看API,我们回到主题发现现在亟待解决的就是if条件判断的问题,我们如何才能:在已经上锁的情况下,发现if判断错误时,给其他线程机会并自己一直等着if判断变回正确。
Condition类就是为了解决这个难题而生的,有了Condition类之后,我们在if语句下面直接跟上await方法,这个方法表示这个线程被阻塞,并放弃了锁,等其他的线程来操作。
注意在这里我们用的名词是阻塞,我们之前也说过阻塞和等待有很大不同:等待获得锁时,一旦锁有了空闲,他可以自动的去获得锁,而阻塞获得锁时,即使有空闲的锁,也要等待线程调度器允许他去持有锁的时候才能获得锁。
其他的线程在顺利执行if语句内容之后,要去调用signalAll方法,这个方法将会重新去激活所有的因为这个条件被阻塞的线程,让这些线程重新获得机会,这些线程被允许从被阻塞的地方继续进行。此时,线程应该再次测试该条件,如果还是不能满足条件,需要再次重复上述操作。
ReentrantLock myLock = new ReentrantLock(); //创建锁对象myLock.lock(); //给下面的临界区上锁 Condition cd = myLock.newCondition(); //创建一个Condition对象,这个cd对象表示条件对象while(!(a>b)) cd.await(); //上面的while循环和await方法调用是标准写法 //如果不能满足if的条件,那么他将进入阻塞状态,放弃锁,等待别人去激活它a.set(b-1); //一直等到从while循环出来,满足了判断的条件,我们执行自己的功能cd.signalAll(); //最后一定不能忘记调用signalAll方法去激活其他的被阻塞的线程 //如果所有的线程都在等待其他线程signalAll,则进入死锁
非常不妙的,如果所有的线程都在等待其他线程signalAll,则进入死锁的状态。死锁状态是指所有的线程需要的资源都被其他的线程形成环状结构而导致谁都不能执行的情况。最后调用signalAll方法激活其他因为cd而阻塞的“兄弟”是必须的,方便你我他,减少死锁的发生。
总结来说,Condition对象和锁有这样几个特点。
锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
锁可以管理试图进入临界区的线程
锁可以拥有一个或多个条件对象
每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程
我们上面介绍的ReentrantLock和Condition对象是一种用来保护代码片段的方法,在java中还有另外一种机制:通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。从版本开始,java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。
我们先拿出上面的代码:
public void function(){ ReentrantLock myLock = new ReentrantLock(); myLock.lock(); Condition cd = myLock.newCondition(); while(!(a>b)) cd.await(); a.set(b-1); cd.signalAll(); }
如果我们用synchronized来实现这段代码,将会变成下面的样子:
public synchronized void function(){ while(!(a>b)) wait(); a.set(b-1); notifyAll(); }
需要我们注意的是,在使用synchronized关键词时,无需再去用ReentrantLock和Condition对象,我们用wait方法替换了await方法,notifyAll方法替换了signalAll方法。这样写确实比之前的简单了很多。
将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。
内部锁虽然简便,但是他存在着很多限制:
不能中断一个正在试图获得锁的线程
试图获得锁时不能设定超时
因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的
在代码中应该使用这两种锁中的哪一种呢?Lock和Condition对象还是同步方法?在core java一书中有一些建议:
It is best to use neither ReentrantLock nor the synchronized keyword. In many cases you can use the java.util.concurrent package
If synchronized meets the needs of your code, please use it first
Until If you particularly need ReentrantLcok, use it again
In order to speed up the running speed of the code, we adopt a multi-threading method. Parallel execution does make the code more efficient, but the problem that comes with it is that there are many threads running at the same time in the program. If they modify an object at the same time, it is likely to cause corruption. At this time we A synchronization mechanism is needed to manage these threads.
The above is the detailed explanation of java thread (2) - thread synchronization. For more related content, please pay attention to the PHP Chinese website (www.php.cn)!