高并发synchronized深入详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chenbetter1996/article/details/89047867

1. synchronized概述

  synchronized是Java的一个关键字,修饰符。是Java多线程加锁机制的一种,一种隐式内置锁/监听器锁(对比显式Lock锁)。它也是一种互斥锁,保证了被修饰的块每次只能有一条线程访问。

1.1 细分

  • 对象锁:synchronize修饰的是实例方法、synchronized语句块参数使用的是实例对象
  • 类锁: synchronized修饰的是类方法、synchronized语句块参数是类类型

在进入方法或者语句块的时候获取到相关的锁,方法结束或者语句块结束自动释放锁。不同Lock的手动操作。

1.2 用处

  • 原子性: 保证线程原子性,如上所述,修饰的块或者方法每次只能有一条线程访问。
  • 可见性: 保证了修饰块方法修改的变量对其他线程是可见的。

1.3 案例

1.3.1 多线程问题
package xyz.cglzwz.thread_concurrency.other._synchronized;

/**
 * count++三步,并发条件不同步会有问题
 * synchronized(new Object())也是不行的,锁不一致
 * 
 * @author chgl16
 * @date 2019-04-05
 */
public class ConcurrentProblem {
	static int count = 0;
	
	static Object o = new Object();
	
	public static void main(String[] args) throws InterruptedException {
		Runnable r = () -> {
			for (int i = 0; i < 10000; ++i) {
				count++;
			}	
			
		};
		Thread t1 = new Thread(r, "t1");
		Thread t2 = new Thread(r, "t2");
		
		t1.start();
		t2.start();
		
		t1.join();
		t2.join();
		
		System.out.println(count);
	}
}

输出(每次都会不一样)
12223

如果不并发结果显然就是20000,但是在这里多线程的情况下,输出一般都是10000-15000左右。因为count++ 实际上是三条操作

  1. 先从内存(方法区)读取count到当前线程(栈帧)
  2. 当前线程值的count+1
  3. 写回内存

因此会出现很多情况:t1获取到count=5的时候(第一步),t2也获取到count=5(第一步),然后大家都写入了,写完后内存中的count是6,而不是7,因此少了1。

如果如下加锁,那就是保证了结果必为20000

Object o = new Object();

Runnable r = () -> {
	for (int i = 0; i < 10000; ++i) {
		synchronized (o) {
			count++;
		}	
	}			
};

这里使用的是对象锁,每次只能是t1、t2其中的一个持有这个锁,保证了count++细分的三步是原子性的。
如果如下加锁,结果有也不会是20000,但是比不加的大

Runnable r = () -> {
	for (int i = 0; i < 10000; ++i) {
		synchronized (new Object()) {
			count++;
		}	
	}			
};

因为每次都会创建一把新锁,所以其实t1和t2访问count++语句块是不会互斥的。

1.3.2 死锁问题
package xyz.cglzwz.thread_concurrency.other._synchronized;

/**
 * 死锁
 * @author chgl16
 * @date 2019-04-05
 */

public class Deadlock {

	private static Object resourceA = new Object();
	private static Object resourceB = new Object();
	
	public static void main(String[] args) {
		Thread t1 = new Thread("t1") {
			@Override 
			public void run() {
				synchronized(resourceA) {
					System.out.println(Thread.currentThread().getName() + " 获取到了 resourceA");
					try {
						// 休眠一秒,以保证线程B获取到了resourceB
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
					synchronized(resourceB) {
						System.out.println(Thread.currentThread().getName() + " 获取到了 resourceB");
					}
				}
			}
		};
		
		Thread t2 = new Thread("t2") {
			@Override 
			public void run() {
				synchronized(resourceB) {
					System.out.println(Thread.currentThread().getName() + " 获取到了 resourceB");
					try {
						// 休眠一秒,以保证线程A获取到了resourceA
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
					synchronized(resourceA) {
						System.out.println(Thread.currentThread().getName() + " 获取到了 resourceA");
					}
				}
			}
		};

		t1.start();
		t2.start();
	}
}

输出

t1 获取到了 resourceA
t2 获取到了 resourceB
  1. 这里的t1和t2在持有自己的资源的情况下(语句块未结束,未释放锁,这里把锁当做资源),而又相互请求对方的资源,明显死锁。两个都获取不到。
  2. 解决方法就是打破死锁的必要条件,这里可以把每个run方法的synchronized语句块嵌套改为并列,这样大家都分别释放了资源再请求新资源就不会互斥等待了。

2. synchronized原理


3. 常见问题

  1. 两个线程同时访问一个对象的同步方法
package xyz.cglzwz.thread_concurrency.other._synchronized;

public class SynchronizedDemo1 implements Runnable {
   static SynchronizedDemo1 instance = new SynchronizedDemo1();
   
   @Override
   public void run() {
   	method1();
   }
   
   public synchronized void method1()  {
   	System.out.println(Thread.currentThread().getName() + "开始访问");
   	try {
   		// 休眠下,更好看结果
   		Thread.sleep(1000);
   	} catch (InterruptedException e) {
   	}
   	System.out.println(Thread.currentThread().getName() + "结束访问");
   }
   
   
   public static void main(String[] args) {
   	Thread t1 = new Thread(instance, "t1");
   	Thread t2 = new Thread(instance, "t2");
   	
   	t1.start();
   	t2.start();

   }
}

结果串行,因为是同一个对象锁

t1开始访问
t1结束访问
t2开始访问
t2结束访问
  1. 两个线程访问的是两个对象的同步方法
    结果是并行的,因为是两个不同的对象锁
  2. 两个线程访问的是synchronized的静态方法
    结果是串行的,因为是同一把类锁
  3. 同时访问同步方法与非同步方法
package xyz.cglzwz.thread_concurrency.other._synchronized;

public class SynchronizedDemo4 implements Runnable {
	static SynchronizedDemo4 instance = new SynchronizedDemo4();
	
	@Override
	public void run() {
		if (Thread.currentThread().getName().equals("t1"))
			method1();
		else 
			method2();
	}
	
	public synchronized void method1()  {
		System.out.println(Thread.currentThread().getName() + "开始访问1");
		try {
			// 休眠下,更好看结果
			Thread.sleep(1000);
		} catch (InterruptedException e) {
		}
		System.out.println(Thread.currentThread().getName() + "结束访问1");
	}
	
	public void method2()  {
		System.out.println(Thread.currentThread().getName() + "开始访问2");
		try {
			// 休眠下,更好看结果
			Thread.sleep(1000);
		} catch (InterruptedException e) {
		}
		System.out.println(Thread.currentThread().getName() + "结束访问2");
	}
	
	
	public static void main(String[] args) {
		Thread t1 = new Thread(instance, "t1");
		Thread t2 = new Thread(instance, "t2");
		
		t1.start();
		t2.start();
	}
}
t1开始访问1
t2开始访问2
t1结束访问1
t2结束访问2

结果是并行的,一个同步只是作用在自己范围内
5. 访问同一个对象的不同的普通同步方法
因为是通一个对象锁,所以是串行的
6. 同时访问静态synchronized和非静态synchronized方法:
因为是一个是类锁,一个是实例锁,所以是并行的。
7. 方法抛出异常后,会释放锁

3.1 核心思想

3个核心思想;

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
  2. 每个实例都对应有自己的一把锁,不同实例之前互不影响;
    例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应2、3、4、6种情况)
  3. 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)

猜你喜欢

转载自blog.csdn.net/chenbetter1996/article/details/89047867