Java之多线程

来源:互联网 发布:2017年网络新项目 编辑:程序博客网 时间:2024/05/22 02:23

一、线程概述

1、什么是线程

  想要理解线程,需要先知道什么是进程。进程就是一个正在执行的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。
  一个进程可以有一到多个线程,而一个线程只能属于一个进程。

2、多线程的优势

  运行一个进程时,如果这个进程中只有一个线程,很可能会出现很多问题,如果采用多线程的方式,可以把任务分块执行,分块后可以同时进行而不用等待, 这样效率更高。

二、创建线程

  创建线程有两种方法:一种是继承Thread类;另一种是通过实现Runnable接口。

1、通过继承Thread类创建线程

  1)定义一个类并继承Thread。
  2)重写 Thread 类的 run 方法。
   目的:将自定义的代码存储在run方法中,让线程运行。
  3)建立自定义类的一个对象(此时一个线程也被建立)。
  4)调用线程的start方法。
    该方法有两个作用:一是启动该线程,二是运行该线程的run方法。

  为什么要重写run方法呢?
  因为Thread类是用来描述线程的,该类定义了一个功能,用于存储线程要运行的代码,该功能即是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
  示例代码:

/*练习:创建两个线程,和主线程交替运行。*/class ThreadTest2{    public static void main(String [] args){        Test2 t1 = new Test2("线程0");//新建一个线程对象t1        Test2 t2 = new Test2("线程1");//新建一个线程对象t2        t1.start();//开启线程t1,并运行Test2类里的run方法        t2.start();//开启线程t2,并运行Test2类里的run方法        //主线程运行的代码        for(int i = 0;i < 60;i++){            System.out.println("main____"+i);        }    }}class Test2 extends Thread{    //用于给创建的线程重新命名    Test2(String name){        super(name);    }    //复写run方法    public void run(){        for(int i = 0;i < 60;i++){            //Thread.currentThread().getName()用来获取当前线程的名称。            System.out.println(Thread.currentThread().getName()+"  __run__"+i);        }    }}

  代码运行结果:

线程0  __run__54main____53线程0  __run__55main____54线程0  __run__56main____55线程0  __run__57main____56线程0  __run__58main____57线程0  __run__59main____58main____59

  因为主线程、线程0以及线程1的执行是由CPU随机决定的,所以运行时是随机的,也就是说运行结果不唯一。

2、通过实现Runnable接口创建线程

  Thread类是一个实体类,实体类具有很多缺点,例如,由于Java的单继承机制,实现多线程的类必须继承Thread类,因而就不能再继承其他类了。这时可以通过实现Runnable接口来创建线程。具体创建步骤:
  1)定义类实现Runnable接口
  2)覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
  3)通过Thread类建立线程对象。
  4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
  原因:自定义的run方法所属对象是实现Runnable接口的自定义类的对象,所以要让线程去执行指定对象的run方法,就必须明确该run方法的对象。
  5)调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
  示例代码:

/*需求:简单的卖票程序。多个窗口同时卖票*/class Ticket implements Runnable{//实现Runnable接口    private int tick = 100;    //复写run方法    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();//创建一个实现Runnable接口的子类的一个对象。        Thread t1 = new Thread(t);//创建一个线程t1        Thread t2 = new Thread(t);//创建一个线程t2        Thread t3 = new Thread(t);//创建一个线程t3        Thread t4 = new Thread(t);//创建一个线程t4        t1.start();        t2.start();        t3.start();        t4.start();    }}

三、Java中的线程的状态

  Java中线程一共有下列状态:
  
  1)New(新建状态):新建了一个线程对象,但是尚未被start()方法调用。
  2)Runnable(可运行状态):线程有运行资格,但是CUP没有将使用权交给该线程。处于可运行状态中的线程可以是正在运行中,也可以是还没开始运行。
  
  线程进入可运行状态的方式:
    a、新建一个线程对象,并被其他线程调用了start()方法。
    b、当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行状态。
  
  3)Running(运行状态):CPU选择一个处于可运行状态中的线程,并让它开始运行run()方法。
  
  4)Blocked(阻塞状态):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入可运行状态,才有机会转到运行状态。阻塞的情况分三种:
    a、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    b、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    c、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
  5.)Dead(死亡状态):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
  
  这里写图片描述

四、多线程安全问题

  在多次运行上面的卖票程序后(尤其是在电脑很卡的时候运行),会发现有时候会打印出0、-1、-2等错票,这说明多线程运行时会出现安全问题。

1、多线程出现安全问题的原因

  1)多线程出现安全问题的原因

  当多条语句在操作多个线程共享数据时,如果一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,就会导致共享数据出错。

  2)如何找到多线程的安全问题出现位置

    a、明确哪些代码是多线程运行代码。
    b、明确共享数据。
    c、明确多线程运行代码中哪些语句是操作共享数据的。
 

2、解决办法——对象锁机制

  让多条线程中的一条线程执行完全部的共享数据后(在执行过程中,其他线程不可以参与执行),才允许其他线程执行共享数据。Java提供的锁机制通过将共享数据加锁禁止其他线程打断这段代码的执行,直至解锁后另一个线程才能再执行共享数据,从而避免了数据操作的失误。

  1)加锁的前提

  a、必须要有两个或者两个以上的线程。
  b、必须是多个线程使用同一个锁。
  如果用完加锁机制后,发现仍然存在安全问题,这时候就需要考虑上面两个加锁的前提里到底是哪个条件没有满足。 

  2)同步代码块

  格式如下:

synchronized(对象){    //需要同步的代码}

  对象如同锁,持有锁的线程可以在同步中执行,没有锁的线程即使获取cpu的使用权,也进不去。
  

/*对卖票程序进行优化,使用同步代码块来加锁共享数据。*/class Ticket implements Runnable{    private int tick = 500;    Object obj = new Object();    public void run(){        while(true){            synchronized(obj){                if(tick > 0){                    //sleep方法会抛出InterruptedException异常,所以调用者(run)必须对该异常进行处理。由于该run()方法是复写接口Runnable的run()方法,接口中的run()方法没有抛出InterruptedException,所以在这儿必须try                    try{                        Thread.sleep(10);                    }                    catch(Exception e){                    }                    //打印出线程名称和剩余票数。                    System.out.println(Thread.currentThread().getName()+"...."+"sale : "+ tick--);                }            }        }    }}class TicketDemo{    public static void main(String [] args){        Ticket t = new Ticket();//创建一个实现Runnable接口的子类的一个对象。        Thread t1 = new Thread(t);//创建一个线程t1        Thread t2 = new Thread(t);//创建一个线程t2        Thread t3 = new Thread(t);//创建一个线程t3        Thread t4 = new Thread(t);//创建一个线程t4        t1.start();        t2.start();        t3.start();        t4.start();    }}

  3)同步方法

  通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。例如,public synchronized void add(int num){}
示例:

/*需求:银行有一个金库。有两个储户分别存300元,每次存100元,分3次。*/class Bank{    private int sum;    Object obj = new Object();    //将需要进行同步的代码通过同步方法进行加锁    public synchronized void add(int num){            sum = sum + num;            try{                Thread.sleep(10);            }            catch(Exception e){            }            System.out.println("sum="+sum);    }}class Cus implements Runnable{    private Bank b = new Bank();    //run()方法里的代码不需要同步    public void run(){        for(int x = 0;x < 3;x++){            b.add(100);        }    }}

  那么同步方法用的是哪一个锁呢?
  方法需要被对象调用,那么方法都有一个所属对象引用,就是this,所以同步方法的锁是this。现在通过下面的程序来进行验证同步方法的锁是this:

.*一个线程在同步代码块中,一个线程在同步方法中。都在执行卖票动作。*/class Ticket implements Runnable{    private int tick = 100;    private boolean flag = true;    Object obj = new Object();    public void run(){        if(flag){            while(true){                //将同步代码块中的对象改为this后,发现最后打印结果没有出现0、-1、-2等错票,说明安全问题已解决,也就验证了同步方法使用的锁是this。                synchronized(this){                    if(tick > 0){                        try{                            Thread.sleep(10);                        }                        catch(Exception e){                            e.printStackTrace();                        }                        System.out.println(Thread.currentThread().getName()+"...."+"code : "+ tick--);                    }                }            }        }        else{            while(true){                this.show();                }        }    }    public synchronized void show(){        if(tick > 0){            try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();}            System.out.println(Thread.currentThread().getName()+"...."+"show++++++ : "+ tick--);        }    }    public void changeFlag(){        flag = false;    }}class ThisLockDemo{    public static void main(String [] args){        Ticket t = new Ticket();        Thread t0 = new Thread(t);        Thread t1 = new Thread(t);        t0.start();        //try{Thread.sleep(10);}catch(Exception e){}        t.changeFlag();        t1.start();    }}

  如果同步方法被static修饰后,使用的锁是什么呢?
  通过上述代码验证,发现不再是this,因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class, 该对象的类型是Class。
  所以静态的同步方法,使用的锁是该方法所在类的字节码文件对象:类名.class。

  4)Lock锁

  在JDK5.0之后,Java的锁机制中使用Lock 替代了 synchronized 方法和语句的使用,具体来说就是通过Lock类的lock方法实现的,解锁则是通过unlock方法实现,如果希望对共享数据进行加锁,形式如下:

class X {    //ReentrantLock类是接口Lock接口的一个具体实现,所以首先建立一个ReentrantLock的对象lock   private ReentrantLock lock = new ReentrantLock();   public void m() {      lock.lock();  // 加锁     try {       //需要加锁的共享数据     }      finally {       lock.unlock()//解锁     }   } }

  现在使用Lock锁来对卖票程序进行优化:
  

import java.util.concurrent.locks.ReentrantLock;class TicketDemo3{    public static void main(String [] args){        Ticket t = new Ticket();        Thread t1 = new Thread(t,"卖票窗口1");        Thread t2 = new Thread(t,"卖票窗口2");        Thread t3 = new Thread(t,"卖票窗口3");        Thread t4 = new Thread(t,"卖票窗口4");        t1.start();        t2.start();        t3.start();        t4.start();    }}class Ticket implements Runnable{    private int tick = 1100;    private ReentrantLock lock = new ReentrantLock();新建一个锁对象    public void run(){        while(true){            lock.lock();//加锁            try{                if(tick > 0){                    Thread.sleep(10);                    System.out.println(Thread.currentThread().getName() + "....." + tick--);                }            }            catch(Exception e){            }                       finally{                lock.unlock();//解锁            }        }    }}

3、解决线程间通信问题——等待唤醒机制

  除了锁机制,Java还提供了等待唤醒机制,使得某个线程进入阻塞状态,并释放其所持的锁,直到另一个线程调用特定的方法使进入阻塞状态的线程唤醒。在JDK5.0之前,等待唤醒机制中常用的方法有wait()、notify()、notifyAll()。这些方法存在于Object类中。
  wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。当前线程必须拥有此对象监视器。
  notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。
  notifyAll():唤醒在此对象监视器上等待的所有线程。
示例:

class ProducerConsumerDemo{    public static void main(String [] args){        Resource res = new Resource();        Producer pro = new Producer(res);        Consumer con = new Consumer(res);        Thread t1 = new Thread(pro,"生产者1号");        Thread t2 = new Thread(pro,"生产者2号");        Thread t3 = new Thread(con,"消费者1号");        Thread t4 = new Thread(con,"消费者2号");        t1.start();        t2.start();        t3.start();        t4.start();    }}class Resource{    private String name;    private int count = 1;    private boolean flag = false;    public synchronized void set(String name){        while(flag){            try{                this.wait();//阻塞生产者线程(t1、t2)            }            catch(Exception e){            }        }        this.name = name+"---"+count++;        System.out.println(Thread.currentThread().getName()+"......"+this.name);        flag = true;        this.notifyAll();//唤醒所有因wait()方法而进入阻塞状态的线程    }    public synchronized void out(){        while(!flag){            try{                this.wait();//阻塞消费者线程(t3、t4)            }            catch(Exception e){            }        }        System.out.println(Thread.currentThread().getName()+"..."+this.name);        flag = false;        this.notifyAll();//唤醒所有因wait()方法而进入阻塞状态的线程    }}class Producer implements Runnable{    private Resource res;    Producer(Resource res){        this.res = res;    }    public void run(){        while(true){            res.set("商品");        }    }}class Consumer implements Runnable{    private Resource res;    Consumer(Resource res){        this.res = res;    }    public void run(){        while(true){            res.out();        }    }}

运行结果:
这里写图片描述

  JDK 5.0之后,Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
  在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。Condition的强大之处在于它可以为多个线程间建立不同的Condition类的对象。例如, 

import java.util.concurrent.locks.*;class ProducerConsumerDemo2{    public static void main(String [] args){        Resource res = new Resource();        Producer pro = new Producer(res);        Consumer con = new Consumer(res);        Thread t1 = new Thread(pro,"生产者1号");        Thread t2 = new Thread(pro,"生产者2号");        Thread t3 = new Thread(con,"消费者1号");        Thread t4 = new Thread(con,"消费者2号");        t1.start();        t2.start();        t3.start();        t4.start();    }}class Resource{    private String name;    private int count = 1;    private boolean flag = false;    private ReentrantLock lock = new ReentrantLock();//新建一个锁对象。    private Condition condition_pro = lock.newCondition();//通过Lock接口里的newCondition()方法为生产者建立一个Condition类对象。    private Condition condition_con = lock.newCondition();//通过Lock接口里的newCondition()方法为消费者建立一个Condition类对象。    public void set(String name) throws InterruptedException{        lock.lock();//加锁        try{            while(flag){                condition_pro.await();//阻塞绑定condition_pron对象的线程,即阻塞生产者线程t1、t2            }            this.name = name+"---"+count++;            System.out.println(Thread.currentThread().getName()+"......"+this.name);            flag = true;            condition_con.signal();//唤醒绑定condition_con对象的线程,即唤醒消费者线程t3、t4        }        finally{            lock.unlock();//解锁        }       }    public void out() throws InterruptedException{        lock.lock();        try{            while(!flag){                condition_con.await();//阻塞绑定condition_con对象的线程,即阻塞消费者线程t3、t4            }            System.out.println(Thread.currentThread().getName()+"..."+this.name);            flag = false;            condition_pro.signal();//唤醒绑定condition_pron对象的线程,即唤醒生产者线程t1、t2        }        finally{            lock.unlock();        }    }}class Producer implements Runnable{    private Resource res;    Producer(Resource res){        this.res = res;    }    public void run(){        while(true){            try{                res.set("+商品+");            }            catch(InterruptedException e){            }        }    }}class Consumer implements Runnable{    private Resource res;    Consumer(Resource res){        this.res = res;    }    public void run(){        while(true){            try{                res.out();            }            catch(InterruptedException e){            }        }    }}

4、停止线程

  停止线程

5、死锁

  线程要发生死锁,必须满足一下4个条件:
  1)互斥条件:一个资源每次只能被一个线程持有。
  2)请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3)不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
  举个简单的例子:假设线程1取得资源A的锁并请求资源B的锁,线程2取得资源B的锁并请求资源A的锁,现在资源A和资源B满足互斥条件(A和B都只能被一个线程持有)、请求与保持条件(线程1在没有取得资源B的锁前不会释放资源A的锁,线程2在没有取得资源A的锁前不会释放资源B的锁)、不剥夺条件(线程1和线程2没有被外部力量干预而放弃其所持有的锁)、循环等待条件(线程1等待线程2,线程2又在等待线程1)。
  一个死锁的示例代码:

class Test implements Runnable{    private boolean flag;    Test(boolean flag){        this.flag = flag;    }    public void run(){        if(flag){            synchronized(MyLock.locka){                System.out.println("if locka");                synchronized(MyLock.lockb){                    System.out.println("if lockb");                }            }        }        else{            synchronized(MyLock.lockb){                System.out.println("else lockb");                synchronized(MyLock.locka){                    System.out.println("else locka");                }            }        }    }}class MyLock{    static Object locka = new Object();    static Object lockb = new Object();}class DeadLockTest{    public static void main(String [] args){        Thread t1 = new Thread(new Test(true));        Thread t2 = new Thread(new Test(false));        t1.start();        t2.start();    }}

这里写图片描述

五、线程的其他知识

1、线程的优先级

  当Java准备从一组处于阻塞状态的线程中唤醒某个线程时,系统总是倾向于启动优先级别高的线程。Java的线程分为十级,分别是1到10,为了方便记忆,Java的Thread类中提供了三个整形静态变量MORN_PRIORITY(5级)、MAX_PRIORITY(10级)、MIN_PRIORITY(1级)。使用示例:

thread.setPriority(Thread.MAX_PRIORITY);//设置线程优先级为10

2、守护线程

  在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)。Daemon的作用是为其他线程的运行提供便利服务,比如垃圾回收线程就是一个的守护者线程。User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。 要将一个线程设置成守护者线程必须调用Thread类的setDaemon(boolean daemon)。
  注意:(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
   (2) 在Daemon线程中产生的新线程也是Daemon的。

3、sleep() 与 wait()的区别

  a、这两个方法来自不同的类:sleep()来自Thread类,和wait()来自Object。
  b、wait(),notify()和notifyAll()只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
  c、sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
  而当调用wait()方法的时候,线程会释放对象锁,使得其他线程可以使用同步控制块或者方法,自己则进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

4、yield()和join()方法

  yield()方法:暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。yield()只能使同优先级或更高优先级的线程有执行的机会。
  join()方法:阻塞所在线程,等调用它的线程执行完毕,再向下执行
等待该线程终止。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

0 0
原创粉丝点击