转载请声明出处:http://blog.csdn.net/zhongkelee/article/details/45892177
说在前面
在介绍java的多线程技术之前,我们先来看一下几个概念。
硬盘:持久化数据存储设备(寻道,磁盘磁头,运算速度慢)
内存:临时性数据存储设备(寻址,颗粒晶片,运算速度快)<-- CPU过来运算
进程:就是应用程序在内存中分配的空间。(正在运行中的程序)
线程:是进程中负责程序执行的执行单元。也成为执行路径,控制单元。
进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。一个进程中至少有一个线程在负责该进程的运行。如果一个进程中出现了多个线程,就称该程序为多线程程序。
举例:运动场--鸟巢。水立方。
多线程技术:解决多部分代码同时执行的需求。可以合理地使用cpu资源。
单核cpu:
相比多核,单核不具备真正的线程级并行能力。
比如在单核上开了多个线程,这多个线程是会分时使用CPU的,在某一时刻,单核只可能为某一个线程服务,不存在并行。
只是cpu以ms级甚至更低的时间片的方式在做非常快速的切换,让你感觉像是同时执行多个线程。且切换具有不确定性。
此外,此时的线程也都是串行程序,不涉及并行算法,不涉及并行计算,本质仍是串行的。
多核cpu:
多核的并行处理是真的。
假设有4个核,不开超线程,那么同时可以执行4个线程,是真正的并行执行。
由于并行执行,也引入了一些新的单核不存在的问题,比如负载均衡、优先级调度等等。这就是真的并行计算的范畴了。
所以,论并行“能力”,多核是要强的,而单核的单线程能力往往要比多核里面的核要强。
但“能力”是一回事,真正用起来后的“性能”是另一回事。未经并行设计的程序/软件跑起来很难真正用满多核的性能,在多核上跑单线程程序效果肯定不好,此外,即使一个程序线程数很多,如果这些线程无法并发,那依然不行。所以并行计算是机遇与挑战并存的。
多线程的运行是根据cpu的切换完成的。怎么切换cpu说的算,所以多线程运行有随机性。
cpu随机性原理:cpu的快速切换造成的,哪个线程获取到了cpu的执行权,哪个线程就执行。
jvm中的多线程
jvm中至少有两个线程:
一个是负责自定义代码运行的。这个从main方法开始执行的线程称之为主线程。主线程执行的代码都在main方法中。
一个是负责垃圾回收的。当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样,会出现主线程中的代码执行会停止,会去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。
至于某一时刻到底哪个线程在运行,cpu说了算。
我们来看下面这段代码:
class Demo{ //定义垃圾回收方法 public void finalize(){ System.out.println("demo ok"); } } class FinalizeDemo{ public static void main(String[] args){ new Demo(); new Demo(); new Demo(); System.gc();//启动垃圾回收器。垃圾回收线程是后台线程(守护进程),随着其他线程的结束而自动结束。 System.out.println("Hello Threads"); } }运行结果如下:
通过实验,会发现每次结果不一定相同,那是因为cpu的随机性切换造成的。而且每一个线程都有自己运行的代码内容。这个称之为线程的任务。
我们之所以创建一个线程就是为了去运行指定的任务代码。而线程的任务都封装在特定的区域中。
比如:
主线程运行的任务都定义在main方法中,这个是jvm定义的。
垃圾回收线程在收垃圾时都会运行对象自定义的finalize方法。
Thread类中的run()方法,用于存储自定义线程要运行的代码。
创建线程方式一:继承Thread类
我们先来看一个单线程的例子。下面一段代码,除了垃圾回收线程之外,只有主线程。所以d1.show执行完之后,才会执行d2.show。
class Demo{ private String name; Demo(String name){ this.name = name; } public void show(){ for(int i = 0; i < 10; i++) System.out.println(name+"..."+i); } } class ThreadDemo{ public static void main(String[] args){ Demo d1 = new Demo("张三"); Demo d2 = new Demo("麻子"); d1.show(); d2.show(); } }
输出为:
那么,如何开辟一个执行路径呢?
通过查阅API文档 java.lang.Thread类。该类的描述中有创建线程的两种方式。
第一种就是继承Thread类。
步骤:
1.继承Thread类。
2.覆盖run方法。
3.创建子类对象就是创建线程对象。
4.调用Thread类中的start方法就可以执行线程。并会调用run方法。
class Demo extends Thread{ private String name; public Demo(String name){ super();//父类构造函数Thread(),会生成gname线程名称""Thread-n""。可以使用父类的getName()方法获取。 this.name = name; } //覆盖run方法 public void run(){ for (int x = 1; x <= 40; x++){ System.out.println(Thread.currentThread().getName()+"...."+name+"...."+x);//如果仅仅是getName(),获得的只是当前对象的gname,不一定是当前正在运行的线程的gname。 } } /*public void start(){//覆盖了Thread类的start方法后,就不能够启动线程了。这时只有main主线程运行。 //super.start();//但如果有这句,就可以启动线程了。使用Thread类的start方法,即可开启。 this.run();//这里,被覆盖的start()方法只有运行run的能力,没有启动线程的能力。这时就只是普通的对象在调用自己的成员方法而已。 }*/ } class ThreadDemo{ public static void main(String[] args){//主线程栈->main方法->程序入口 Demo d1 = new Demo("张三");//创建d1线程对象。名称gname被设置为 Thread-0,可以通过调用Thread类的getName()方法获取该名称。 Demo d2 = new Demo("麻子");//创建d2线程对象。名称gname被设置为 Thread-1,子类自定义名称name被设置为 麻子。 d1.start();//start():两件事:1.开启d1线程栈,2.调用run方法。(主线程开辟的d1、d2线程栈区) d2.start();//主线程开启d2线程栈。但是当前时刻是否执行该线程该run方法,cpu说了算。 for (int x = 1; x < 40; x++){ System.out.println(Thread.currentThread().getName()+"----"+x); } } }
程序输出:
start()开启线程后,都会执行run方法,说明run方法中存储的是线程要运行的代码。所以,记住,自定义线程的任务代码都存储在run方法中。(而主线程的代码存储在main方法中)
返回当前线程的名称:Thread.currentThread().getName()
线程的名称是由:Thread—编号定义的。编号从0开始。
内存图解:
调用Thread类start和调用run方法的区别?
调用Thread类start会开启线程,让开启的线程去执行run方法中的线程任务。
直接调用run方法,线程并未开启,去执行run方法的只有主线程main。
多线程的五种状态
被创建:start()
运行:具备执行资格,同时具备执行权;
冻结:sleep(time)-sleep(time over),wait()—notify()唤醒;线程释放了执行权,同时释放执行资格;
临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
消亡:stop()
多线程示例一:售票示例
卖票的代码需要被多个线程执行,所以要将这些代码定义在线程任务中。run方法。代码如下:
<span style="font-size:14px;">class SaleTicket extends Thread{ private int tickets = 100; public void run(){ while(true){ if (tickets > 0){ System.out.println(Thread.currentThread().getName()+"...."+tickets--); } } } } class TicketDemo{ public static void main(String[] args){ SaleTicket t1 = new SaleTicket(); SaleTicket t2 = new SaleTicket(); SaleTicket t3 = new SaleTicket(); SaleTicket t4= new SaleTicket(); t1.start(); t2.start(); t3.start(); t4.start(); } }</span>
创建四个线程。会创建400张票。不合适,不建议票变成静态的,所以如何共享这100张票。需要将资源和线程分离。
到api文档中查阅了第二种创建线程的方式,实现Runnable接口。
创建线程方式二:实现Runnable接口
步骤:
1.定义一个类实现Runnable。
2.覆盖Runnable接口中的run方法,将线程要运行的任务代码存储到该方法中。
3.通过Thread类创建线程对象,并将实现了Runnable接口的对象作为Thread类的构造函数的参数进行传递。
4.调用Thread类的start方法,开启线程。
改进了的售票示例的代码如下:
class SaleTicket implements Runnable{ private int tickets = 100; public void run(){ while(true){ if (tickets > 0){ System.out.println(Thread.currentThread().getName()+"...."+tickets--); } } } } class TicketDemo2{ public static void main(String[] args){ //线程任务对象 SaleTicket t = new SaleTicket(); //创建四个线程。通过Thread类对象。 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(); } }
实现Runnable接口的好处:
1.避免了继承Thread类的单继承的局限性。
2.Runnable接口的出现更符合面向对象,将线程任务单独进行对象的封装。
3.Runnable接口的出现降低了线程对象和线程任务的耦合性。(解耦)
综上所述,以后创建线程都是用第二种方式。
解决多线程安全问题:同步--synchronized
上述售票代码的运行,会出现如下的错误情况:
产生的原因:
1.线程任务中有处理到共享的数据。
2.线程任务中有多条对共享数据的操作。一个线程在操作共享数据的过程中,还没有执行完,其他线程参与了运算,造成了数据的错误。
解决的思想:
只要保证多条操作共享数据的代码在某一时间段,被一条线程所执行,在执行期间不允许其他线程参与运算。
咋保证呢?
java中提供了一个解决方式:同步代码块。
格式:
synchronized(对象) // 任意对象都可以。这个对象就是锁,也称为监视器。
{
需要被同步的代码。
}
再一次改进后的售票示例代码如下:
class SaleTicket implements Runnable{ private int tickets = 100; Object obj = new Object(); public void run(){ while(true){ /*在线程任务中,只有操作到共享数据的部分需要使用同步代码块*/ synchronized(obj){//obj->同一个锁 if (tickets > 0){ try{Thread.sleep(10);}catch(InterruptedException e){}//让线程到这里稍微停一下,这样可以看到cup切换线程的过程。 System.out.println(Thread.currentThread().getName()+"...."+tickets--); } } } } } class TicketDemo3{ public static void main(String[] args){ SaleTicket t = new SaleTicket(); 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(); } }
运行结果就正确了:
同步在目前情况下保证了一次只能有一个线程在执行。其他线程进不来。(火车上的厕所)。这就是同步的锁机制。
好处:解决了多线程的安全问题。
弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁。
有可能出现这样一种情况:
多线程安全问题出现后,加入了同步机制,没有想到,安全问题依旧!咋办?
这时,肯定是同步出了问题。
只要遵守了同步的前提,就可以解决。
同步的前提:
多个线程在同步中必须使用同一个锁。这才是对多个线程同步。也就是说,必须是多个线程在同一个锁上处理同一个共享数据。
练习:
两个储户,到同一个银行存钱,每个人存了3次,一次100元。
1.描述银行。
2.描述储户任务。
分析多线程是否存在安全隐患。
1.线程任务中是否有共享的数据。
2.是否有多条操作共享数据的代码。
发现该程序中是有安全隐患的:
加上同步以后,问题得以解决:
class Bank{ private int sum; private Object obj = new Object(); public void add(int n){ synchronized(obj){ sum = sum + n; System.out.println("sum = "+sum); } } } class Customer implements Runnable{ private Bank b = new Bank(); public void run(){ for (int x = 0; x < 3; x++){ b.add(100); } } } class ThreadTest{ public static void main(String[] args){ //1.创建任务对象 Customer c = new Customer(); //2.创建线程对象 Thread t1 = new Thread(c); Thread t2 = new Thread(c); //3.开启线程 t1.start(); t2.start(); } }
运行结果:
匿名线程对象示例
实际中创建线程,可以使用匿名对象的方式,代码如下:
class ThreadTest{ public static void main(String[] args)throws InterruptedException{ new Thread(){ public void run(){ for (int x = 1; x <= 50; x++) System.out.println(Thread.currentThread().getName()+"...x = "+x); } }.start(); Runnable r = new Runnable(){ public void run(){ for (int x = 1; x <= 50; x++) System.out.println(Thread.currentThread().getName()+"y = "+x); } }; //new Thread(r).start(); Thread t = new Thread(r); t.start(); t.join(); for (int x = 1; x <= 50; x++) System.out.println(Thread.currentThread().getName()+"z = "+x); //-----------如果错误,错误发生在哪一行--------- class Test implements Runnable{ public void run(Thread t){ } } //-->错误在第一行,应该被abstract修饰。 //-->一个类如果实现了接口,但是接口的抽象方法没有全被覆盖,该类应该是抽象类。 //------------------结果是什么?------------------- new Thread(new Runnable(){ public void run(){ System.out.println("runnable run"); } }){ public void run(){ System.out.println("subThread run"); } }.start(); //-->以子类为主。打印:subThread run } }
同步函数
同步函数其实就是在函数上加上了同步关键字进行修饰。
同步的表现形式有两种:
1.同步代码块(明锁)
2.同步函数(this锁)。
同步函数使用的锁是什么呢?
函数需要被对象调用,哪个对象不确定,但是都用this来表示。同步函数使用的锁就是this。
class Bank{ private int sum; public synchronized void add(int n){ sum += n; try{Thread.sleep(10);}catch(Exception e){} System.out.println("sum = "+sum); } } class Customer implements Runnable{ private Bank b = new Bank(); public void run(){ for(int x = 0; x < 3; x++){ b.add(100); } } } class BankDemo{ public static void main(String[] args){ Customer c = new Customer(); Thread t1 = new Thread(c); Thread t2 = new Thread(c); t1.start(); t2.start(); } }
如何验证同步函数使用的锁就是this呢?
验证需求:
启动两个线程。一个线程负责执行同步代码块(使用明锁)。另一个线程使用同步函数(使用this锁)。两个线程执行的任务是一样的,都是卖票。如果他们没有使用相同的锁,说明他们没有同步,会出现数据错误。
怎么让一个线程一直在同步代码块中,一个线程在同步函数中呢?可以通过切换的方式。设置flag标记位。
我们还以卖票示例,实验结果发生错误:
而如果将同步代码块中的obj锁改成this锁,结果就是正确的,说明此时两个线程是同一个锁。这就验证了同步函数使用的是this锁。正确代码如下:
class SaleTicket implements Runnable{ private int tickets = 100; //定义一个boolean标记。 boolean flag = true; Object obj = new Object(); public void run(){ if (flag) while(true){ synchronized(this){//obj-->this if (tickets > 0){ try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+"...code..."+tickets--); } } } else while(true) sale(); } public synchronized void sale(){ if (tickets > 0){ try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+"...func..."+tickets--); } } } class ThisLockDemo{ public static void main(String[] args) throws InterruptedException{ SaleTicket s = new SaleTicket(); Thread t1 = new Thread(s); Thread t2 = new Thread(s); t1.start(); Thread.sleep(10); s.flag = false; t2.start(); } }
运行结果:
那如果同步函数被static修饰呢?
注意:字节码文件进入内存后,除了在方法区中进行分布以外,还在堆中生成了一个自己的对象。比如Demo.class字节码文件对象。每个字节码文件对象在堆内存中都是唯一的。也就是说,类进入内存后就有一个对象,这个对象就是字节码文件对象。后期根据new产生的对象都是类的实例,都是根据那个唯一的字节码文件对象在堆内存中创建的。后面说到有关java的反射技术时,会详细介绍。
static方法随着类加载,这时不一定有该类的对象。但是一定有一个该类的字节码文件对象。这个对象简单的表示方式就是 类名.class (java.lang.Class类)
所以,被static修饰的同步函数的锁就是类名.class对象。
class SaleTicket implements Runnable{ private static int tickets = 100; boolean flag = true; public void run(){ if (flag) while(true){ synchronized(SaleTicket.class){ if (tickets > 0){ try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+"...code..."+tickets--); } } } else while(true) sale(); } public static synchronized void sale(){ if(tickets > 0){ try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+"...func..."+tickets--); } } } class StaticLockDemo{ public static void main(String[] args) throws InterruptedException{ SaleTicket t = new SaleTicket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); Thread.sleep(10); t.flag = false; t2.start(); } }
同步函数和同步代码块有什么区别呢?
同步代码块使用的是任意的对象作为锁。
同步函数只能使用this作为锁。
如果说,一个类中只需要一个锁,这时可以考虑同步函数,使用this,写法简单。
但是,一个类中如果需要多个锁,还有多个类中使用同一个锁,这时只能使用同步代码块。
建议使用同步代码块。
单例模式的并发访问
饿汉式。相对于多线程并发,比较安全!因为虽然有共享数据,但是没有对共享数据有多条操作。
class Single{ private static final Single SINGLE_INSTANCE = new Single(); private Single(){} public static Single getInstance(){ //这里如果有判断语句,注意线程安全。 return SINGLE_INSTANCE; } }
懒汉式。延迟加载模式。在多线程并发访问时,会出现线程安全问题。
加了同步就可以解决。无论是同步函数,还是同步代码块都行。
但是,效率低了。怎么解决效率问题呢?
可以通过对单例对象的双重if判断的形式解决!
class Single1{ private static Single s = null; private Single(){} public static Single getInstance(){ if (s == null){ synchronized (Single.class){ if (s == null){ s = new Single(); } } } return s; } } /*单例延迟加载模式,并且多线程安全,解决效率问题。*/ class Single2{ private static final Single s = null; private Single(){} public static Single getInstance(){ if (s == null){ lock.lock(); try{ if (s == null) s = new Single(); }finally{ lock.unlock(); } } return s; } } class Demo implements Runnable{ public void run(){ Single.getInstance(); } } class ThreadSingleTest{ public static void main(String[] args){ } }
面试:
延迟加载单例模式-->函数上加同步解决线程安全问题-->使用的锁是类名.class,字节码对象,比如Single.class-->双重判断解决线程并发访问的效率问题。
死锁
场景一:同步嵌套。
场景二:所有线程全部冻结,wait().
代码示例:
class SaleTicket implements Runnable{ private int tickets = 100; boolean flag = true; Object obj = new Object(); public void run(){ if (flag){ while(true){ synchronized (obj){//obj lock sale(); } } } else while(true) sale(); } public synchronized void sale(){//this lock synchronized(obj){ if (tickets > 0){ try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+"...func..."+tickets--); } } } } class DeadLockDemo{ public static void main(String[] args)throws InterruptedException{ SaleTicket t = new SaleTicket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); Thread.sleep(10); t.flag = false; t2.start(); } } /*---------------------------------------------------------*/ class Task implements Runnable{ private boolean flag; public Task(boolean flag){ this.flag = flag; } public void run(){ if (flag){ while (true){ synchronized(MyLock.LOCKA){ System.out.println("if......locka"); synchronized(MyLock.LOCKB){ System.out.println("if......lockb"); } } } } else{ while(true){ synchronized(MyLock.LOCKB){ System.out.println("else......lockb"); synchronized(MyLock.LOCKA){ System.out.println("else......locka"); } } } } } } class MyLock{ public static final Object LOCKA = new Object(); public static final Object LOCKB = new Object(); } class DeadLockTest{ public static void main(String[] args){ Task t1 = new Task(true); Task t2 = new Task(false); new Thread(t1).start(); new Thread(t2).start(); } }
多线程间的通信:生产者消费者示例
现实中,我们经常遇到多个线程都在处理同一个资源,但是处理的任务却不一样。
这里典型的例子就是生产者消费者问题。
我们先来看单生产者单消费者问题:
这里程序会出现还没生产就消费的问题,于是在代码中加了同步得以解决。代码如下:
//描述资源 class Res{ private String name; private int count = 1; //提供一个给商品赋值的方法 public synchronized void set(String name){//加了同步之后,不会出现还没生产就消费的情况了。 this.name = name + "--" + count; count++; System.out.println(Thread.currentThread().getName()+"....生产者...."+this.name); } //提供一个获取商品的方法 public synchronized void get(){ System.out.println(Thread.currentThread().getName()+"......消费者......"+this.name); } } //生产者 class Producer implements Runnable{ private Res r; Producer(Res r){ this.r = r; } public void run(){ while(true) r.set("面包"); } } //消费者 class Consumer implements Runnable{ private Res r; Consumer(Res r){ this.r = r; } public void run(){ while(true) r.get(); } } class ProducerConsumerDemo{ public static void main(String[] args){ //创建资源 Res r = new Res(); //创建两个任务 Producer pro = new Producer(r); Consumer con = new Consumer(r); //创建线程 Thread t1 = new Thread(pro); Thread t2 = new Thread(con); //开启线程 t1.start(); t2.start(); } }
但是出现了连续的生产没有消费的情况,和需求生产一个就消费一个的情况不符。
如何实现生产一个就消费一个呢?
等待唤醒机制
Object监视器方法:
wait():该方法可以让线程处于冻结状态,并将线程临时存储到线程池中。
notify():唤醒指定线程池中的任意一个线程。(具体唤醒哪个线程是随机的,允许空唤醒)
notifyAll():唤醒指定线程池中的所有线程。
注意:只要使用等待唤醒机制,都应该使用循环判断。
class Res{ private String name; private int count = 1; private boolean flag;//定义标记(java中默认值是false) //提供了给商品赋值的方法。(该同步函数用的锁或者说监视器是this) public synchronized void set(String name){ if (flag)//判断标记为true,执行wait等待;标记为false,就生产。 try{this.wait();}catch(InterruptedException e){} this.name = name + "--" + count; count++; System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); flag = true;//生产完毕,将标记改为true。 this.notify();//唤醒消费者。 } //提供一个获取商品的方法。 public synchronized void get(){ if (!flag) try{this.wait();}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+".....消费者....."+this.name); flag = false;//将标记改为false。 this.notify();//唤醒生产者。 } } class Producer implements Runnable{ private Res s; Producer(Res s){ this.s = s; } public void run(){ while(true) s.set("面包"); } } class Consumer implements Runnable{ private Res s; Consumer(Res s){ this.s = s; } public void run(){ while(true) s.get(); } } class ProducerConsumerDemo2{ public static void main(String[] args){ Res r = new Res(); Producer pro = new Producer(r); Consumer con = new Consumer(r); Thread t1 = new Thread(pro); Thread t2 = new Thread(con); t1.start(); t2.start(); } }
使用了等待唤醒机制后,单生产者单消费者问题得以解决。运行结果如下 :
wait()、notify()、notifyAll()方法必须使用在同步代码块或者同步函数中,因为它们是用来操作同一同步锁上的线程的状态的(即操作同一个监视器对象上线程状态的一组方法)。必须要明确到底操作的是哪个锁上的线程。在使用这些方法时,必须标识它们所属于的锁。标识方式就是锁对象.wait(); 锁对象.notify(); 锁对象.notifyAll();相同锁的notify(),可以获取相同锁的wait()。
wait(),notify(),notifyAll()用来操作线程为什么定义在了Object类中?
1.这些方法存在于同步(synchronized)中。
2.使用这些方法时必须要标识所属的同步的锁。
3.锁可以是任意对象,所以任意对象调用的方法一定定义在Object类中。
其实这些方法是监视器方法,监视器就是锁,锁可以是任意对象,任意对象调用的方式一定定义在Object中。
备注:
synchronized 同步函数或同步代码块 可持任意对象作为同步锁 同一锁对象 同一对象监视器 同一组监视器方法 同一线程池 同一等待集 自动释放锁的功能
同一锁对象所对应的wait,notify,notifyAll监视器方法操作该锁对象上的若干线程
使用wait,notify,notifyAll监视器方法必须标识所属的同步锁对象,必须用在synchronized中
wait释放cpu执行权,释放锁。
sleep释放cpu执行权,不释放锁。
多生产者多消费者问题
接下来,我们来看多生产多消费的例子。
如果我们仅仅是多开一对线程,代码会出现以下两个问题:
问题1:重复生产、重复消费。
原因:经过复杂(冻结、临时阻塞、运行)的分析,发现被唤醒的线程没有判断标记就开始工作(生产or消费)了。导致了重复的生产和消费的发生。因为使用的if判断标记,所以从wait处唤醒后直接就向下执行代码。
解决:被唤醒的线程必须判断标记。使用while循环搞定。
问题2:死锁了。所有的线程都处于冻结状态。
原因:本方线程在唤醒时,又一次唤醒了本方线程。而本方线程循环判断标记,又继续wait,而导致所有的线程都wait了。
解决:希望本方如果唤醒了对方线程,就可以解决了。可以使用notifyAll()方法。
疑问:那不是全唤醒了吗?
回答:是的。既有本方,又有对方。但是本方醒后,会判断标记继续wait。这样对方就有线程可以执行了。
代码如下:
class Res{ private String name; private int count = 1; private boolean flag; public synchronized void set(String name){ while(flag) try{this.wait();}catch(InterruptedException e){}//每次醒来都应该再次判断标记。所以用while,安全。 this.name = name + "--" + count; count++; System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); flag = true; this.notifyAll();//唤醒所有等待线程(包括本方线程) } public synchronized void get(){ while(!flag) try{this.wait();}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+".....消费者....."+this.name); flag = false; this.notifyAll(); } } class Producer implements Runnable{ private Res s; Producer(Res s){ this.s = s; } public void run(){ while(true) s.set("面包"); } } class Consumer implements Runnable{ private Res s; Consumer(Res s){ this.s = s; } public void run(){ while(true) s.get(); } } class ProducerConsumerDemo3{ public static void main(String[] args){ Res r = new Res(); 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(); t1.start(); t2.start(); t3.start(); } }
该程序已经实现了多生产多消费。但是有些小问题,效率有点低。因为notifyAll也唤醒了本方,做了不必要的标记判断。而且唤醒了对方全部,也不太合适。如何解决效率问题呢?
JDK1.5: Lock接口、Condition接口
解决多生产多消费的效率问题。使用了JDK.5 java.util.concurrent.locks包中的对象。
Lock接口:它的出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,通常需要定义到finally代码块中。
同步代码块或者同步函数的锁操作是隐式的。
JDK1.5 Lock接口,按照面向对象的思想,将同步和锁单独封装成了一个对象。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口就是同步的替代。将线程中的同步更换为Lock接口的形式。
注意:不使用synchronized块结构锁就失去了使用synchronized方法和语句时会出现的锁自动释放功能,需使用finally代码块确保执行unlock释放锁。
替换完运行失败了。无效的监视器状态异常:IllegalMonitorStateException。这是为什么?
因为wait没有了同步区域,没有了所属的同步锁。那么就不能够使用绑定在同步锁对象上的监视器方法(wait,notify,notifyAll)。同步升级了。其中锁已经不再是任意对象,而是Lock类型的对象。那么和任意对象(同步锁)绑定的监视器方法(监视该锁上线程的状态的方法),是不是也升级了,有专门和Lock类型锁绑定的监视器方法呢?答案是有的!
查阅api:
Condition接口:它的出现替代了Object中的wait,notify,notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以和任意锁进行组合。
await():wait()
signal():notify()
signalAll():notifyAll()
Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。
但是,问题依旧,一样唤醒了本方,效率仍旧低!
Condition将Object监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用。Lock可以支持多个相关的Condition对象。
以前监视器方法封装到每一个对象(Object)中。现在将监视器方法封装到了Condition对象中。
方法名为: await signal signalAll
那监视器对象Condition如何和Lock绑定呢?
可以通过Lock接口的newCondition()方法完成。它返回绑定到此Lock实例的新Condition实例。
JDK1.4:
监视器方法-->Object同步锁对象。
监视器与锁绑定,锁也就是监视器,一个锁上只能有一组监视器。
要想一组监视生产者,一组监视消费者,从而让生产者等待的同时唤醒消费者,那么需要将两个锁嵌套,才可以两组监视器,但是这样容易发生死锁。所以我们使用了while、notifyAll的组合,但是效率偏低。
JDK1.5:
监视器方法-->Condition对象-->Lock对象。
监视器方法封装在Condition对象中,一个Lcok锁对象可以绑定多个监视器对象。
这样就可以一个监视器对象监视生产者,一个监视器对象监视消费者,生产的await与消费的signal搭配使用,就相当于同一个锁上有了两个线程池。这样就实现了本方只唤醒对方中的一个。
使用JDK1.5 Lock接口改善效率问题,多生产者多消费者完整示例:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.Condition; class Res{ private String name; private int count = 1; private boolean flag; private Lock lock = new ReentrantLock();//创建一个锁对象 //private Condition con = lock.newCondition();//通过已有的锁对象获取该锁上的监视器对象,使得监视器和锁绑定。 //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。 private Condition producer_con = lock.newCondition(); private Condition consumer_con = lock.newCondition(); public void set(String name){ lock.lock(); try{ while(flag) try{producer_con.await();}catch(InterruptedException e){}//只要醒了,就判断,应该使用循环判断。 this.name = name +"--"+ count; count++; System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name); flag = true; //this.notifyAll(); //con.signalAll(); consumer_con.signal();//生产完毕,应该唤醒一个消费者来消费。 }finally{ lock.unlock(); } } public void get(){ lock.lock(); try{ while(!flag) try{consumer_con.await();}catch(InterruptedException e){} System.out.println(Thread.currentThread().getName()+".....消费者5.0....."+this.name); flag = false; //this.notifyAll(); //con.signalAll(); producer_con.signal();//消费完后,应该唤醒一个生产者。 }finally{ lock.unlock(); } } } class Producer implements Runnable{ private Res s; Producer(Res s){ this.s = s; } public void run(){ while(true) s.set("面包"); } } class Consumer implements Runnable{ private Res s; Consumer(Res s){ this.s = s; } public void run(){ while(true) s.get(); } } class NewProducerConsumerDemo{ public static void main(String[] args){ Res r = new Res(); 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(); t1.start(); t2.start(); t3.start(); } }
JDK1.5的API文档中,Condition接口示例:
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
练习:
1.搞定妖的问题。
分析:1.共享数据 2.线程任务中有多条操作共享数据的代码。
加了同步,问题依旧。看同步的前提!多个线程、同一个锁。将输入输出都加了同步,问题解决
2.name和sex是私有的。需要在Res类中对外提供访问name和sex的方法。这个可以参照生产者消费者ProducerConsumerDemo.java。已解决。
3.实现间隔输出,使用等待唤醒机制。ProducerConsumerDemo.java
一般情况下需要判断条件。
import java.util.concurrent.locks.*; /*实在找不到合适的锁,就自己定义一个*/ class MyLock{ //自定义锁 public static final Object obj = new Object(); } class Res{ private String name; private String sex; private boolean flag; private Lock lock = new ReentrantLock(); private Condition con = lock.newCondition(); public void set(String name, String sex){ lock.lock(); try{ while(flag) try{con.await();}catch(InterruptedException e){} this.name = name; this.sex = sex; flag = true; con.signal(); }finally{ lock.unlock(); } } public void get(){ lock.lock(); try{ while(!flag) try{con.await();}catch(InterruptedException e){} System.out.println(this.name+"......"+this.sex); flag = false; con.signal(); }finally{ lock.unlock(); } } } class Input implements Runnable{ private Res s; Input(Res s){ this.s = s; } public void run(){ int x = 0; while(true){ if (x == 0) s.set("张三","男男男男男男"); else s.set("rose","woman"); x = (x+1)%2; } } } class Output implements Runnable{ private Res s; Output(Res s){ this.s = s; } public void run(){ while(true) s.get(); } } class Test{ public static void main(String[] args){ Res r = new Res(); Input in = new Input(r); Output out = new Output(r); Thread t0 = new Thread(in); Thread t1 = new Thread(out); t0.start(); t1.start(); } }
wait()和sleep()区别
相同:可以让线程处于冻结状态。
不同:
1.wait(): 可以指定时间,也可以不指定。
sleep():必须指定时间。
2.wait(): 释放CPU资源,释放锁。
sleep():释放CPU资源,不释放锁。
同步里面只能有一个线程。但是同步中如果有wait(),会出现多线程情况,但是不用担心数据错误的问题。是因为,在同步中的临时阻塞状态的线程要想运行,必须要持有锁。只有持有锁的那个线程具有执行权。执行完再把锁放掉,其他线程才有资格执行。
所以记住,在同步之中,假设有n个线程从冻结状态恢复到了临时阻塞状态,即具备了执行资格,此时如果锁没有被任何线程持有,并且CPU切到这n个线程其中的一个上,那么这个线程同时具备执行权和锁。只有拿到锁的这个线程才能运行。 所以即使都醒了,也不怕,因为任意时刻只有一个线程可以持有锁,持有锁的线程才能执行。
同步中,具备执行资格的活着的线程可以有多个,但是真正具备执行权的运行的线程只有一个。谁持有着锁,谁就运行。
synchronized(obj){ obj.wait();//t0, t1, t2,...,tn争夺执行权,获得执行权的同时获得被t3释放的锁 code... } synchronized(obj){ obj.notifyAll();//t3 }//t3释放锁
异常在多线程中的体现
异常会提示它发生在哪个线程上。
异常会结束线程任务,也就是说可以结束所在线程。
class Demo implements Runnable{ public void run(){ System.out.println(4/0); } } class ThreadExceptionDemo{ public static void main(String[] args)throws Exception{ new Thread(new Demo()).start(); Thread.sleep(10); int[] arr = new int[3]; System.out.println(arr[2]); System.out.println("over"); } }
运行结果:
停止线程方式
方法一:定义循环结束标记(变量)
原理:让run方法结束。
线程任务通常都有循环。因为开启线程就是为了执行需要一些时间的代码。不让任务A苦苦等待任务B的完成才执行。
只要控制住循环,就可以结束run方法,就可以停止线程。
class StopThread implements Runnable{ private boolean flag = true; public void run(){ while(flag){ System.out.println(Thread.currentThread().getName()+"......"); } } public void setFlag(){ this.flag = false; } } class StopThreadDemo{ public static void main(String[] args){ StopThread st = new StopThread(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); t2.start(); int num = 1; for(;;){ if (++num == 50){ st.setFlag(); break; } System.out.println(Thread.currentThread().getName()+"............"+num); } System.out.println("over"); } }
但是,第一种方式会出现死锁的情况,如果线程处于冻结状态,就无法读到定义的标记,也就无法在想要的时刻实现停止线程。如下所示:
class StopThread implements Runnable{ private boolean flag = true; public synchronized void run(){ while(flag){ try{ wait();//t0 t1 }catch(InterruptedException e){ System.out.println(Thread.currentThread().getName()+"......"+e); } System.out.println(Thread.currentThread().getName()+".....++++++"); } } public void setFlag(){ this.flag = false; } } class StopThreadDemo{ public static void main(String[] args){ StopThread st = new StopThread(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); t2.start(); int num = 1; for(;;){ if (++num == 50){ st.setFlag(); break; } System.out.println(Thread.currentThread().getName()+"............"+num); } System.out.println("over"); } }
方法二:使用interrupt(中断)方法
interrupt():中断线程,强制结束线程的冻结状态,并抛出异常。
将线程从冻结状态强制恢复到临时阻塞或运行状态,让线程具备cpu的执行资格,但是强制动作会发生InterruptedException异常,记得要处理。
class StopThread implements Runnable{ private boolean flag = true; public synchronized void run(){ while(flag){ try{ this.wait(); }catch(InterruptedException e){ System.out.println(Thread.currentThread().getName()+"................"+e.toString()); flag = false; } System.out.println(Thread.currentThread().getName()+"......hello"); } } } class StopThreadDemo{ public static void main(String[] args)throws InterruptedException { StopThread st = new StopThread(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); t2.start(); Thread.sleep(10); for(int x = 0; x <= 50; x++){ if (x == 40){ t1.interrupt();//强制唤醒线程,并抛出InterruptedException异常。 t2.interrupt(); } System.out.println(Thread.currentThread().getName()+"......"+x); } System.out.println("main over"); } }
stop()方法已经过时,不再使用。
守护进程
守护进程:即后台进程,当所有前台进程结束,后台进程无论是否执行完,随之结束。当正在运行的线程都是守护进程时,jvm退出。
setDaemon():该方法必须在启动线程前调用。
应用:比如一个输入线程,一个输出线程,可以将输出线程置为守护线程。只要没有输入了,输出线程就自动结束。
class StopThread implements Runnable{ private boolean flag = true; public synchronized void run(){ while(flag){ try{ this.wait(); }catch(InterruptedException e){ System.out.println(Thread.currentThread().getName()+"............"+e.toString()); flag = false; } System.out.println(Thread.currentThread().getName()+"......hello"); } } } class StopThreadDemo2{ public static void main(String[ ] args){ StopThread st = new StopThread(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); t2.setDaemon(true);//将t2设置为守护进程。 t2.start(); for(int x = 0; x <= 50; x++){ if (x == 40){ t1.interrupt(); } System.out.println(Thread.currentThread().getName()+"..."+x); } System.out.println(Thread.currentThread().getName()+" over"); } }
Thread类的一些方法
join():等待该线程终止。临时加入一个线程运算时,可以使用join方法。
class Demo implements Runnable{ public void run(){ for (int x = 1; x <= 40; x++) System.out.println(Thread.currentThread().getName()+"......"+x); } } class JoinDemo{ public static void main(String[] args)throws InterruptedException{ Demo d = new Demo(); Thread t0 = new Thread(d); Thread t1 = new Thread(d); t0.start(); //t0.join();//t0线程申请加入进来运行,当前主线程释放执行权和执行资格,处于冻结状态,等待t0线程终止,主线程再醒过来执行。 //执行这句话的当前线程冻结,抛出执行权。(本例也即主线程冻结) t1.start(); t0.join(); //执行到这句话的主线程释放执行权和执行资格,处于冻结状态。 //临时阻塞的所有线程t0、t1争夺该执行权。 //冻结的主线程等到调用join的t0线程终止才被唤醒,重新获得执行资格抢夺执行权。 for (int x = 1; x <= 40; x++) System.out.println(Thread.currentThread().getName()+"......"+x); } }
toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
setPriority():设置线程优先级(1-10),优先级越大,当前线程获取cpu执行权的几率越高。默认优先级:5
yield():暂停当前正在执行的线程对象。
线程组ThreadGroup:也就是一个集合,后面会讲到。操作线程组,也就是同时操作线程组中的所有线程。
class Demo implements Runnable{ public void run(){ for (int x = 1; x <= 40; x++){ System.out.println(Thread.currentThread().toString()+"......"+x); Thread.yield();//暂停当前正在执行的线程对象,释放执行权。 } } } class PriorityDemo{ public static void main(String[] args)throws InterruptedException{ Demo d = new Demo(); Thread t1 = new Thread(d); Thread t2 = new Thread(d); t1.start(); t2.start(); //t1.join();//等待该线程终止。 Thread.sleep(10); t1.setPriority(Thread.MAX_PRIORITY);//最大优先级:10 t2.setPriority(Thread.MIN_PRIORITY);//最小优先级:1 for (int x = 1; x <= 40; x++) System.out.println(Thread.currentThread().toString()+"......"+x); } }
多线程总结
1,进程和线程的概念。
|--进程:
|--线程:
2,jvm中的多线程体现。
|--主线程,垃圾回收线程,自定义线程。以及他们运行的代码的位置。
3,什么时候使用多线程,多线程的好处是什么?创建线程的目的?
|--当需要多部分代码同时执行的时候,可以使用。
4,创建线程的两种方式。★★★★★
|--继承Thread
|--步骤
|--实现Runnable
|--步骤
|--两种方式的区别?
5,线程的5种状态。
对于执行资格和执行权在状态中的具体特点。
|--被创建:
|--运行:
|--冻结:
|--临时阻塞:
|--消亡:
6,线程的安全问题。★★★★★
|--安全问题的原因:
|--解决的思想:
|--解决的体现:synchronized
|--同步的前提:但是加上同步还出现安全问题,就需要用前提来思考。
|--同步的两种表现方法和区别:
|--同步的好处和弊端:
|--单例的懒汉式。
|--死锁。
7,线程间的通信。等待/唤醒机制。
|--概念:多个线程,不同任务,处理同一资源。
|--等待唤醒机制。使用了锁上的 wait notify notifyAll. ★★★★★
|--生产者/消费者的问题。并多生产和多消费的问题。 while判断标记。用notifyAll唤醒对方。 ★★★★★
|--JDK1.5以后出现了更好的方案,★★★
Lock接口替代了synchronized
Condition接口替代了Object中的监视方法,并将监视器方法封装成了Condition
和以前不同的是,以前一个锁上只能有一组监视器方法。现在,一个Lock锁上可以多组监视器方法对象。
可以实现一组负责生产者,一组负责消费者。
|--wait和sleep的区别。★★★★★
8,停止线程的方式。
|--原理:
|--表现:--中断。
9,线程常见的一些方法。
|--setDaemon()
|--join();
|--优先级
|--yield();
|--在开发时,可以使用匿名内部类来完成局部的路径开辟。
好了,Java多线程的内容就这么多,接下来将介绍Java中常用的一些API。
有任何问题请和我联系,共同进步:[email protected]
转载请声明出处:http://blog.csdn.net/zhongkelee/article/details/45892177