JavaSE实战——多线程

来源:互联网 发布:加大音量的软件 编辑:程序博客网 时间:2024/05/16 13:52

转载请声明出处: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-->thisif (tickets > 0){try{Thread.sleep(10);}catch(InterruptedException e){}System.out.println(Thread.currentThread().getName()+"...code..."+tickets--);}}}elsewhile(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--);}}}elsewhile(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 locksale();}}}elsewhile(true)sale();}public synchronized void sale(){//this locksynchronized(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("张三","男男男男男男");elses.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);//最大优先级:10t2.setPriority(Thread.MIN_PRIORITY);//最小优先级:1for (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。

有任何问题请和我联系,共同进步:lichunchun4.0@gmail.com

转载请声明出处:http://blog.csdn.net/zhongkelee/article/details/45892177

1 0
原创粉丝点击