Java线程教程(三)之线程同步synchronized和lock

目录

 

一、为什么要线程同步?

二、为什么有线程安全问题?

三、线程安全解决办法

3.1 同步代码块

3.2 同步方法 (函数)

3.3 Lock锁机制

四、最后总结


一、为什么要线程同步?

因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,现实生活中,银行取钱问题、火车票多个窗口售票问题等,通常会涉及并发问题,从而需要用到多线程技术。当进程中有多个并发线程进入一个重要数据的代码块时,在修改数据的过程中,很有可能引发线程安全问题,从而造成数据异常。例如,正常逻辑下,同一个编号的火车票只能售出一次,却由于线程安全问题而被多次售出,从而引起实际业务异常。

二、为什么有线程安全问题?

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。

案例:需求现在有20张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果。

代码:

class SellTickets01 implements Runnable {

	int tickets = 20;

	@Override
	public void run() {
		// 同步代码块
		while (tickets > 0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			sale();
		}
	}
	
	private void sale(){
		if(tickets > 0){
			System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
			tickets--;
		}else {
			System.out.println(Thread.currentThread().getName() + " -->售票结束!");
		}
	}
}

/**
 * 需求现在有30张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果
 */
public class ThreadSync01{
	
	public static void main(String[] args) {
		SellTickets01 tickets = new SellTickets01();
		Thread t1 = new Thread(tickets, "1号窗口");
		Thread t2 = new Thread(tickets, "2号窗口");
		Thread t3 = new Thread(tickets, "3号窗口");
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

我们会发现3个窗口同时出现重复票数,

结论发现,多个线程共享同一个全局成员变量时,做写的操作可能会发生数据冲突问题。

三、线程安全解决办法

问:如何解决多线程之间线程安全问题?

答:使用多线程之间同步synchronized或使用锁(lock)。

问:为什么使用线程同步或使用锁能解决线程安全问题呢?

答:将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。

问:什么是多线程之间同步?

答:当多个线程共享同一个资源,不会受到其他线程的干扰。

3.1 同步代码块

什么是同步代码块?

答:就是将可能会发生线程安全问题的代码,给包括起来。

synchronized(同一个数据){

 可能会发生线程冲突问题

}

就是同步代码块 。同步代码块会被JVM自动加上内置锁,从而实现同步。

对象如同锁,持有锁的线程可以在同步中执行,没持有锁的线程即使获取CPU的执行权,也进不去 

使用synchronized(线程同步)必须有一些条件:
1.必须要有两个或者两个以上的线程需要发生同步。

2.多个线程想同步,必须使用同一把锁

3.保证只有一个线程进行执行

synchronized原理:
1.首先有一个线程已经拿到了锁,其他线程已经有cup执行权,一直排队,等待释放锁。
2.锁是在什么时候释放?代码执行完毕或者程序抛出异常都会被释放掉。
3.锁已经被释放掉的话,其他线程开始进行抢锁(资源竞争),谁抢到谁进入同步中去,其他线程继续等待。

好处:解决了多线程的安全问题 

弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。 

synchronized 只能有一个线程进行执行(就像厕所只有一个坑,好多人等着上厕所,谁进入厕所谁上锁,其他人在外等待),这个线程不释放锁的话,其他线程就一直等,就会产生死锁问题。

代码:

在介绍之前我们先说一下临界区,在Java中代码段访问了同一个对象或数据,那么这个代码段就叫做临界区。上面程序中run()方法中循环里面的代码就叫做临界区。我们需要对这个临界区进行保护,这样就用到了线程的同步。

class SellTickets2 implements Runnable {

	int tickets = 20;

	@Override
	public void run() {
		// 同步代码块
		while (tickets > 0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			sale();
		}
	}
	//如果将当前方法声明为static,那么能替换this锁的方式就是 SyncThread.class
	private void sale(){
		//synchronized使用的是this对象锁,如果某个线程获得了锁,那么其他线程只可以执行当前对象的非临界区(非加锁程序块)
        //获得锁的线程在进入下一个加锁临界区(加锁块)则不用再次获取锁,这就是锁的可重如性。
        //JVM负责跟踪对象被加锁的次数,在任务第一次给对象加锁的时候,计数变成1,每当这个相同的任务在这个对象上获得锁时,计数都会递增(只有首先获得锁的任务才能继续获取锁)
        //每当任务离开一个synchronized的方法,计数递减,当计数为0时,锁被完全释放,此时别的任务线程就可以使用此资源。
		synchronized (this) {
			if(tickets > 0){
				System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
				tickets--;
			}else {
				System.out.println(Thread.currentThread().getName() + " -->售票结束!");
			}
		}
	}
}

/**
 * 需求现在有20张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果
 */
public class ThreadSync02{
	
	public static void main(String[] args) {
		SellTickets2 tickets = new SellTickets2();
		Thread t1 = new Thread(tickets, "1号窗口");
		Thread t2 = new Thread(tickets, "2号窗口");
		Thread t3 = new Thread(tickets, "3号窗口");
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

上面使用synchronized关键字的时候需要一个对象,这里的对象可以是任意对象,我们在这里选择的是this对象,你也可以选择String、Object类的对象。每一个对象都有一个监视器或者叫锁。当我将obj作为参数传给synchronized关键字的时候,就相当于给这段代码加了一个锁,当我执行到这段代码的时候,先判断是否加锁,如果没有加锁,先将其加锁然后执行代码。如果加锁了只能等待。等到给代码加锁的线程执行完成之后,会将锁打开。


3.2 同步方法 (函数)

即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

什么是同步函数?
答:在方法上加上synchronized进行修饰 称为同步函数。

同步函数使用的是什么锁?答:this锁

代码:

class SellTickets3 implements Runnable {

	int tickets = 20;

	@Override
	public void run() {
		// 同步代码块
		while (tickets > 0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			sale();
		}
	}
	
	private synchronized void sale(){
		if(tickets > 0){
			System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
			tickets--;
		}else {
			System.out.println(Thread.currentThread().getName() + " -->售票结束!");
		}
	}
}

/**
 * 需求现在有20张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果
 */
public class ThreadSync03{
	
	public static void main(String[] args) {
		SellTickets3 tickets = new SellTickets3();
		Thread t1 = new Thread(tickets, "1号窗口");
		Thread t2 = new Thread(tickets, "2号窗口");
		Thread t3 = new Thread(tickets, "3号窗口");
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

3.3 Lock锁机制

通过创建Lock对象,采用lock()加锁,采用unlock()解锁,来保护指定代码块。  在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock类的常用方法有:
         ReentrantLock() : 创建一个ReentrantLock实例 
         lock() : 获得锁 
         unlock() : 释放锁 

 注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 
代码:

class SellTickets implements Runnable {

	int tickets = 20;

	private final ReentrantLock lock = new ReentrantLock(true);
	@Override
	public void run() {
		// 同步代码块
		while (tickets > 0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			sale();
		}
	}
	
	private void sale(){
		lock.lock();//获取锁
		try {
			if(tickets > 0){
				System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
				tickets--;
			}else {
				System.out.println(Thread.currentThread().getName() + " -->售票结束!");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			 lock.unlock();//释放锁
		}
	}
}

public class ThreadLock {
	public static void main(String[] args) {
		SellTickets tickets = new SellTickets();
		Thread t1 = new Thread(tickets, "1号窗口");
		Thread t2 = new Thread(tickets, "2号窗口");
		Thread t3 = new Thread(tickets, "3号窗口");
		t1.start();
		t2.start();
		t3.start();
	}
}

运行结果:

四、最后总结

  由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。

  另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。

补充:

在使用synchronized 代码块时,可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。其中,wait()方法会释放占有的对象锁,当前线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序;线程的sleep()方法则表示,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码。
wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会释放对象锁。notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM会在等待的线程中调度一个线程去获得对象锁,执行代码。

需要注意的是,wait()和notify()必须在synchronized代码块中调用。

notifyAll()是唤醒所有等待的线程。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值