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

简介: 参考资料《Java并发编程的艺术》《Java编程思想》《Java核心技术》

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)相关API:

        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 线程本地存储

        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多了一些特性。

相关文章
|
12天前
|
安全 Java 编译器
线程安全问题和锁
本文详细介绍了线程的状态及其转换,包括新建、就绪、等待、超时等待、阻塞和终止状态,并通过示例说明了各状态的特点。接着,文章深入探讨了线程安全问题,分析了多线程环境下变量修改引发的数据异常,并通过使用 `synchronized` 关键字和 `volatile` 解决内存可见性问题。最后,文章讲解了锁的概念,包括同步代码块、同步方法以及 `Lock` 接口,并讨论了死锁现象及其产生的原因与解决方案。
42 10
线程安全问题和锁
|
1月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
72 0
|
7天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
25天前
|
数据采集 存储 安全
如何确保Python Queue的线程和进程安全性:使用锁的技巧
本文探讨了在Python爬虫技术中使用锁来保障Queue(队列)的线程和进程安全性。通过分析`queue.Queue`及`multiprocessing.Queue`的基本线程与进程安全特性,文章指出在特定场景下使用锁的重要性。文中还提供了一个综合示例,该示例利用亿牛云爬虫代理服务、多线程技术和锁机制,实现了高效且安全的网页数据采集流程。示例涵盖了代理IP、User-Agent和Cookie的设置,以及如何使用BeautifulSoup解析HTML内容并将其保存为文档。通过这种方式,不仅提高了数据采集效率,还有效避免了并发环境下的数据竞争问题。
如何确保Python Queue的线程和进程安全性:使用锁的技巧
|
1月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
25天前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
Java多线程教程:使用ReentrantLock实现高级锁功能
23 1
|
1月前
|
存储 安全 容器
【多线程面试题二十一】、 分段锁是怎么实现的?
这篇文章解释了分段锁的概念和实现方式,通过将数据分成多个段并在每段数据上使用独立锁,从而降低锁竞争,提高并发访问效率,举例说明了`ConcurrentHashMap`如何使用分段锁技术来实现高并发和线程安全。
【多线程面试题二十一】、 分段锁是怎么实现的?
|
1月前
|
安全 Java
【多线程面试题十九】、 公平锁与非公平锁是怎么实现的?
这篇文章解释了Java中`ReentrantLock`的公平锁和非公平锁的实现原理,其中公平锁通过检查等待队列严格按顺序获取锁,而非公平锁允许新线程有更高机会立即获取锁,两者都依赖于`AbstractQueuedSynchronizer`(AQS)和`volatile`关键字以及CAS技术来确保线程安全和锁的正确同步。
【多线程面试题十九】、 公平锁与非公平锁是怎么实现的?
|
18天前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
10 0
|
24天前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器