java多线程

线程安全问题产生的原因:

1.多个线程在操作共享的数据

2. 操作共享数据的线程代码有多条

用示例解释如下:开启了四条线程 ,同时操作共享的数据,并且有两条操作共享数据的线程代码

class Ticket implements Runnable//extends Thread
{
	private  int tick = 100;
	public void run()
	{
		while(true)
		{
			if(tick>0) //操作一次共享数据
			{
				System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);//第二次操作共享数据
			}
		}
	}
}

class  TicketDemo
{
	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);//创建了一个线程;
		Thread t4 = new Thread(t);//创建了一个线程;
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}//运行过程中票出现了0 -1 -2情况。这与实际已经不符合了
}

解决思路:

    就是将多条操作共享数据的线程代码进行封装,当有线程在执行这些代码的时候,其他线程不能参与运算。必须是当前线程执行完 这些代码后,其它线程才能参与运算。   通常使用同步代码块或者 同步函数 。  synchronized

同步的出现,解决了线程的安全问题。却相对降低了效率,因为同步外的线程都会去判断同步锁。

同步的前提:必须有多个线程并且使用同一个锁。

 仍旧拿上个例子来说,在Ticket中定义一个Object类型的成员变量obj作为锁,把while 循环中的 if语句放入同步代码块中就解决了出现0 -1 -2的情况。  

同步函数和同步代码块的区别:

    同步函数的锁是固定的this(调用run方法的对象)

    同步代码块的锁是任意的对象 。建议使用同步代码块

静态同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。·

当单例遇上了多线程, 如何来保持对象单一呢?

如果不明白单例模式,请看这篇文章  java设计模式

class Single
{ // 饿汉式
	private static final Single s = new Single();
	private Single(){}
	public static Single getInstance()
	{
		return s;// 只有一条操作共享数据的代码
	}
}



//懒汉式

class Single
{
	private static Single s = null;
	private Single(){}


	public static  Single getInstance()
	{
		//if(s==null)
		//{
			synchronized(Single.class)
			{
				if(s==null)
					//--->A;
					s = new Single();
			}
		//}
		return s;
	}
}

请看上面的代码,对于饿汉式来说,仅仅有一条操作共享数据的代码,所以不会出现线程安全问题,也能保证对象的单一。

但对于懒汉式来说,情况就不同了。请看上面懒汉式的单例代码,当不加同步代码时,某个时刻会存在这样的情况(以两个线程为例): 线程A判断s=null成立,进入if语句,此时恰好时间片切换到线程B(线程A还没有创建单例对象),于是线程B开始执行,也判断了s是否为空,为真,于是也进入了if语句,并创建了对象,此时时间片又切换到线程A,因为已经进入了if语句,就又创建了一次单例对象,这样,就不符合我们的单例设计模式了。  我们加入同步之后,当线程A挂起,线程B开始执行后,由于没有锁,无法执行,只能乖乖滴等线程A执行完毕,释放了锁,B才能执行,此时,已经创建了对象,s!= null ,不会再创建对象了,就保证了对象的单一 。  如上面的代码所示。又在同步前面加了一层if判定是为了尽量避免别的线程去判断锁,判断锁太消耗资源,稍稍提高下效率,也可以注释掉。

------------------

死锁

在了解死锁前我们有必要了解一下死锁是怎么产生的呢? 

    死锁是两个及以上线程在执行过程中,由于抢夺资源而造成的互相等待对方的资源,在没有得到对方资源的情况下,不会释放自身持有的资源,这种情况就叫做死锁。

死锁的常见的形式之一: 同步的嵌套

class Ticket implements Runnable
{
	private  int tick = 1000;
	Object obj = new Object();
	boolean flag = true;
	public  void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(obj)
				{
					show();
				}
			}
		}
		else
			while(true)
				show();
	}
	public synchronized void show()//this
	{
		synchronized(obj)
		{
			if(tick>0)
			{
				try{Thread.sleep(10);}catch(Exception e){}
				System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
			}
		}
	}
}


class  DeadLockDemo
{
	public static void main(String[] args) 
	{

		Ticket t = new Ticket();

		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		try{Thread.sleep(10);}catch(Exception e){} //使主线程睡眠,不继续进行
		t.flag = false;
		t2.start();


	}
}

如上述代码所示,当主线程运行起来之后,就把t1线程启动了,此时就去执行run里面的方法,t1线程拿到obj和this锁,开始执行代码,但是可能存在这样的一个时刻,当t1线程执行完show方法,释放了this锁之后,时间片切换到主线程了,主线程就将flag改为false ,并且启动t2线程。此时t2线程开始运行,由于flag为false,t2线程执行run方法中的else中的语句,执行同步函数show() ,t2持有this锁,进入发现没有obj锁,无法继续执行,因为是while不断循环,t1拿着obj锁进入同步代码块发现并没有this锁,不能执行同步函数,于是两个线程都在等对方释放锁,程序就宕起了。这就是死锁。

  线程间通讯:

        多个线程在处理同一个个资源,但是任务却不同。我们要注意的是在操作共享的数据的时候,注意保持线程同步,避免出现错误的结果。

等待唤醒机制

    涉及的方法:

        1.wait() 使线程处于冻结状态,释放cpu执行权和执行资格(释放锁),被wait的线程会被存储到线程池(即容器,用于存放没有消亡的线程)中。

        2. notify() 唤醒线程池中的一个线程(任意),注意:唤醒的是同一个锁上的任意一个线程

        3. notifyAll()唤醒线程池中的所有线程。即是所有线程获得执行资格。

注意: 这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程。  不同的锁存在着 不同的线程池,用锁区分。例如,r.wait() 之后就用r.notify()唤醒r线程池中的线程。

查看api发现,这些方法都定义在了 Object类中, 这是由于这些方法是监视器的方法。监视器其实就是锁,锁可以是任意对象,任意的对象调用方式一定定义在Object类中。

生产者消费者问题

先写一个简单的需求: 使用两个线程,一个生产烤鸭,一个消费烤鸭 。简单代码如下:

class Resource{
	private String name;
	private int count = 1 ;
	private boolean flag=false;

	public synchronized void set(String name){
		if(flag)
			try{this.wait();}catch(Exception x){}

		this.name =name+count;
		count++ ;
		System.out.println(Thread.currentThread().getName()+"..生产者"+this.name);
		flag = true ; notify();
	}
	public synchronized void out(){
		if(!flag)
			try{this.wait();}catch(Exception x){}
		System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);
		flag = false ;  notify() ;
	}
}
public class ProduceDemo{
	public static void main(String[] args) {
		Resource r = new Resource();
	
		Producer pro = new Producer(r);
		Consumer con =new Consumer(r);

		Thread t0 = new Thread(pro);
		//Thread t1 = new Thread(pro);// 暴露多生产多消费问题时,打开此注释
		Thread t2 =new Thread(con) ;
		//Thread t3 =new Thread(con) ;
		t0.start(); t2.start() ;
		//t1.start() ;t3.start(); 
		
	}
}
class Consumer implements Runnable{
	Resource r ;
	Consumer(Resource r){
		this.r=r;
	}
	public void run(){
		while(true)
			r.out();
	}
}
class Producer implements Runnable{
	private Resource r;
	Producer(Resource r){
		this.r=r; 
	}
	public void run(){
		while(true)
			r.set("烤鸭.."); 
	}
}

这里我们使用的是同步函数,保证生产过程结束,消费者才去消费 。但是问题来了,假如我们的小作坊要扩展业务(多个生产,多个消费),此时把上面代码中的注释解开,运行代码就会有以下的情况发生:

Thread-1..生产者烤鸭..2718
Thread-0..生产者烤鸭..2719
Thread-3..消费者.......烤鸭..2719
Thread-2..消费者.......烤鸭..2719

生产者生产了两个烤鸭,但是一只没有被消费,另一只被消费了两次,这是什么原因呢? 回到代码中分析如下: 

在某个时间点上,存在着这样的情况,线程t1判断标记为真,已经wait(),而且之前t0也已经wait() (注意这完全是有可能的),此时消费者刚消费完一只烤鸭,将flag改为false,并唤醒this锁的线程池中的一个线程,假如唤醒了t1,t1继续执行下面的代码(并没有返回去判断if中的标记),就生产了烤鸭2178 ,并把flag标记改为true,又唤醒一个线程,不幸运的是t0被唤醒了,于是又生产了一只烤鸭,到这里已经出现线程安全问题了,生产了两只。同理,对消费者来说也是一样,由于唤醒线程的不确定性使得,一只烤鸭被消费了两次 。分析问题产生的原因,是由于挂起的线程被唤醒后,并没有再去判断一次标记,才导致这种情况发生。

怎么样让它返回去在判断一次标记呢? 使用while ,我们知道if语句仅仅执行一次,但是while由于是个循环结构,会在返回去判断一次。到这里问题似乎已经解决了,可是情况是这样吗?  我们将set和out中的if语句改为while,并执行代码,却发现了程序宕机 ,也就是死锁。这是由于存在这样的片段,消费者线程t2抢到了执行权,消费了一直烤鸭后将标记改为false,并唤醒了一个线程,结果就唤醒了另一个消费者线程t3,于是t3就进入了等待状态,接着t2又抢到了执行权,也进入了等待状态。此时两个生产者线程中假如t0抢到执行权,就生产了一只烤鸭,把标记改为true ,问题就出现在这里,如果唤醒的仍旧是生产者中的一个线程,由于标记为true,就进入了等待状态,此时线程池存活状态的就只有生产者中的一个线程,一执行立即也进入了等待状态,线程池中的四个线程全部进入等待状态,程序就宕机了。

怎么解决这个问题呢?

分析原因,其实就是唤醒任意一个线程造成的,如果消费者线程唤醒的是生产者中的一个线程,那么程序就和谐了,所以如何保证生产者 唤醒消费者中的一个线程,是解决问题的关键。可以使用notifyAll()方法,将线程池中的所有线程都唤醒,这就把问题解决了。

总结如下:

if判断标记,只有一次,,会导致不该运行的线程运行了,出现了数据错误的情况、
while判断标记, 解决了线程获取执行权后,是否要运行。
notify 只能唤醒一个线程, 如果本方唤醒了本方,没有意义。而且while判断标记+notify会导致死锁。
notifyAll解决了本方线程一定会唤醒对方线程的问题。

至此附上已经解决问题的代码。

class Resource{
	private String name;
	private int count = 1 ;
	private boolean flag=false;

	public synchronized void set(String name){
		while(flag)
			try{this.wait();}catch(Exception x){}

		this.name =name+count;
		count++ ;
		System.out.println(Thread.currentThread().getName()+"..生产者"+this.name);
		flag = true ; notifyAll();
	}
	public synchronized void out(){
		while(!flag)
			try{this.wait();}catch(Exception x){}
		System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);
		flag = false ;  notifyAll() ;
	}
}
public class ProduceDemo{ 
	public static void main(String[] args) {
		Resource r = new Resource();
	
		Producer pro = new Producer(r);
		Consumer con =new Consumer(r);

		Thread t0 = new Thread(pro);
		Thread t1 = new Thread(pro);// 暴露多生产多消费问题时,打开此注释
		Thread t2 =new Thread(con) ;
		Thread t3 =new Thread(con) ;
		t0.start(); t2.start() ;
		t1.start() ;t3.start(); 	
	}
}
class Consumer implements Runnable{
	Resource r ;
	Consumer(Resource r){
		this.r=r;
	}
	public void run(){
		while(true)
			r.out();
	}
}
class Producer implements Runnable{
	private Resource r;
	Producer(Resource r){
		this.r=r; 
	}
	public void run(){
		while(true)
			r.set("烤鸭.."); 
	}
}

但是notifyALl()虽然解决了问题,但是己方线程也被唤醒了,还要再去判断标记,浪费资源,降低了性能。怎么样才能仅仅唤醒对方的线程呢? 有! jdk1.5以后,将同步和锁封装成了对象 。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显式动作。  java.util.concurrent.locks 包下提供的Lock 接口。请看Lock的源码。四个方法。lock和unlock方法用来获取锁和释放锁。newCondittion()方法,返回一个Condition对象。其一个常用实现子类ReentrantLock   

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();// 返回绑定到此Lock实例的新Condition实例
}
Condition 接口,位于 java.util.concurrent.locks 包下,将Object监视器方法(wait,notify,notifyAll)分解为不同的对象,使得可以通过这些对象实现与任意Lock组合使用。Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。 

常用方法: void  await() 造成当前线程在接到信息或被中断之前一直处于等待状态

                boolean await(long time,TimeUnit unit)造成当前线程在接到信号、被中断或到达等待时间之前一直处于等待状态

                void  signal()  唤醒一个等待线程 。

                void signalAll() 唤醒所有等待线程。

到此就可以允许一个锁上有多组监视器,仍旧拿生产者消费者问题来说,我们可以在生产者中使用消费者监视器唤醒消费者线程。这样就避免了原本的会唤醒己方锁,造成资源浪费的情况。

import java.util.concurrent.locks.Lock ;
import java.util.concurrent.locks.Condition ;
import java.util.concurrent.locks.ReentrantLock;

class Resource{
	private String name;
	private int count = 1 ;
	private boolean flag=false;

	//创建一个锁对象。
	Lock lock = new ReentrantLock();
	// 通过已有的锁获取该锁上的监视器对象。
	Condition pro =lock.newCondition();
	Condition cus =lock.newCondition();

	public  void set(String name){
		lock.lock();
		try{
			while(flag)
			try{pro.await();}catch(Exception x){}

			this.name =name+count;
			count++ ;
			System.out.println(Thread.currentThread().getName()+"..生产者"+this.name);
			flag = true ; 
			cus.signal(); //使用消费者锁的监视器唤醒消费者中的一个线程。
		}finally{
			lock.unlock(); 
		}
	}
	public  void out(){
		lock.lock() ;
		try{
			while(!flag)
			try{cus.await();}catch(Exception x){}
			System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);
			flag = false ;  
			pro.signal(); // 唤醒生产者中的一个线程。
		}finally{
			lock.unlock();
		}
		
	}
}
public class ProduceDemo{
	public static void main(String[] args) {
		Resource r = new Resource();
	
		Producer pro = new Producer(r);
		Consumer con =new Consumer(r);

		Thread t0 = new Thread(pro);
		Thread t1 = new Thread(pro);// 暴露多生产多消费时,打开此注释
		Thread t2 =new Thread(con) ;
		Thread t3 =new Thread(con) ;
		t0.start(); t2.start() ;
		t1.start() ;t3.start(); 
		
	}
}
class Consumer implements Runnable{
	Resource r ;
	Consumer(Resource r){
		this.r=r;
	}
	public void run(){
		while(true)
			r.out();
	}
}
class Producer implements Runnable{
	private Resource r;
	Producer(Resource r){
		this.r=r; 
	}
	public void run(){
		while(true)
			r.set("烤鸭.."); 
	}
}

wait和sleep的区别:

 1. wait可以指定时间也可以不指定。sleep必须指定时间

2,在同步中时,对cpu的执行权和锁的处理不同

    wait  :释放执行权,释放锁

    sleep: 释放执行权,不释放锁。

在多生产多消费中有个细节:

    同步中只有一个线程可以运行,但是可以存在多个活着的线程。借用下面的简单代码帮助理解:

class Demo{
	void show(){
		synchronized(this){ // t0 t1 t2
			wait();
		}
	}
	void method(){
		synchronized(this){
			notifyAll() ;
		}
	}
}
某个片段,t0,t1,t2执行show方法,结果就都wait了,此时假如t3执行了method方法,由于使用的是同一个锁,所以t0,t1,t2全部被唤醒了。这就证明了同步中可以存在多个活着的线程,但是存活(具有执行权)并不代表可以执行,在t0 ,t1,t2中只有拿到锁的线程才能继续执行下面的代码,并且当执行完同步代码块,释放锁之后其余线程才可能获取到锁,执行代码。

    

你可能感兴趣的:(java,基础,多线程,Java基础)