java进阶:15.3 多线程 - 锁、ReentrantLock

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

与synchronized类似的,lock也能够达到同步的效果。

1. 利用加锁同步

锁是一种实现资源排他使用的机制。

  • 对于实例方法,要给调用该方法的对象加锁。
  • 对于静态方法,要给这个类加锁。
  • 如果一个线程调用一个对象上的同步实例方法(静态方法),首先给该对象(类)加锁,然后执行该方法,最后解锁。
  • 在解锁之前,另一个调用那个对象(类)中方法的线程将被阻塞,直到解锁。

与 synchronized (someObject) 类似的,lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。
与 synchronized 不同的是,一旦synchronized 块结束,就会自动释放对someObject的占用。 lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行。

 

Java 可以 显式地加锁,这给协调线程带来了更多的控制功能。一个锁是一个Lock 接口的实例,它定义了加锁和释放锁的方法。锁也可以使用 newCondition() 方法来创建任意个数的Condition 对象,用来进行线程通信。

ReentrantLock 是Lock 的一个具体实现,用于创建相互排斥的锁。可以创建具有特定的公平策略 的锁。
Lock lock = new ReentrantLock();

公平策略值为真,则确保等待时间最长的线程首先获得锁
取值为假的公平策略将锁给任意一个在等待的线程。被多个线程访问的使用公正锁的程序,其整体性能可能比那些使用默认设置的程序差,但是在获取锁且避免资源缺乏时可以有更小的时间变化。
在这里插入图片描述
使用显式锁方法修改

	private static class Account{
		private static Lock lock = new ReentrantLock(); // creat a lock
		
		private int balance = 0;
	
		public int getBalance() {
			return balance;
		}
	
		public void deposit(int amount) {
			lock.lock();
			
			try {
				int newBalance = balance + amount;
				Thread.sleep(5);
				balance = newBalance;
			}
			catch(InterruptedException ex) {
			}
			finally {
				lock.unlock();  // release the lock
			}	
		}	
	}

在对lock() 的调用之后紧随一个try - catch 块并且在finally 子句中释放这个锁
是一个很好的编程习惯!

通常,使用synchronized 方法或语句比使用相互排斥的显式锁简单些。然而,使用显式锁对同步具有状态的线程更加直观和灵活。
 
 

2. tryLock

synchronized 是不占用到手不罢休的,会一直试图占用下去。
与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个 trylock 方法。
trylock会在指定时间范围内试图占用,占成功了,执行。 如果时间到了,还占用不成功,扭头就走~

注意: 因为使用trylock有可能成功,有可能失败,所以后面unlock释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock,就会抛出异常

     Thread t1 = new Thread() {
            public void run() {
                boolean locked = false;
                try {
                    locked = lock.tryLock(1,TimeUnit.SECONDS);
                    if(locked){
                        Thread.sleep(5000);
                    }
                    else{
                    }
 
                }
                 catch (InterruptedException e) {
                    e.printStackTrace();
                } 
                finally {          
                    if(locked)
                        lock.unlock();
                }
            }
        };
        t1.setName("t1");
        t1.start();

3. 死锁

  1. 线程1 首先占有对象1,接着试图占有对象2
  2. 线程2 首先占有对象2,接着试图占有对象1
  3. 线程1 等待线程2释放对象2
  4. 与此同时,线程2等待线程1释放对象1

使用一种称为 资源排序 的简单技术可以轻易地避免死锁的发生。
给每一个需要锁的对象指定一个顺序,确保每个线程都按这个顺序来获取锁

 
 

4. 线程间交互

锁上的条件可以用于协调线程之间的交互。

通过保证在临界区上多个线程的相互排斥,线程同步完全可以避免竞争条件的发生,但
是有时候,还需要线程之间的相互协作。 可以使用条件实现线程间通信。

一个线程可以指定在某种条件下该做什么。条件是通过调用Lock 对象的newCondition() 方法而创建的对象。一旦创建了条件,就可以 首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法来实现线程之间的相互通信。

await() 方法可以让当前线程进入等待,直到条件发生。
signal()方法唤醒一个等待的线程。
signalAll() 唤醒所有等待的线程。

条件由Lock 对象创建。为了调用它的方法(await() 、signal()和signalAll()),必须首先拥有锁。如果没有获取锁就调用这些方法,会抛出 IllegalMonitorStateException 异常。

接下来举例:给一个银行账户存取款,当要取款时,如果账户余额小于输入的取款金额,则等待账户存钱,直至大于输入金额后,进行取款操作。

package Thread;

import java.util.concurrent.*;
import java.util.concurrent.locks.*;


public class ThreadCooperation {

	private static Account account = new Account();
	
	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(2);
		executorService.execute(new DepositTask());  // 提交提款任务
		executorService.execute(new WithdrawTask()); // 提交存款任务
		executorService.shutdown();
		
		System.out.println("Thread 1\t\t Thread 2\t\t Balance");
	}
	
	public static class DepositTask implements Runnable{
		@Override
		public void run() {
			try {
				while(true) {
					account.deposit((int)(Math.random()*10)+1);
					Thread.sleep(1000);
				}
			}
			catch(InterruptedException ex) {
				ex.printStackTrace();
			}
		}
	}
	
	public static class WithdrawTask implements Runnable{
		@Override
		public void run() {
			while(true) {
				account.withdraw((int)(Math.random()*10)+1);
			}
		}
	}
	
	
	
	private static class Account{
		private static Lock lock = new ReentrantLock(); // 创建一个锁
		
		private static Condition newDeposit = lock.newCondition();  // 锁上的条件
		
		private int balance = 0;
	
		public int getBalance() {
			return balance;
		}
	
		public void withdraw(int amount) {
			lock.lock();   // 获取锁
			try {
				while(balance < amount) {
					System.out.println("\t\t Wait for a deposit"); 
					newDeposit.await();   // 等待newDeposit条件
				}
				
				balance -= amount;
				System.out.println("\t\t\tWithdraw " + amount + "\t\t" + getBalance()); 
			}
			catch(InterruptedException ex) {
				ex.printStackTrace();
			}
			finally {
				lock.unlock();   // 释放锁
			}
		}
		
		
		
		
		public void deposit(int amount) {
			lock.lock();
			
			try {
				balance += amount;
				System.out.println("deposit " + amount + "\t\t\t\t\t" + getBalance());
				newDeposit.signalAll();
			
			}		
			finally {
				lock.unlock();  // release the lock
			}	
		}	
	}
	
}

 
 

5. 线程状态

任务在线程中执行。线程可以是以下5 种状态之一: 新建、就绪、运行、阻塞、结束

新创建一个线程时,它就进入新建状态(New) 。调用线程的startO 方法启动线程后,它进入就绪状态(Ready) 。就绪线程是可运行的,但可能还没有开始运行。操作系统必须为它分配CPU 时间。

就绪线程开始运行时,它就进入运行状态。如果给定的CPU 时间用完或调用线程的 yield()方法,处于运行状态的线程可能就进入就绪状态。
在这里插入图片描述有几种原因可能使线程进人阻塞状态(即非活动状态) 。可能是它自己调用了join() 、sleep() 或 wait() 方法。它可能是在等待I/O 操作的完成。当使得其处于非激活状态的动作不起作用时,阻塞线程可能被重新激活。例如,如果线程处于休眠状态并且休眠时间已过期,线程就会被重新激活并进入就绪状态。

最后,如果一个线程执行完它的run()方法,这个线程就被结束(finished) 。

isAlive() 方法是用来判断线程状态的方法。如果线程处于就绪、阻塞或运行状态,则返回true; 如果线程处于新建并且没有启动的状态,或者已经结束,则返回false

方法interrupt() 按下列方式中断一个线程:当线程当前处于就绪或运行状态时,给它设置一个中断标志;当线程处于阻塞状态时,它将被唤醒并进入就绪状态,同时抛出异常java.lang.lnterruptedException 。
 
 

6. 总结Lock和synchronized的区别

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。

  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。

  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

猜你喜欢

转载自blog.csdn.net/L20902/article/details/89315018