ReentrantLock是怎么保证线程安全中的可见性的?
众所周知,可重入锁ReentrantLock可以用来实现线程安全(可见性,有序性,原子性),其中有序性和原子性都能理解,那么可见性是如何保证的呢?
1.锁中更改volatile变量
class MyTest{
static class MyClass{
public volatile int a = 0;
public int b = 0;
}
public static void main(String[] args) throws InterruptedException {
MyClass clazz = new MyClass();
ReentrantLock lock = new ReentrantLock();
new Thread(()->{
try{
lock.lock();
Thread.sleep(300);
clazz.a++;
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
while(clazz.a == 0){
}
System.out.println("A结束");
while(clazz.b==0){
}
System.out.println("b结束");
}
//运行结果:
//A结束
}
从结果可以看出来,在子线程中修改了MyClass类中的a变量,Main线程中是可见的,而变量b没有被修改,所以最后会一直循环,程序不会结束。
2.锁中修改非volatile变量
如果将上面代码中的变量a改为非volatile,那么结果是什么呢?
// 运行结果:
//
久久等不来想要的输出,Main线程还惦记着它的工作内存里的变量a呢。说明可重入锁里修改了变量其他线程无法立马知道。那么让Main线程先sleep一会呢?
class MyTest{
static class MyClass{
public int a = 0;
public int b = 0;
}
public static void main(String[] args) throws InterruptedException {
MyClass clazz = new MyClass();
ReentrantLock lock = new ReentrantLock();
new Thread(()->{
try{
lock.lock();
Thread.sleep(1000);
clazz.a++;
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
System.out.println(clazz.a);
while(clazz.a == 0){
}
System.out.println("A结束");
while(clazz.b==0){
}
System.out.println("b结束");
}
//运行结果:
//1
//A结束
}
说明可重入锁修改的变量,在Mian线程里还是可以发现的,只是**不知道什么原因没法立即发现。**猜测是Main线程不会重新读取JMM中主内存的变量,而如果使用sleep方法则会重新读取。
3.多个可重入锁间变量修改了可见吗
虽然上面的结论有点出乎我的意料,不过上面的代码其实并不常用。我们使用可重入锁的时候大多是互斥的访问临界区,而之前的经验中这样的代码似乎没有出过可见性的问题。
class MyTest{
static class MyClass{
public int a = 0;
public int b = 0;
}
public static void main(String[] args) throws InterruptedException {
MyClass clazz = new MyClass();
ReentrantLock lock = new ReentrantLock();
new Thread(()->{
try{
lock.lock();
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
try{
lock.lock();
System.out.println(clazz.a);
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
try{
lock.lock();
System.out.println(clazz.a);
}finally {
lock.unlock();
}
}).start();
}
//运行结果:
//1
//1
}
这里的结果到是符合想象,一个加锁线程里共享变量被修改了,其余加锁线程是可以知道的。
4.线程池里的尝试
上面的线程数太少了可能不正确,这里尝试一下线程池进行大量的计算,看看结果还是正确的吗?
class MyTest{
static class MyClass{
public int a = 0;
public int b = 0;
}
public static void main(String[] args) throws InterruptedException {
MyClass clazz = new MyClass();
ReentrantLock lock = new ReentrantLock();
ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 100, 10000, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>());
for (int i = 0; i < 10000; i++) {
pool.submit(() -> {
try {
lock.lock();
clazz.a++;
}finally {
lock.unlock();
}
});
}
pool.shutdown();
Thread.sleep(10000);
System.out.println(clazz.a);
}
//运行结果:
//10000
}
很明显,通过可重入锁加锁的变量被修改后,在这些加了可重入的线程里是可见的。而对于其他没=有加可重入锁的线程,如Main线程,就不一定可见了。
5.最后
还是挺好奇为什么Mian线程里没法保证可见性,希望有大佬赐教。