上一篇文章: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关键字
- 同步代码块:
synchronized (对象){ // 需要被同步的代码; }
- 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、如何解决呢?
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中。
既然叫同步锁,肯定有释放锁和不会释放锁的操作了。
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的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 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用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继续消费}
就说这么一个例子,还有点题会分享到下一篇文章里。