Java多线程下(线程的同步(synchronized&lock用法以及区别)、死锁问题的引入以及避免以及线程的通信(生产者、消费者问题))

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/zxdspaopao/article/details/101429306

上一篇文章:Java多线程上(线程的四种创建方式以及比较、线程的调度和线程的生命周期)

那么继续说说线程的同步吧。

首先说一个生活中处处可见的案例。

模拟火车站售票程序,开启三个窗口售票。

public class Ticket implements Runnable{	
	private int tick = 100;
	public void run() {
		while(true){
			if(tick > 0){
				try {
					Thread.sleep(10);  //睡眠10毫秒
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()
+"售出车票,tick号为:"+tick--);
			}else{
				break;
			}
		}
	
	}

}

接下来创建三个线程并启动。

public class TicketTest {
	public static void main(String[] args) {
		Ticket t = new Ticket();

		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		t1.setName("t1窗口");
		t2.setName("t2窗口");
		t3.setName("t3窗口");
		t1.start();
		t2.start();
		t3.start();
	}
}

多次运行会出现这么个一个结果:

What?出现了0和-1,这是什么情况,这就是一种极端状态,当t3进入run方法的同时,t2,t1也进入了run方法,那怎么办?只能执行tick—操作,这就是线程不同步造成的,拿图来说明一下。

理想状态下的卖票:

极端情况下的卖票:

1. 多线程出现了安全问题

2. 问题的原因:

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

3. 解决办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

 

Java的同步机制

Synchronized关键字

  1. 同步代码块:
    synchronized (对象){
          // 需要被同步的代码;
    }
  1. synchronized还可以放在方法声明中,表示整个方法为同步方法。

例如:   

public synchronized void show (String name){
            ….
   }

接下来对前面的案例进行同步操作:
 

ublic class Ticket implements Runnable{	
	private int tick = 100;
	public void run() {
		synchronized(Ticket.class){
			while(true){
				if(tick > 0){
					try {
						Thread.sleep(10);  //睡眠10毫秒
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()
+"售出车票,tick号为:"+tick--);
				}else{
					break;
				}
			}		
		}
	
	}

}

测试:

可以看到修改过后的代码售票正常。

分析同步原理:

用synchronized关键字所包的代码块成为同步代码块,线程运行的时候有一个线程进入同步代码块后,自己就会将门关注,不让其他的线程进入,直到进入的线程执行完方法出去后才将门打开,所以就不会出现案例所出现的问题了。

说说用synchronized关键字的细节部分吧。

同步锁机制:

在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

synchronized的锁是什么?

  • 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
  • 同步方法的锁:静态方法(类名.class)、非静态方法(this)
  • 同步代码块:自己指定,很多时候也是指定为this或类名.class

注意:

  • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
  • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

 

同步的范围:

1、如何找问题,即代码是否存在线程安全?

(1)明确哪些代码是多线程运行的代码

(2)明确多个线程是否有共享数据

(3)明确多线程运行代码中是否有多条语句操作共享数据

2、如何解决呢?

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

即所有操作共享数据的这些语句都要放在同步范围中。

 

既然叫同步锁,肯定有释放锁和不会释放锁的操作了。

释放锁的操作:

  •  当前线程的同步方法、同步代码块执行结束。
  •  当前线程在同步代码块、同步方法中遇到breakreturn终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的ErrorException,导致异常结束。
  •  当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作:

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。

 

Lock()

  • JDK 5.0开始Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

实例:

public class Ticket implements Runnable{	
	private final ReentrantLock lock = new ReentrantLock(); //创建lock对象
	private int tick = 100;
	public void run() {
		lock.lock();   //加锁
		try {
			while(true){
				if(tick > 0){
					try {
						Thread.sleep(10);  //睡眠10毫秒
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()
+"售出车票,tick号为:"+tick--);
				}else{
					break;
				}
			}
		}finally{
			lock.unlock();   //解锁
		}			
	}

}

synchronized Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。

说完线程同步了,说说死锁吧。

死锁

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

模拟一个死锁

class A{
	public synchronized void foo(B b){
		System.out.println("当前线程名:"+Thread.currentThread().getName()+
				" 进入了A实例的foo()方法");//1
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("当前线程名:"+Thread.currentThread().getName()+
				" 企图调用B实例的last()方法");//3
		b.last();
	}
	public synchronized void last(){
		System.out.println("进入了A类的last()方法内部");
	}
}

class B{
	public synchronized void bar(A a){
		System.out.println("当前线程名: " + Thread.currentThread().getName() + 
				"进入了B实例的bar()方法" );//2
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("当前线程名:" + Thread.currentThread().getName()+ 
				" 企图调用A实例的last()方法");//4
		a.last();
	}
	
	public synchronized void last(){
		System.out.println("进入了B类的last()方法内部");
	}
}


public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();
	
	public void init(){
		Thread.currentThread().setName("主线程");
		
		a.foo(b);
		System.out.println("进入了主线程之后");
	}
	@Override
	public void run() {
		Thread.currentThread().setName("副线程");
		b.bar(a);
		
		System.out.println("进入副线程之后:");
	}
	public static void main(String[] args) {
		DeadLock d1 = new DeadLock();
		new Thread(d1).start();
		d1.init();
	}

}

结果:

可以看到程序一直在运行,控制台不动的状态。

那么是怎么出现死锁的呢?我们来分析一下。

1、首先主方法new一个DeadLock线程调用执行,然后在DeadLock中创建了两个对象(A,B)。

2、在new DeadLockd时会调用run()方法,run方法调用了b.bar(a),而d1调用init()方法,init又调用了a的foo(b),这时就会产生冲突,a:你给我让让,让我先调用下你,b:你给我让让,让我先调用下你。这样就因为互不相让导致程序得不到执行以及资源得不到释放。

死锁的产生简而意之就是你调用我,我调用你。

死锁解决不了,只能避免。

那么怎么避免呢?

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

线程的同步和死锁说完了,在说说线程的通信。​​​​​​​

线程的通信

先看看一段代码

class Communication implements Runnable {
    int i = 1;
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                } else
                    break;
                try {
                    wait();
                } catch (InterruptedException e) {
		    e.printStackTrace();
                }
            }
        }
    }
}

可以看到代码中有下面前两个方法: 

  • wait():令当前线程挂起并放弃CPU同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
  • notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明

wait()方法

  • 在当前线程中调用方法:  对象名.wait()
  • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (notifyAll) 为止。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  • 调用此方法后,当前线程将释放对象监控权  ,然后进入等待
  • 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

​​​​​​​ notify()/notifyAll()方法

  • 在当前线程中调用方法:  对象名.notify()
  • 功能:唤醒等待该对象监控权的一/所有线程
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

生产者/消费者问题

        生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

分析:
  1.是否是多线程问题?是。生产者、消费者
  2.会出现线程安全问题吗?(换句话说,是否有共享数据?)有!共享数据:产品的数量
  3.会使用线程通信吗?有!

用代码实现一下:

class Clerk{ //员工
	private int productCount;  //产品的数量
	//同步方法
	public synchronized void addProduct(){ //增加产品
		if(productCount >= 20){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}else{
			productCount++;
			System.out.println(Thread.currentThread().getName()
					+"生产了第"+productCount+"个产品");
			notify();
		}
	}
	
	public synchronized void reduceProduct(){ //减少产品
		if(productCount <= 0){
			try{
				wait();
			}catch(InterruptedException ex){
				ex.printStackTrace();
			}
		}else{
			System.out.println(Thread.currentThread().getName()
					+"消费了第"+productCount+"个产品");
			productCount--;
			notify();
		}
	}
 	
}


class Consumer implements Runnable{//消费者
	Clerk clerk;    //创建一个员工对象
	
	public Consumer(Clerk clerk){
		this.clerk = clerk;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"开始消费...");
		
		while(true){
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			clerk.reduceProduct();   //消费产品
		}
	}
	
}

class Product implements Runnable{//生产者

	Clerk clerk;
	
	public Product(Clerk clerk){
		this.clerk = clerk;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"开始生产...");
		
		while(true){
			try {
				Thread.sleep(20);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.addProduct();    //增加产品
		}
	}	
}

public class ProduceConsumerTest {
	public static void main(String[] args) {
		Clerk clerk = new Clerk();   //员工
		
		Product pro = new Product(clerk);   //生产者
		Consumer con = new Consumer(clerk);    //消费者
		//在此创建一个生产者,两个消费者进行测试
		Thread pro1 = new Thread(pro,"生产者");     //生产者线程
		Thread con2 = new Thread(con,"消费者1");	   //消费者线程
		Thread con1 = new Thread(con,"消费者2");     //消费者线程
		
		con1.start();
		con2.start();
		pro1.start();		
	}
}

生产者/消费者就采用了线程的通信

  • 生产者:当if(生产者生产大于20个商品后){会进行wait()等待} else{notify()继续生产}
  • 消费者:当if(消费者消费产品小于0后){会进行wait()等待} else{notify继续消费}

就说这么一个例子,还有点题会分享到下一篇文章里。

猜你喜欢

转载自blog.csdn.net/zxdspaopao/article/details/101429306