1、多线程安全问题分析
线程安全问题的产生原因:
多线程安全问题原因是在cpu执行多线程时,在执行的过程中可能随时切换到其他的线程上执行。
买票程序中:
在以上红色选中的三个部分,线程都有可能进行切换。只要cpu在这个三个地中的任何地方切换了,都可能导致错误数据出现,线程的不安全因素就有了。
造成错误数据的原因是多个线程可能出现同时访问num的情况。而任何一个线程在访问num的过程中都可以切换到其他的线程上。而其他线程一旦把num的数据改变了,再切换回来时,错误数据就有了。
2、多线程安全问题解决
要解决上述的线程安全问题,错误数据问题。在一个线程进入到if中之后,当cpu切换到其他线程上时,不让其他的线程进入if语句,那么就算线程继续执行当前其他的线程,也无法进入到if中,这样就不会造成错误数据的出现。
Java中给我们提供是同步代码块技术来解决这个线程的安全问题:
格式:
synchronized(任意的对象(锁) )
{
写要被同步的代码
}
同步代码块就可以认为成卫生间的门和锁。
多线程安全问题,是由于多个线程在访问共享的数据(共享的资源),并且操作共享数据的语句不止一条。那么这样在多条操作共享数据的之间线程就可能发生切换。只要切换就有安全问题,那么我们就可以把同步加在这些操作共享数据的代码上。
ThreadDemo.java源代码
1 //书写售票的示例 2 class Demo implements Runnable{ 3 //定义变量记录剩余的票数 4 int num = 100; 5 6 //创建一个对象,用作同步中的锁对象 7 Object obj = new Object(); 8 9 //实现run方法 10 public void run() { 11 12 //实现售票的过程 13 while( true ) { 14 // t1 t2 t3 15 //判断当前有没有线程正在if中操作num,如果有当前线程就在这里临时等待 16 //这种思想称为线程的同步 17 //当票数小等于0的时候,就不再售票了 18 //使用同步代码块把线程要执行的任务代码可以同步起来 19 synchronized( obj ) //t1 在进入同步之前线程要先获取锁 20 /* 21 当某个线程执行到synchronized关键字之后,这时JVM会判断当前 22 同步上的这个对象有没有已经被其他线程获取走了,如果这时没有其他 23 线程获取这个对象,这时就会把当前同步上的这个对象交给当前正要进入 24 同步的这个线程。 25 */ 26 { 27 if( num > 0 ) 28 { 29 //t0 30 try{Thread.sleep(2);}catch( InterruptedException e ){} 31 System.out.println(Thread.currentThread().getName()+"....."+num); 32 num--; 33 } 34 }//线程执行完同步之后,那么这时当前这个线程就会把锁释放掉 35 } 36 } 37 } 38 class ThreadDemo { 39 public static void main(String[] args) { 40 //创建线程任务 41 Demo d = new Demo(); 42 //创建线程对象 43 Thread t = new Thread( d ); 44 Thread t2 = new Thread(d); 45 Thread t3 = new Thread(d); 46 Thread t4 = new Thread(d); 47 //开启线程 48 t.start(); 49 t2.start(); 50 t3.start(); 51 t4.start(); 52 } 53 }
3、多线程安全问题细节
3.1、同步的好处和弊端
好处:可以保证多线程操作共享数据时的安全问题
弊端:降低了程序的执行效率。
3.2、同步的前提
要同步,必须有多个线程,多线程在操作共享的数据,同时操作共享数据的语句不止一条。
3.3、加入了同步安全依然存在
首先查看同步代码块的位置是否加在了需要被同步的代码上。如果同步代码的位置没有错误,这时就再看同步代码块上使用的锁对象是否是同一个。多个线程是否在共享同一把锁。
4、同步锁的问题
4.1、同步是否可以加在run方法上
方法上是可以加同步的,但是不建议把同步加在run方法,如果把同步加在了run方法上,导致任何一个线程在调用start方法开启之后,JVM去调用run方法的时候,首先都要先获取同步的锁对象,只有获取到了同步的锁对象之后,才能去执行run方法。而我们在run中书写的被多线程操作的代码,永远只会有一个线程在里面执行。只有这个线程把这个run执行完,出去之后,把锁释放了,其他某个线程才能进入到这个run执行。
4.2、同步方法上的锁
同步代码块使用的锁是任意对象(由使用者自己来手动的指定)。
非静态的方法上加的同步使用的锁是当前对象(当前调用这个方法的那个对象)。
静态方法上使用的锁是当前的class文件(当前这个方法所属的class文件)。
演示同步代码块的锁、同步方法的锁 、静态同步方法的锁源代码:
1 class Demo implements Runnable 2 { 3 static int num = 100; 4 boolean flag = true; 5 6 Object obj = new Object(); 7 8 public void run() 9 { 10 if( flag ) //使用判断进行线程执行任务的代码切换 11 { 12 while(true) 13 { //thread-0 14 synchronized( Demo.class ) 15 { 16 if( num > 0 ) 17 { 18 System.out.println(Thread.currentThread().getName()+"......"+num); 19 num--; 20 } 21 } 22 } 23 } 24 else 25 { 26 while(true) 27 { 28 //thread-1 29 show(); 30 } 31 } 32 } 33 /* 34 show方法中的所有代码全部是需要被同步的代码。这时就可以把这个同步加载show方法 35 在方法上加同步的格式:直接在方法上书写同步关键字即可 36 37 38 非静态的方法在执行的时候需要被对象调用。 39 这个show方法是被当前的Demo对象调用的,这时在show方法上加的同步使用的锁就是当前的 40 那个Demo对象。就是this 41 42 43 静态方法上使用的锁不是this,静态方法执行时不需要对象。而静态方法是被类名直接调用。 44 静态方法上使用的锁是当前的class文件 45 */ 46 public static synchronized void show() 47 { 48 if( num > 0 ) 49 { 50 System.out.println(Thread.currentThread().getName()+"================="+num); 51 num--; 52 } 53 } 54 } 55 56 class ThreadDemo3 57 { 58 public static void main(String[] args) 59 { 60 //创建任务 61 Demo d = new Demo(); 62 63 //创建线程 64 Thread t = new Thread(d); 65 Thread t2 = new Thread(d); 66 67 t.start(); 68 //程序是从主线程开始运行,在运行时,虽然开启了thread-0线程, 69 //但是cpu可能不会立刻切换到thread-0线程上。 70 //为了保证thread-0一定能够进入到if中执行,thread-1进入else执行 71 //让主线程在开启thread-0线程之后,让主线程休眠 72 try{Thread.sleep(1);}catch(InterruptedException e){} 73 //把标记改为false,让下一个线程进入的else中执行 74 d.flag = false; 75 t2.start(); 76 } 77 }
售票的例子使用Runnable接口实现并加同步:
1 //书写售票的示例 2 class Demo implements Runnable{ 3 //定义变量记录剩余的票数 4 int num = 100; 5 6 //创建一个对象,用作同步中的锁对象 7 Object obj = new Object(); 8 9 //实现run方法 10 public void run() { 11 12 //实现售票的过程 13 while( true ) { 14 // t1 t2 t3 15 //判断当前有没有线程正在if中操作num,如果有当前线程就在这里临时等待 16 //这种思想称为线程的同步 17 //当票数小等于0的时候,就不再售票了 18 //使用同步代码块把线程要执行的任务代码可以同步起来 19 synchronized( obj ) //t1 在进入同步之前线程要先获取锁 20 /* 21 当某个线程执行到synchronized关键字之后,这时JVM会判断当前 22 同步上的这个对象有没有已经被其他线程获取走了,如果这时没有其他 23 线程获取这个对象,这时就会把当前同步上的这个对象交给当前正要进入 24 同步的这个线程。 25 */ 26 { 27 if( num > 0 ) 28 { 29 //t0 30 try{Thread.sleep(2);}catch( InterruptedException e ){} 31 System.out.println(Thread.currentThread().getName()+"....."+num); 32 num--; 33 } 34 }//线程执行完同步之后,那么这时当前这个线程就会把锁释放掉 35 36 37 } 38 } 39 } 40 class ThreadDemo { 41 public static void main(String[] args) { 42 //创建线程任务 43 Demo d = new Demo(); 44 //创建线程对象 45 Thread t = new Thread( d ); 46 Thread t2 = new Thread(d); 47 Thread t3 = new Thread(d); 48 Thread t4 = new Thread(d); 49 //开启线程 50 t.start(); 51 t2.start(); 52 t3.start(); 53 t4.start(); 54 } 55 }
售票的例子使用Thread实现并加同步:
1 class Ticket extends Thread 2 { 3 static int num = 10000; 4 static Object obj = new Object(); 5 public void run() 6 { 7 while( num > 0 ) 8 { 9 synchronized(obj) 10 { 11 if( num > 0 ) 12 { 13 System.out.println(Thread.currentThread().getName()+"..."+num); 14 15 num--; 16 } 17 } 18 } 19 20 } 21 22 } 23 class ThreadDemo2 24 { 25 public static void main(String[] args) 26 { 27 Ticket t = new Ticket(); 28 Ticket t2 = new Ticket(); 29 Ticket t3 = new Ticket(); 30 Ticket t4 = new Ticket(); 31 32 t.start(); 33 t2.start(); 34 t3.start(); 35 t4.start(); 36 } 37 }
5、单例懒汉式线程并发问题
单例设计模式:保证当前程序中的这个类对象唯一。
单例设计模式代码书写步骤:
1、私有本类的构造方法
2、创建本类对象
3、对外提供访问本类对象的方法
常用的模版代码:
1 //饿汉式 2 class Single 3 { 4 private Single(){} 5 6 private static final Single s = new Single(); 7 8 public static Single getInstance() 9 { 10 return s; 11 } 12 13 } 14 15 16 //懒汉式 17 class Single 18 { 19 private Single(){} 20 21 private static Single s = null; 22 23 public static Single getInstance() 24 { 25 if( s == null ) 26 { 27 s = new Single(); 28 } 29 return s; 30 } 31 }
懒汉式的多线程并发访问的安全问题:
1 //书写单例类 2 class Single 3 { 4 private static Object obj = new Object(); 5 //私有构造方法 6 private Single() 7 { 8 } 9 //定义成员变量记录当前本类的对象 10 private static Single s = null; 11 12 //对外提供方法访问本类的对象 13 public static Single getInstance() 14 { 15 /* 16 这里有安全问题,当多个线程进来之后,在执行判断和创建对象之间可能出现 17 cpu在线程之间的切换,一旦cpu切换了线程,就会导致每个线程都创建 18 一个当前单例类的对象,这样就会导致当前的单例类不再保证对象唯一了 19 */ 20 //t1 21 if( s == null )// 这里加判断的目的是保证后续来的线程不用在进入同步代码块中,这个可以提高后续程序效率 22 { 23 synchronized( obj ) 24 { 25 //先判断有没有对象 26 if( s == null ) //判断最后线程进入同步之后到底有没有对象,只有在没有对象的情况下才能创建对象 27 { 28 s = new Single(); 29 } 30 } 31 //t0 32 } 33 //返回对象 34 return s; 35 } 36 } 37 //多线程并发访问单例类获取对象 38 class Demo implements Runnable 39 { 40 public void run() 41 { 42 Single s = Single.getInstance(); 43 System.out.println("s="+s); 44 } 45 } 46 class SingleThreadDemo 47 { 48 public static void main(String[] args) 49 { 50 //创建线程的任务 51 Demo d = new Demo(); 52 53 //创建线程对象 54 Thread t = new Thread(d); 55 Thread t2 = new Thread(d); 56 57 t.start(); 58 t2.start(); 59 } 60 }
6、死锁示例
死锁:
在多线程执行任务的时候,可能出现需要获取到多把锁才能去完成某个任务。
Thread-0 、Thread-1
它们要完成线程的任务,而它们都需要获取不同的锁才能完成。
Thread-0 需要先获取A锁 再获取B才能去完成任务
而Thread-1线程需要先获取B锁,在获取A锁才能完成任务。
这时Thread-0 获取到了A的时候,CPU切换到Thread-1上,这时Thread-1就获取到了B锁。
这时就出现了2个线程要执行任务都需要获取对方线程上的那个锁。
死锁示例源代码:
1 //死锁程序演示 2 class Demo implements Runnable 3 { 4 int num = 100; 5 //定义一个锁 6 Object obj = new Object(); 7 8 //定义标记,让两个线程去不同的代码块中实现售票 9 boolean flag = true; 10 11 public void run() 12 { 13 if( flag ) 14 { 15 while( true ) 16 { 17 synchronized( obj ) 18 { 19 show(); 20 } 21 } 22 } 23 else 24 { 25 while(true) 26 { 27 show(); 28 } 29 } 30 } 31 public synchronized void show() 32 { 33 34 synchronized( obj ) 35 { 36 if( num > 0 ) 37 { 38 System.out.println(Thread.currentThread().getName()+"............"+num ); 39 num--; 40 } 41 } 42 } 43 } 44 class DeadLockDemo 45 { 46 public static void main(String[] args) 47 { 48 Demo d = new Demo(); 49 50 Thread t = new Thread(d); 51 Thread t2 = new Thread(d); 52 53 t.start(); 54 try{Thread.sleep(1);}catch(Exception e){} 55 d.flag = false; 56 t2.start(); 57 58 } 59 }
面试的死锁程序源代码:
1 //死锁程序书写 2 class Demo implements Runnable 3 { 4 private Object objA = new Object(); 5 private Object objB = new Object(); 6 //标记实现线程可以切换 7 boolean flag = true; 8 //实现run方法 9 public void run() 10 { 11 //通过判断flag标记目标是让不同的线程在if和else中执行 12 if( flag ) 13 { 14 while(true) 15 { 16 synchronized( objA ) 17 { 18 System.out.println(Thread.currentThread().getName()+"....objA"); 19 //t0 20 synchronized( objB ) 21 { 22 System.out.println(Thread.currentThread().getName()+"....objB"); 23 } 24 } 25 } 26 } 27 else 28 { 29 while(true) 30 { 31 synchronized( objB ) 32 { 33 System.out.println(Thread.currentThread().getName()+"....objB"); 34 //t1 35 synchronized( objA ) 36 { 37 System.out.println(Thread.currentThread().getName()+"....objA"); 38 } 39 } 40 } 41 } 42 } 43 } 44 45 class DeadLock 46 { 47 public static void main(String[] args) 48 { 49 Demo d = new Demo(); 50 51 Thread t = new Thread(d); 52 Thread t2 = new Thread(d); 53 54 t.start(); 55 try{Thread.sleep(1);}catch(Exception e){} 56 d.flag = false; 57 t2.start(); 58 } 59 }
7、生产者消费者
生产者和消费者程序:
生产者主要负责生产商品,消费者主要负责消费商品。假如生产者把生产者的商品要存放的容器中,而消费者要从容器中取出商品进行消费。
当生产者正在生产的时候,消费者不能来消费。或者是消费者正在消费的时候生产者不要来生产。
当生产者把容器存放满了,这时生产者就不能在继续给容器中存放商品,消费者如果把容器中的商品消费完了,消费者就不能在继续消费。
分析生产者线程和消费者线程:
生产者线程主要负责给容器中存放商品,而消费者线程主要负责从容器中取出商品进行消费。生产者线程和消费者线程它们的线程任务不一样。
由于生产者和消费者线程任务不同,就无法写同一个run方法中,就需要书写两个run方法来封装生产者的现场那个任务和封装消费者的线程任务,
那么就需要定义2个类。这个2个类分别负责生产者和消费者线程任务。
分析容器:
消费者线程任务对象一旦创建就必须明确自己消费的是那个容器中的商品,而生产者线程任务对象一创建,就必须明确自己生产的商品存放到哪个容器中。
这时我们可以单独定义一个类,这个类专门负责封装容器。并且这个类需要对外提供给容器中存放商品的功能,和从容器中取出商品的功能。
单生产单消费代码简单实现:
1 /* 2 生产者和消费者 3 为了程序简单化,先研究单生产和单消费。 4 */ 5 6 //一个用来封装容器的类 7 class Resource 8 { 9 //这里定义一个容器,主要负责用来存放商品和取出商品 10 //定义了一个数组容器,这个容器中只能存放一个商品 11 private Object[] objs = new Object[1]; 12 13 private Object obj = new Object(); 14 15 //对外提供给容器中存放商品的方法 16 public void put( Object obj ) 17 { 18 objs[0] = obj; 19 try{Thread.sleep(1);}catch( InterruptedException e ){} 20 System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); 21 } 22 //对外提供给容器中取出商品的方法 23 public void get() 24 { 25 System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); 26 27 objs[0] = null; 28 } 29 } 30 31 //一个类用来描述生产者线程的任务 32 class Producer implements Runnable 33 { 34 //定义一个成员变量记录当前传递进来的资源对象 35 private Resource r; 36 //在一创建生产者任务对象的时候,就必须明确当前的商品要存放到那个容器资源中 37 Producer( Resource r ) 38 { 39 this.r = r; 40 } 41 //实现run方法,在run方法中完成生产线程的存放商品的动作 42 public void run() 43 { 44 //定义变量记录当前生产的商品编号 45 int num = 1; 46 //生产者线程一旦进入run方法之后就不断的来生产 47 while( true ) 48 { 49 r.put( "面包"+num ); 50 num++; 51 } 52 } 53 } 54 55 //一个类用来描述消费者线程的任务 56 class Consumer implements Runnable 57 { 58 private Resource r; 59 Consumer( Resource r ) 60 { 61 this.r = r; 62 } 63 //run方法中封装的消费者消费商品的任务代码 64 public void run() 65 { 66 while( true ) 67 { 68 r.get(); 69 } 70 } 71 } 72 73 class ThreadDemo4 74 { 75 public static void main(String[] args) 76 { 77 //创建生产者或者消费者要操作的资源对象 78 Resource r = new Resource(); 79 80 //创建生产者任务对象 81 Producer pro = new Producer( r ); 82 //创建消费者任务对象 83 Consumer con = new Consumer( r ); 84 85 86 //创建线程对象 87 Thread t = new Thread( pro ); 88 Thread t2 = new Thread( con ); 89 90 t.start(); 91 t2.start(); 92 93 } 94 }
单生产单消费 中的同步实现:
/* 加入同步解决的是在存入的时候不能消费 在消费的时候不能存入。 */ //一个用来封装容器的类 class Resource { //这里定义一个容器,主要负责用来存放商品和取出商品 //定义了一个数组容器,这个容器中只能存放一个商品 private Object[] objs = new Object[1]; private Object obj = new Object(); //对外提供给容器中存放商品的方法 public void put( Object obj ) { //加入同步目的是保证生产者在生产的时候,消费者不能消费 synchronized( obj ) { objs[0] = obj; System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); } } //对外提供给容器中取出商品的方法 public void get() { //加入同步目的是在消费者消费的时候生产者不要来生产 synchronized( obj ) { System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); objs[0] = null; } } } //一个类用来描述生产者线程的任务 class Producer implements Runnable { //定义一个成员变量记录当前传递进来的资源对象 private Resource r; //在一创建生产者任务对象的时候,就必须明确当前的商品要存放到那个容器资源中 Producer( Resource r ) { this.r = r; } //实现run方法,在run方法中完成生产线程的存放商品的动作 public void run() { //定义变量记录当前生产的商品编号 int num = 1; //生产者线程一旦进入run方法之后就不断的来生产 while( true ) { r.put( "面包"+num ); num++; } } } //一个类用来描述消费者线程的任务 class Consumer implements Runnable { private Resource r; Consumer( Resource r ) { this.r = r; } //run方法中封装的消费者消费商品的任务代码 public void run() { while( true ) { r.get(); } } } class ThreadDemo5 { public static void main(String[] args) { //创建生产者或者消费者要操作的资源对象 Resource r = new Resource(); //创建生产者任务对象 Producer pro = new Producer( r ); //创建消费者任务对象 Consumer con = new Consumer( r ); //创建线程对象 Thread t = new Thread( pro ); Thread t2 = new Thread( con ); t.start(); t2.start(); } }
单生产单消费 中的等待唤醒机制:
/* 解决问题: 由于程序中给的容易仅仅只能存放一个商品, 当生产者正好把商品存放到容器中之后,这时cpu依然在执行生产者线程,很多有可能出现继续执行生产者往 容器中存放商品的现象。这时一旦生产者第二次或者多次存放商品前面已经存放进去的商品就没有被消费 而被后续的商品给覆盖了。 当消费者正好把容器中的商品消费完了,cpu还在执行消费者线程时,就可能发生消费者再次来消费, 但是容器中并没有商品,这时就会出现消费null 解决思路: 在生产者生产商品的时候,首先不应该立刻给容器中就存放商品,而应该先判断当前容器中 有没有商品,如果有就不能存放,应该等待消费者来消费这个商品,当消费者消费完这个商品之后 生产者就可以来把商品存放到容器中。 消费者来消费商品的时候,也不能直接去消费,也需要判断当前的容器中有没有商品,如果有 才能消费,如果没有就需要等待。等待生产者给容器中存放商品。 在解决生产者和消费者问题的时候,我们需要线程进行等待。如何让线程等待呢? 在查阅api的时候在Thread类中没有找到等待的方法,而在Object类中找到等待的方法。 在Java中我们要让那个线程等待,这个线程必须位于同步代码块中,只要线程位于同步中 那么这个线程肯定会持有当前同步的锁对象。 我们如果要让某个线程从执行状态改变为等待的状态,这时需要使用当前同步上的锁对象, 这时这个锁对象就知道当前是那个线程持有自己。这时这个锁对象就有能力让线程处于等待状态。 一个锁可以监视多个线程。就可以使用这个锁让当前线程等待。 锁让线程等待,那么这个等待的方法应该属于锁的。而在同步代码块中,锁又是任意的。 任何一个对象都可以作为锁,那么任何一个对象都应该具备让线程处于等待的功能。 任何对象都具备的功能,就需要抽取到Object类中。 */ //一个用来封装容器的类 class Resource { //这里定义一个容器,主要负责用来存放商品和取出商品 //定义了一个数组容器,这个容器中只能存放一个商品 private Object[] objs = new Object[1]; private Object obj_lock = new Object(); //对外提供给容器中存放商品的方法 public void put( Object obj ) { //加入同步目的是保证生产者在生产的时候,消费者不能消费 synchronized( obj_lock ) { //判断当前容器中是否有商品 if( objs[0] != null ) { //让生产者线程等待 try{obj_lock.wait();}catch(InterruptedException e){} } objs[0] = obj; System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); //生产者在生产完商品之后,要通知消费来消费 obj_lock.notify(); } } //对外提供给容器中取出商品的方法 public void get() { //加入同步目的是在消费者消费的时候生产者不要来生产 synchronized( obj_lock ) { //判断当前是否有商品,如果没有消费者线程要等待 if( objs[0] == null ) { //让消费者线程等待 try{obj_lock.wait();}catch(InterruptedException e){} } System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); objs[0] = null; //消费者在消费完商品之后,要通知生产来生产 obj_lock.notify(); } } } //一个类用来描述生产者线程的任务 class Producer implements Runnable { //定义一个成员变量记录当前传递进来的资源对象 private Resource r; //在一创建生产者任务对象的时候,就必须明确当前的商品要存放到那个容器资源中 Producer( Resource r ) { this.r = r; } //实现run方法,在run方法中完成生产线程的存放商品的动作 public void run() { //定义变量记录当前生产的商品编号 int num = 1; //生产者线程一旦进入run方法之后就不断的来生产 while( true ) { r.put( "面包"+num ); num++; } } } //一个类用来描述消费者线程的任务 class Consumer implements Runnable { private Resource r; Consumer( Resource r ) { this.r = r; } //run方法中封装的消费者消费商品的任务代码 public void run() { while( true ) { r.get(); } } } class ThreadDemo6 { public static void main(String[] args) { //创建生产者或者消费者要操作的资源对象 Resource r = new Resource(); //创建生产者任务对象 Producer pro = new Producer( r ); //创建消费者任务对象 Consumer con = new Consumer( r ); //创建线程对象 Thread t = new Thread( pro ); Thread t2 = new Thread( con ); t.start(); t2.start(); } }
多生产多消费 中的等待唤醒机制 需要把if改成while 需要把notify改为notifyAll:
1 /* 2 程序修改成多个生产者和多个消费之后,有出现了商品的覆盖现象, 3 或者消费者不断的消费null现象 4 导致这个原因的问题,是生产者在唤醒的时候把另外的生产者唤醒了,而被唤醒的 5 生产者没有去判断还有没有商品,直接就开始生产了。这样就会出现商品的覆盖。 6 7 消费者消费null的原因是在消费者唤醒其他的消费者,被唤醒的消费者也不去判断有没有 8 商品,就直接开始消费 9 10 解决方案: 11 在唤醒之后,重新去判断有没有商品即可。 12 这时只需要把判断有没有商品的if结构改为while这样就可以在每次唤醒之后,继续进行 13 while的条件进行判断 14 15 把if改为while之后死锁了,原因是生产者唤醒时唤醒了生产者或者消费者消费时唤醒了消费者。 16 这时就在唤醒之后,又回到while中判断,当前被唤醒的继续等待。就会出现死锁现象。 17 解决方案:在唤醒的时候,不仅唤醒本方,也需要唤醒对象。我们这时可以使用notifyAll方法唤醒当前 18 锁对象上等待的所有线程。 19 20 */ 21 22 //一个用来封装容器的类 23 class Resource 24 { 25 //这里定义一个容器,主要负责用来存放商品和取出商品 26 //定义了一个数组容器,这个容器中只能存放一个商品 27 private Object[] objs = new Object[1]; 28 29 private Object obj_lock = new Object(); 30 31 int num = 1; 32 33 //对外提供给容器中存放商品的方法 34 public void put( Object obj ) 35 { 36 //加入同步目的是保证生产者在生产的时候,消费者不能消费 37 synchronized( obj_lock ) 38 { 39 //判断当前容器中是否有商品 40 while( objs[0] != null ) 41 { 42 //让生产者线程等待 43 try{obj_lock.wait();}catch(InterruptedException e){} // t1 //t0 44 } 45 46 objs[0] = obj+""+num; 47 num++; 48 49 System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); 50 51 //生产者在生产完商品之后,要通知消费来消费 52 obj_lock.notifyAll(); 53 54 } 55 } 56 57 58 //对外提供给容器中取出商品的方法 59 public void get() 60 { 61 //加入同步目的是在消费者消费的时候生产者不要来生产 62 synchronized( obj_lock ) 63 { 64 //判断当前是否有商品,如果没有消费者线程要等待 65 while( objs[0] == null ) 66 { 67 //让消费者线程等待 68 try{obj_lock.wait();}catch(InterruptedException e){} // t3 69 } 70 System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); 71 72 objs[0] = null; 73 74 //消费者在消费完商品之后,要通知生产来生产 75 76 obj_lock.notifyAll(); //t2 77 } 78 } 79 } 80 81 //一个类用来描述生产者线程的任务 82 class Producer implements Runnable 83 { 84 //定义一个成员变量记录当前传递进来的资源对象 85 private Resource r; 86 //在一创建生产者任务对象的时候,就必须明确当前的商品要存放到那个容器资源中 87 Producer( Resource r ) 88 { 89 this.r = r; 90 } 91 //实现run方法,在run方法中完成生产线程的存放商品的动作 92 public void run() 93 { 94 //生产者线程一旦进入run方法之后就不断的来生产 95 while( true ) 96 { 97 r.put( "面包" ); 98 } 99 } 100 } 101 102 //一个类用来描述消费者线程的任务 103 class Consumer implements Runnable 104 { 105 private Resource r; 106 Consumer( Resource r ) 107 { 108 this.r = r; 109 } 110 //run方法中封装的消费者消费商品的任务代码 111 public void run() 112 { 113 while( true ) 114 { 115 r.get(); 116 } 117 } 118 } 119 120 class ThreadDemo7 121 { 122 public static void main(String[] args) 123 { 124 //创建生产者或者消费者要操作的资源对象 125 Resource r = new Resource(); 126 127 //创建生产者任务对象 128 Producer pro = new Producer( r ); 129 //创建消费者任务对象 130 Consumer con = new Consumer( r ); 131 132 133 //创建线程对象 134 Thread t = new Thread( pro ); 135 Thread t2 = new Thread( pro ); 136 137 Thread t3 = new Thread( con ); 138 Thread t4 = new Thread( con ); 139 140 t.start(); 141 t2.start(); 142 t3.start(); 143 t4.start(); 144 145 } 146 }
8、JDK5对锁的升级
在学习生产者和消费者的时候在使用等待和唤醒机制的时候,发生了本方唤醒本方的。这样导致程序的效率降低。
在jdk5中对等待唤醒以及锁的机制进行了升级。
锁的升级:
在jdk5之前同步中获取锁和释放锁都是隐式的。把隐式的锁变成了显示由程序员自己手动的获取和释放。
jdk5对锁的描述:采用的接口来描述。在java.util.concurrent.locks包下有接口Lock它是专门放负责描述锁这个事物。
Lock锁代替了同步代码块。在多线程中能够使用同步代码块的地方都可以使用Lock接口来代替。
当我们使用Lock接口代替同步代码块的时候,就需要程序员手动的来获取锁和释放锁,如果在程序中出现了获取锁之后,没有释放锁,导致其他程序无法进入这个同步语句中。需要使用try-finally语句在finally中书写释放锁的代码。
在Lock接口中lock方法和unLock方法分别是获取锁和释放锁。
1 //使用JDK5的锁完成售票的例子 2 import java.util.concurrent.locks.*; 3 class Ticket implements Runnable 4 { 5 int num = 100; 6 7 //创建锁对象 8 Lock loc = new ReentrantLock(); //多态了 9 10 public void run() 11 { 12 while( true ) 13 { 14 //获取锁 15 loc.lock(); 16 try 17 { 18 if( num > 0 ) 19 { 20 System.out.println(Thread.currentThread().getName()+"............."+num); 21 num--; 22 } 23 } 24 finally 25 { 26 //释放锁 27 loc.unlock(); 28 } 29 } 30 } 31 } 32 33 class LockDemo 34 { 35 public static void main(String[] args) 36 { 37 //创建线程任务 38 Ticket t = new Ticket(); 39 40 //创建线程对象 41 Thread tt = new Thread(t); 42 Thread tt2 = new Thread(t); 43 Thread tt3 = new Thread(t); 44 Thread tt4 = new Thread(t); 45 46 //开启线程 47 tt.start(); 48 tt2.start(); 49 tt3.start(); 50 tt4.start(); 51 } 52 }
9、JDK5对监视器方法的升级
JDK5对同步的锁进行了升级,同时还对等待唤醒机制也进行了升级。
在JDK5之前等待唤醒机制的方法和当前锁绑定在一起。在编写生产者和消费者任务的时候,由于消费者和生产者使用的同一个锁,那么就会导致使用当前这个锁来唤醒在这个锁上等待的线程的时,就可能发生生产者唤醒生产者或者消费者唤醒消费者。
到了JDK5之后,锁被Lock接口来描述,等待唤醒的方法也被重新封装成一个类。专门使用一个类负责等待唤醒。JDK5中使用Condtion接口来专门负责描述等待唤醒机制。Condition代替了jdk5之前的Object类中的wait、notify、notifyAll方法。await方法代替了Object中wait方法、signal代替的notify、signalAll代替notifyAll方法。
当我们需要使用等待唤醒的时候,就需要获取到Condition对象,然后把这个Condition对象和Lock接口绑定在一起即可。这样就可以使用一个Lock下面绑定多个Condition。在Lock接口有个方法newCondition这个方法就可以让一个Condtion对象和Lock接口对象绑定在一起了。
10、使用JDK完成生产者消费者案例
1 //使用Lock代替同步,使用Condition接口代替等待和唤醒 2 import java.util.concurrent.locks.*; 3 class Resource 4 { 5 Object[] objs = new Object[1]; 6 //记录存入的商品编号 7 int num = 1; 8 9 //获取Lock锁对象 10 Lock loc = new ReentrantLock(); 11 //定义生产者的等待唤醒对象(监视器对象 Condition) 12 Condition pro_con = loc.newCondition(); 13 //定义消费者者的等待唤醒对象(监视器对象 Condition) 14 Condition con_con = loc.newCondition(); 15 16 public void put( Object obj ) 17 { 18 //获取锁 19 loc.lock(); 20 try 21 { 22 //判断有没有商品 23 if( objs[0] !=null ) 24 { 25 //让生产者等待 26 try{pro_con.await();}catch(Exception e){} 27 } 28 objs[0] = obj +"" + num; 29 num++; 30 System.out.println(Thread.currentThread().getName()+"........"+objs[0]); 31 //唤醒消费者 32 con_con.signal(); 33 34 } 35 finally 36 { 37 loc.unlock(); 38 } 39 40 } 41 public void get() 42 { 43 //获取锁 44 loc.lock(); 45 try 46 { 47 //判断有没有商品 48 if( objs[0] == null ) 49 { 50 //让消费者等待 51 try{con_con.await();}catch(Exception e){} 52 } 53 54 System.out.println(Thread.currentThread().getName()+"================="+objs[0]); 55 objs[0]=null; 56 //唤醒生产者 57 pro_con.signal(); 58 } 59 finally 60 { 61 loc.unlock(); 62 } 63 } 64 65 } 66 class Producer implements Runnable 67 { 68 private Resource r; 69 public Producer( Resource r ) 70 { 71 this.r = r; 72 } 73 public void run() 74 { 75 while( true ) 76 { 77 r.put("方便面"); 78 } 79 } 80 } 81 82 class Consumer implements Runnable 83 { 84 private Resource r; 85 public Consumer( Resource r ) 86 { 87 this.r = r; 88 } 89 public void run() 90 { 91 while( true ) 92 { 93 r.get(); 94 } 95 } 96 } 97 98 99 class LockConditionTest 100 { 101 public static void main(String[] args) 102 { 103 Resource r = new Resource(); 104 105 Producer pro = new Producer(r); 106 Consumer con = new Consumer(r); 107 108 Thread t = new Thread(pro); 109 Thread t2 = new Thread(pro); 110 Thread t3 = new Thread(con); 111 Thread t4 = new Thread(con); 112 113 t.start(); 114 t2.start(); 115 t3.start(); 116 t4.start(); 117 } 118 }