多线程基础篇(3)——初试锁

1. 锁的概念

    锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。 当一个资源被一个线程操作时,会对该资源加上锁,在锁未被释放期间,其他进行操作的线程都会陷入阻塞。

2. 为什么需要锁

    当多个线程对同一个资源进行访问时,就会出现线程安全问题,就比如,你坐在桌子边手上拿着筷子正要去夹取最后一块食物时,突然被旁边的人夹走了食物,这时你就已经无法再进行操作,筷子就相当于CPU时间片,食物就相当于共享资源,所以此时程序就会发生一些无法预料的异常。

    以下列购票程序为例,若ticket=1时,当线程t0执行到切换点时,失去CPU时间片切换到t1,但t1执行到切换点时也失去了CPU时间片,切换到t2线程顺利运行,ticket=0,t2运行完毕后,又切回t0继续运行,ticket=-1,又切到t1线程,ticket=-2,最后的结果会使得ticket数量出现负数,这显然是错误的。虽然这并不一定会发生,但一定有可能发生,所以线程安全也叫线程隐患。

public class Demo6 {
	//线程安全例子,以售票为例
	public static void main(String[] args) {
		sell s=new sell();
		Thread t0=new Thread(s, "线程1");
		Thread t1=new Thread(s, "线程2");
		Thread t2=new Thread(s, "线程3");
		t0.start();
		t1.start();
		t2.start();
	}
}
class sell implements Runnable{
	private int ticket=100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true){
			if(ticket>0){
                //切换点
				System.out.println("当前"+Thread.currentThread().getName()+"以出售1张票,"+"剩余"+(--ticket));
			}else{
				break;
			}
		}
		
	}
	
}

3.锁的实现

    3.1 synchronized

        代码形式为:synchronized(obj){    do work    },上述购票代码则可以改成

public class Demo11 {
	public static void main(String[] args) {
		TicketSell t=new TicketSell();
		Thread t1=new Thread(new Run(t),"线程1");
		Thread t2=new Thread(new Run(t),"线程2");
		Thread t3=new Thread(new Run(t),"线程3");
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketSell{
	private Integer num=100;
	public void sell(){
		num--;
	}
	public int getNum(){
		return num;
	}
}
class Run implements Runnable{
	private TicketSell t;
	//private Object obj;//也可以通过这个对象来获取锁
	public Run(TicketSell t){
		this.t=t;
	}
	@Override
	public void run() {
		while (true) {
			// synchronized (obj)
			synchronized (t) {
				if (t.getNum() > 0) {
					t.sell();
					System.out.println(Thread.currentThread().getName() + "出售一张");
					System.out.println("当前剩余" + t.getNum());
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				} else {
					break;
				}
			}
		}

	}
	
}

    synchronized(obj)中的obj可以为任意对象,但必须是一个对象而不是基本类型数据,obj被称为同步锁,用术语说应该是对象监视器。但是我们可以将共享资源封装为一个对象,然后通过该对象来获取锁,在同步代码块中通过调用方法来访问资源对象。

        3)同步方法:

        只需在方法返回类型前加上synchronized即可,同步方法中的锁时当前实例对象的锁,也就是this。然而对于静态同步方法,锁是当前类的Class对象的锁,若一个线程任务调用此方法则另一个线程不能调用同为该类的其他静态同步方法,静态同步方法的锁只能来自于所在类的Class对象,即只能为静态同步方法或同步代码块synchronized(类名.class){  do work  }。

public class Demo12 {
	public static void main(String[] args) {
		TicketSell1 t=new TicketSell1();
		Thread t1=new Thread(new Run1(t),"线程1");
		Thread t2=new Thread(new Run1(t),"线程2");
		Thread t3=new Thread(new Run1(t),"线程3");
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketSell1{
	private Integer num=100;
	public synchronized void sell(){
		if(num>0){
			num--;
			System.out.println(Thread.currentThread().getName() + "出售一张");
			System.out.println("当前剩余" + num);
			try {
				Thread.sleep(100);// 可以增大线程切换的概率
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
	}
	public synchronized int getNum(){
		return num;
	}
}
class Run1 implements Runnable{
	private TicketSell1 t;
	public Run1(TicketSell1 t){
		this.t=t;
	}
	@Override
	public void run() {
		while (true) {
			t.sell();
		}

	}
	
}

        注意:对于需要加锁的对象,其必须是包装类型不能为基本类型,因为我们需要通过该对象来获取它所持有的锁,且应该设置为private,因为锁无法阻止线程任务直接通过访问域对象来修改值。

    3.2 Lock接口显式锁

    它提供了与synchronized关键字类似的同步功 能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以 及超时获取锁等多种synchronized关键字所不具备的同步特性。

        1)lock锁的使用方式:

public class Demo13 {
	public static void main(String[] args) {
		TicketSell2 t=new TicketSell2();
		Thread t1=new Thread(new Run2(t),"线程1");
		Thread t2=new Thread(new Run2(t),"线程2");
		Thread t3=new Thread(new Run2(t),"线程3");
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketSell2{
	private Integer num=100;
	public void sell(){
			num--;
	}
	public int getNum(){
		return num;
	}
}
class Run2 implements Runnable{
	private TicketSell2 t;
	private Lock lock=new ReentrantLock();
	private Condition con1=lock.newCondition();
	public Run2(TicketSell2 t){
		this.t=t;
	}
	@Override
	public void run() {
		while (true) {
			lock.lock();
			try {
				while (t.getNum()<=0) {
					con1.await();// 调用此方法阻塞当前线程,并且释放锁,便可以让另一个线程来对票数进行补充
				}
				t.sell();
				System.out.println(Thread.currentThread().getName() + "出售一张");
				System.out.println("当前剩余" + t.getNum());
				Thread.sleep(100);// 可以增大线程切换的概率
				con1.signalAll();//若票数充足则唤醒所有的被阻塞线程
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}

	}
	
}

       在finally中释放锁是为了保证在获取锁以后能够释放锁,也不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放,必须保证return语句发生在try子句中,确保unlock()不会过早发生,将数据暴露给第二个任务。

        2)Lock提供了一些synchronized所不具备的特性:

多线程基础篇(3)——初试锁

        3)相关API:

多线程基础篇(3)——初试锁

        4)条件对象:

Lock lock = new ReentrantLock();
	Condition condition = lock.newCondition();

	public void conditionWait() throws InterruptedException {
		lock.lock();
		try {
			condition.await();
		} finally {
			lock.unlock();
		}
	}

	public void conditionSignal() throws InterruptedException {
		lock.lock();
		try {
			condition.signal();
		} finally {
			lock.unlock();
		}
	}

     条件对象通过Lock对象的newCondition()方法获得,当线程A中的条件对象调用await()方法时,他会进入该条件的等待集,即使其他线程已经释放了锁,他仍然会处于阻塞状态,直到某个线程使用同一个条件对象进行了signalAll()操作(signal()方法也可以,但是这个方法是随机解除等待集中的某一个线程的阻塞),使得线程A脱离阻塞状态,但并不一定会立即运行,只有他再次获得锁之后才能继续从上次运行的地点继续运行。但这也可能会带来一个问题,那就是死锁,因为线程A依赖于其他线程来唤醒,如果没有线程来进行唤醒就会造成死锁。Condition的详细API

多线程基础篇(3)——初试锁

    3.3 线程本地存储

        1)概念:线程本地存储是一种自动化机制,它的原理是通过根除对变量的共享,为使用相同变量的每个不同线程都创建不同的存储。例如,有5个线程都要使用变量x所代表的对象,那么本地存储会生成5个用于x的不同存储块。ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。通过这些值我们可以看做线程的一种状态表现,他是每个线程所独享的,不受其他线程影响。

public class ThreadLocalHolder {
	private static final ThreadLocal<Long> time = new ThreadLocal<Long>() {
		protected Long initialValue() {
			return System.currentTimeMillis();
		}
	};
	public static final void begin() {
		time.set(System.currentTimeMillis());
	}
	public static final long end() {
		return System.currentTimeMillis() - time.get();
	}
	public static void main(String[] args) {
		ThreadRunTime t1=new ThreadRunTime();
		ThreadRunTime t2=new ThreadRunTime();
		t1.start();
		t1.start();
	}
}
class ThreadRunTime extends Thread{
	@Override
	public void run() {
		try {
			ThreadLocalHolder.begin();
			Thread.sleep(1000);
			Thread.yield();
			System.out.println(getName()+ThreadLocalHolder.end());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}

        那么如果不使用线程变量会如何?如果不使用线程变量也就是直接将time变量的类型设为Long,如果在线程休眠期间或者线程切使t1线程切换到了t2线程运行,t2运行完毕再返回t1线程时,调用ThreadLocalHolder.end()所获取的就是线程t2的运行时间。因此,从另一种角度来说通过线程变量ThreadLocal也避免了对共享资源的竞争,但是这种方法却无法实现同步,所以我们可以将ThreadLocal中所包含的对象视作线程的状态。

    3.4 死锁,活锁,饥饿

    理解死锁与活锁只需抓住两点:

    1)死锁是两个线程都持有对方锁所需要的锁且永不释放,都等待着对方释放锁,也就是互不让步。比如线程1已经持有A锁,需要B锁才能继续运行,而线程2持有B锁,需要A锁才能继续运行,然而线程1不会释放A锁,线程2不会释放B锁,导致线程陷入无限的等待,就会导致死锁。避免死锁的方法:

  • 避免一个线程同时获取多个锁。 
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。 
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

    2)活锁是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源,也就是互相让步。

    3)饥饿是指线程的CPU时间片被其他线程完全抢占了所有CPU时间片而导致的无法运行,原因有

  • 高优先级线程吞噬所有的低优先级线程的CPU时间。
  • 线程被永久堵塞在一个等待进入同步块的状态。
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)。

    3.5 总结

        Java中的锁可以避免多线程对同一资源竞争所引起的线程安全问题,并实现同步。必须记住, Java中的锁都是来自于对象,synchronized同步代码块必须给定一个需要进行同步的对象,也就是共享资源对象,而synchronized同步方法实际上相当于synchronized(this)形式的同步代码块,Lock显示锁也类似于synchronized同步代码块,不过Lock显示锁直接通过自身来获取锁,并且比synchronized多了一些特性。

上一篇:《React Native移动开发实战》一一2.3 React Native的JSX解决方案


下一篇:《BREW进阶与精通——3G移动增值业务的运营、定制与开发》连载之56---BREW SDK 个版本的区别(下)