Java多线程2:同步机制—锁,ReentrantLock和synchronized异同,踩坑synchronized出现的问题及原因分析:代码块只有一个线程执行,synchronized失效等

在多线程中使用synchronized代码块时,可能会出现以下问题,如代码块只有一个线程执行,synchronized失效等,重复执行等。

例子

买票,见如下,假设有10张票,三个线程售票,运行该程序可看到结果中第10张票被售出三次,这显然是不符合逻辑的。
这是因为在线程2出售第10张票时,线程3读取的票也是第10张,他们读取了同一张票。而我们想要是当一个窗口售出这张票(资源)后,其他线程不能再出售该票。

public class SynchronizedTest implements Runnable{
	private int tickets=10;
//	买票例子
	public void run() {
		while(tickets>0) {
			System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
			tickets=tickets-1;
		}
	}
	public static void main(String[] args) {
		SynchronizedTest s = new SynchronizedTest();
		new Thread(s,"线程1").start();
		new Thread(s,"线程2").start();
		new Thread(s,"线程3").start();
	}
}

结果:
image.png

加上synchronized代码块出现的问题及原因分析

  • 加上synchronized只有一个线程执行,其他不执行,代码如下:
public class SynchronizedTest implements Runnable{
	private Integer tickets=1000;
	private String lockFlag="aaa";
//	买票例子
	public void run() {	
		synchronized (lockFlag) {
			while(tickets>0) {		
				System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
				tickets=tickets-1;
				try {
					Thread.sleep(100);//放大效果
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}				
			}	
		}
	}
	public static void main(String[] args) {
		SynchronizedTest s = new SynchronizedTest();
		new Thread(s,"线程1").start();
		new Thread(s,"线程2").start();
		new Thread(s,"线程3").start();
	}
}

结果如下:
image.png

可以看到在程序中只有一个线程执行了,其他的线程并没有执行,原因主要在与锁放在while的上方,导致循环一直进行,锁在线程执行完成后才会释放锁,而其他线程却无法获得到锁资源,导致其他线程没有参与售票。
可能有人会这么改,那我把synchronized下移,那么就会在每售出一张票后就释放锁,代码如下:

public class SynchronizedTest implements Runnable{
	private Integer tickets=100;
	private String lockFlag="aaa";
//	买票例子
	public void run() {	
			while(tickets>0) {
				synchronized (lockFlag) {		
					System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
					tickets=tickets-1;
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO 自动生成的 catch 块
						e.printStackTrace();
					}				
				}	
			}
		}
	public static void main(String[] args) {
		SynchronizedTest s = new SynchronizedTest();
		new Thread(s,"线程1").start();
		new Thread(s,"线程2").start();
		new Thread(s,"线程3").start();
	}
}

结果会真的是我们想要的吗?显然不会!结果如下:
image.png
我们虽然解决了只有一个线程运行的问题,却又出现了另一个问题,那就是出现了第0张票和第-1张票.你如果启动了4个线程还可能会出现第-2张票。出现此现象的原因在与判断语句并没有处于synchronized代码块中,假设在第1张票初,三个线程均判断票数,都得到了1>0,第一个线程进入synchronized代码块,其他线程等待,第一个线程结束,其他线程依次进入代码块,但此时实际票数已经为0而其他线程并不知道,仍会执行。
那么我们继续修改,按照上面的分析,我们知道在判断语句上出了问题,应该在synchronized代码块中进行判断,而循环在synchronized代码块外。
修改后代码如下:

public void run() {	
			while(true) {
				synchronized (lockFlag) {
					if(tickets>0) {
						System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
						tickets=tickets-1;
						try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}				
					}
				}
			}
		}

此时的输出是符合逻辑的,也是我们想要的,即票数正确,多个线程同时售票。
image.png

  • 锁失效的问题
    解决了上述问题,可能有的会出现以下问题,他们认为我既然修改的是票,那我将票对象加锁行不行,synchronized(对象)中对象使用的是如下方式:
synchronized (tickets)

运行结果如下:
image.png
不仅票名不连续,我设置了10张票,实际却售出了11张
原因在于我们使用票数对象作为synchronized参数,却忽略了tickets是变化的,当tickets变化后,我们可能会出现几个线程中使用的锁并不是同一个锁,所以synchronized ()应使用一个不变的对象。

  • synchronized关键字可以加在方法上,作用与上述类似

更新:

显式锁,可重入锁ReentrantLock()

其实现了Lock接口(实现该接口的锁均为悲观锁),在代码中可显式开启锁和释放锁,使用示例如下,下方存在冗余代码,一般使用是将其放在try{}finnal{释放锁}中。

public class LockTest implements Runnable {
	public static void main(String[] args) {
		LockTest lockTest=new LockTest();
		new Thread(lockTest,"线程1").start();
		new Thread(lockTest,"线程2").start();
		new Thread(lockTest,"线程3").start();
	}
	private Integer tickets=10;
	private final ReentrantLock lock=new ReentrantLock();
//	买票例子
	public void run() {	
		while(true) {
			lock.lock();//加锁
			if(tickets>0) {		
				System.out.println(Thread.currentThread().getName()+"售出第"+tickets+"张票售出");
				tickets=tickets-1;
				try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
				lock.unlock();//释放锁				
			}else {
				lock.unlock();//释放锁	
				break;
			}			
		}
	}
}

synchronized和ReentrantLock异同

  • 一个时关键字层面,为JVM内的,一个为API层面。
  • synchronized不可中断,除非出现异常和运行完毕,ReentrantLock可以被中断。
  • synchronized为非公平锁,ReentrantLock可通过构造函数参数(boolean类型,默认fales非公平)调整公平性
  • 两者都是悲观锁。前置知识:乐观锁认为在自己使用时某一数据时没有其他线程来修该数据,在使用数据时之只会判断该数据有没有没修改;悲观锁认为在自己使用时某一数据时会有其他线程来修该数据。因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  • synchronized锁的时对象,ReentrantLock锁的是线程。
  • 待续。。。
  • 关于锁的详细理解见于 https://tech.meituan.com/2018/11/15/java-lock.html