java多线程

进程:就是应用程序在运行时期,所占用的内存空间区域,这个区域就称为这个应用程序的进程。

线程:Thread,就是进程中执行的一个小程序,对于CPU,线程是一条可以被CPU执行的路径。

一、多线程的创建方式

线程这个事物也是对象,对象的描述类是java.lang.Thread。之前我们使用的Thread.sleep()方法就是线程类中的静态方法。线程进程都是由操作系统,依靠JVM找操作系统才实现线程的功能。

方式一:子类实现Thread

  • 定义类继承Thread类,入伙
  • 重写Thread类的run方法,为什么重写,Java工程师,不知道其他程序人员要用线程运行什么代码,规范就是run
  • 创建Thread子类的对象,创建了一个线程,多了一个CPU的执行路径
  • 调用Thread类的start()方法,开启线程。开始运行线程,JVM调用线程的run

方式二:实现Runnable接口

  • 创建一个类实现Runnable接口,重写里面的run()方法
  • 创建一个线程对象 的时候,传递子类实例对象进线程的构造方法
  • 然后,线程对象调用start方法,开启线程

两种实现方式的比较:

多实现明显比单实现好,避免了局限性,但是也因此将线程操作的数据变成了共享数据,存在需要处理的安全隐患。

二、线程名字的设置和获取

设置和获取线程的名字就是调用线程内置的两个方法

public String getName();
public void setName(String name); 
static Thread currentThread();//返回当前方法运行的线程对象

getName方法相关:

如果是进程的实例来调用此方法并没有什么可以说的,直接调用即可。但是要是在main调用此方法的话,需要先调用Thread中的静态方法static Thread currentThread()来返回线程对象,然后再调用getName方法才能获取线程名字。

线程名字设置方法相关:

其实设置线程名字还有另外一种更为简便的方法,就是在构造线程的时候,传递String name给构造方法即可,Thread中有重载此类型的构造方法。但是中点不在这里,重点在于要是子类继承Thread,要以此种方法实现线程的名字 的设置就需要再重载传String name参数的构造方法,在方法的首行super(String name)来将参数传递给父类。

这里提供一个demo作为演示:

package thread;
/*
 * 进程和线程
 * 进程类
 * 进程的run方法和start
 * 进程名字的设置和获取
 */

//定义实现一个Thread的子类
class Threadtemp extends Thread{
    
    public void run(){
        for(int i  = 0 ; i < 10 ; i++ ){
            System.out.println("thread:" + i);
        }
    }
}

public class ThreadDemo {
    //main也是一个线程
    public static void main(String[] args) {
        //创建进程实例
        Threadtemp thread  = new Threadtemp();
        thread.start();//调用父类start方法开启线程
        thread.setName("六娃");
        System.out.println(thread.getName());
        
        for(int i  = 0 ; i < 10 ; i++ ){
            System.out.println("main:" + i);
        }
        System.out.println(Thread.currentThread().getName());
        
    }
}

三、线程共享数据的安全性

使用上述的第二种实现接口方式来创建多线程的方法,存在一个安全隐患。因为多个线程的构造可以传递同一个对象,这就使得该对象的数据变成了多个线程共享的数据。但是由于线程的并发性,会导致产生数据同步等安全问题的出现。

举个例子不安全的例子:

这是一个实现Runnable接口的类:

class Ticket2 implements Runnable {
    private int ticketNum = 100;
    public void run() {

        while (true) {
        
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticketNum--);
                } else {
                    break;
                }
        }
    }
}

此时,在main中运行下面的代码,就会使得对象中的ticketNum变成多个线程共享的数据。

public static void main(String[] args) {
        Ticket2 t = new Ticket2();

        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);

        t0.start();
        t1.start();
    }

现在,两个线程同时对同一份数据进行操作,会使得两次出现数据不同步的情况,甚至出现一些单线程下原本不会出现的数据异常情况。

问题描述到这里就很明白了,那怎么来解决这个问题呢,就这必须提到java中的同步代码块技术,代码如下。

synchronized(Object o){
  ...
}

当一段代码加上同步代码块的时候,多个线程就无法并发的访问此代码,每次最多只能有一个线程运行同步代码块中的代码,当有线程运行同步代码块时,别的线程的访问就会被拒绝,直到该进程执行完毕。所以这样做也就会牺牲部分运行的性能。

下面提供一个demo来演示解决共享数据的安全问题:

package thread;
/*  
 * 线程的两种创建方式以及数据共享问题
 * 多线程共享数据问题
 * 模拟多个窗口售卖火车票问题
 */

//先来一个线程的类
class Ticket extends Thread {

    private static int ticketNum = 100;

    public void run() {
        while (true) {
            if (ticketNum > 0) {
                System.out.println(getName() + ":" + ticketNum--);
            }
        }
    }
}

// 实现Runnable的接口的类,加锁保证共享数据的安全性
class Ticket2 implements Runnable {
    private int ticketNum = 100;
    private Object obj = new Object();

    public void run() {

        while (true) {
            synchronized (obj) {
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticketNum--);
                } else {
                    break;
                }
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        // Ticket t0 = new Ticket();
        // Ticket t1 = new Ticket();
        // Ticket t2 = new Ticket();
        // Ticket t3 = new Ticket();
        //
        // t0.start();
        // t1.start();
        // t2.start();
        // t3.start();

        Ticket2 t = new Ticket2();

        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

有同步代码块,就有同步方法,其实java源码中也可以看到很多线程安全的对象使用同步方法来操作共享数据,保证其安全性。下面提供一个demo,演示同步方法的使用,代码中有对同步方法的详细解释和说明,这里就不赘述:

package thread;
/*
java的共享数据问题
此处来模拟银行的多个窗口同时对同一个账户存款的操作
来演示共享数据和同步方法的使用
*/

public class ThreadDemo2 {
    public static void main(String[] args) {
        //在main方法中,创建两个进程,来模拟两个银行窗口存钱
        Customer cust = new Customer();
        
        Thread t1 = new Thread(cust);
        Thread t2 = new Thread(cust);
        
        t1.start();
        t2.start();
    }
}


class Bank {
    private static int money = 0;
  //常规的同步方法,直接使用synchronized关键字修饰方法
  //下面add和add2方法对其实现原理做出解释
    public synchronized void method(){  
    }
  
  //如果同步方法是静态的,锁是本类对象 class
    public  static  void add(int sum) {
        synchronized(Bank.class){
        money = money + sum;
        System.out.println(money);
        }
    }
    
    //如果同步方法非静态,锁是本身 this
    public void add2(int sum){
        synchronized(this){
            money = money + sum;
            System.out.println(money);
            }
    }
}

class Customer implements Runnable {

    private Bank bank = new Bank();

    public void run() {
        for (int i = 0; i < 3; i++) {
            bank.add2(100);
        }
    }
}

四、线程通信

线程间的通信就是多个线程操作同一共享数据。一个线程负责数据的填充,一个线程负责数据的获取。之前只是解决了线程数据的同步问题,但是数据块的安全问题还是没有解决。

现在,直接给出一个题目:

需要实现交替打印数据,一个线程填充完数据后由另一个线程负责将填充进去的数据打印出来。要实现这个就必须使用同步技术来实现。

直接给出demo:

package thread;

/*
 * 进程通信demo
 * 
 * 两个Runnable的实现类对共享的数据进行读写操作
 * 互相传递数据和信息,实现进程简单的通信
 * 
 * 在资源对象里面设置标志boolean型数据flag,来区分资源对象是否有数据在里面
 * 规定若flag为真,说明资源对象里面没有数据,input线程就需要填充数据进去
 * 填充完了,再将标志位设置为false,此时就算output线程无法执行,
 * 当input线程去执行此处代码的时候,会走wait()方法,无限期等待
 * 
 * 此时,标志位为false,也就是说表示有数据,output回走sop语句
 * 执行完了之后,设置标志位为true,表示没有数据,等待数据填充,并
 * 执行notify()唤醒刚在在资源对象上等待的数据输入线程
 * 
 * 这里面同时也就涉及到了wait()方法的一些特点
 * 
 * 分析:代码里面已经表示的很清楚了,wait()方法是写在同步代码块内的
 * 也就是说,一个线程拿到了锁在同步代码块内执行了wait方法后,另一个拿同一把锁的线程
 * 也进入到了自己的同步代码块,那就说明该线程也拿到了这把锁。
 * 所以,wait()方法的特性就明白了,调用该方法会释放会释放锁。
 * 与之相似的sleep方法不会释放锁,这就是为什么调用sleep方法后大家都
 * 得等的原因所在
 * 
 * 其实,这就是一个简单的生产消费设计模式
 * 只不过这里是单生产和单消费
 */
public class ThreadCommunicationDemo {
    public static void main(String[] args) {
        Resource r = new Resource();

        new Thread(new Input(r)).start();
        new Thread(new Output(r)).start();
    }
}

class Resource {
    String name;
    String sex;
    boolean flag = true;
}

//填充数据的线程
class Input implements Runnable {
    
    private Resource r;
    
    Input(Resource r) {
        this.r = r;
    }

    public void run() {
        int i = 0;
        while (true) {
            synchronized (r) {
                if(!r.flag)
                    try{r.wait();}catch(Exception e){}
                if (i % 2 == 0) {
                    r.name = "张三";
                    r.sex = "男";
                } else {
                    r.name = "李四";
                    r.sex = "女";
                }
                i++;
                r.flag = false;
                r.notify();
            }
        }
    }
}

//输出数据的线程
class Output implements Runnable {

    private Resource r;

    Output(Resource r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            synchronized(r){
                if(r.flag)
                    try{r.wait();}catch(Exception e){}
            System.out.println(r.name + "..." + r.sex);
            r.flag = true;
            r.notify();
            }
        }
    }
}

1、wait()和sleep()方法的区别

  • 首先从外在的特点上来说,wait方法是Object中的非静态方法,而sleep是Thread类中的静态方法
  • wait方法必须有锁的支持,而sleep任意程序都可以调用执行,不需要锁
  • sleep方法不会释放锁,wait方法会使得线程释放同步锁,当线程被唤醒的时候,必须重新获取同步锁,才能够继续执行。

2、为什么线程的等待方法写在Object中

wait和notify这两个方法操作本锁上的线程,必须有锁 的支持,而锁是任意对象。所以讲wait和notify方法写在Object类中,保证了任意对象都可以调用线程的等待唤醒方法。

3、生产和消费模式

上面的里中其实就是生产和消费模式,只不过是单生产和单消费,也就是都只有一个线程来运行。实际应用中的生产和消费模式都是多生产和多消费(有多个负责生产的线程和多个负责消费的线程),需要注意的是就算是多生产和多消费,也只能生产一个消费一个。

现在,将线程通信中的demo进行改造,使之支持多生产和多消费。有以下进行改造的点:

  • 将同步代码块改为同步方法,这样可以代码的可读性,简化逻辑
  • 将if改为while,使得在此等待的线程被唤醒时仍然需要进行锁的逻辑判断,不能直接执行生产和消费的代码
package thread;

/*
 * 线程通信
 * 将ThreadCommunicationDemo中的代码改造为同步方法
 * ,添加逻辑处理。到此,就引入了生产者和消费者的设计模式
 * 
 * 使其支持多线程生产,多线程消费
 * 
 * 缺点:每次都唤醒全部线程,浪费资源,影响运行速度
 */
public class ThreadCommunicationDemo2 {
    public static void main(String[] args) {
        Resource r = new Resource();
        //建立三个生产者,三个消费者
        new Thread(new Input(r)).start();
        new Thread(new Input(r)).start();
        new Thread(new Input(r)).start();
        new Thread(new Output(r)).start();
        new Thread(new Output(r)).start();
        new Thread(new Output(r)).start();
    }
}

class Resource {
    private String name;
    private String sex;
    private boolean flag = true;

    public synchronized void setNameSex(String name, String sex) {
        while (!this.flag)
            try {this.wait();}catch (Exception e){}
        this.name = name;
        this.sex = sex;
        System.out.println("制造:" + name + "." + sex);
        flag = false;
        this.notifyAll();
    }
    
    public synchronized void getNameSex() {
        while (this.flag){
            try {
                this.wait();
            } catch (Exception e) {}
        }
        System.out.println(name + "." + sex);
        this.flag = true;
        this.notifyAll();
    }
}

// 实现两个用于读取Resource的进程类
class Input implements Runnable {

    private Resource r = null;

    Input(Resource r) {
        this.r = r;
    }

    public void run() {
        int i = 0;
        while (true) {
            if (i % 2 == 0) {
                r.setNameSex("张三", "男");
            } else {
                r.setNameSex("李四", "女");
            }
            i++;
        }
    }
}

class Output implements Runnable {

    private Resource r = null;

    public Output(Resource r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.getNameSex();
        }
    }
}

这两个地方改造完成,基本上就实现了多生产和多消费,并且是生产一个消费一个。不过这样写并不是最好的实现方式,因为每次都notifyAll会不分青红皂白的唤醒所有等待的线程,造成了大量的系统的资源浪费。我们希望实现的方式是生产者执行完生产代码后只唤醒消费者中的一个,同理消费代码执行完成后也是只唤醒一个生产者线程这样。但是目前,这个代码还没有办法做到这样。

额外提供一个逻辑看起来比较清晰的demo:

package thread;

/*
 * 模式:
 * 多生产,多消费
 * 
 * 目标:
 * 生产一个,就必须消费一个
 */
public class ThreadCommunicationDemo3 {
    public static void main(String[] args) {
        Product p = new Product();
         
        new Thread(new Produce(p)).start();
        new Thread(new Produce(p)).start();
        new Thread(new Produce(p)).start();
        new Thread(new Produce(p)).start();
        new Thread(new Consumer(p)).start();
        new Thread(new Consumer(p)).start();
        new Thread(new Consumer(p)).start();
        new Thread(new Consumer(p)).start();
    }
}

//产品类
class Product {
    private String name;
    private int count;
    private boolean flag = true;
    
    //生产方法
    public synchronized void setName(String name){
        while(!this.flag)
            try{this.wait();}catch(Exception e){}
        this.name = name + count++;
        System.out.println(Thread.currentThread().getName() +" 生产:" + this.name );
        flag = false;
        this.notifyAll();
    }
    
    //消费方法
    public synchronized void getName(){
        while(this.flag)
            try{this.wait();}catch(Exception e){}
        System.out.println(Thread.currentThread().getName() +" 消费了产品:" + name);
        flag = true;
        this.notifyAll();
    }
}

//生产者类
class Produce implements Runnable{
    private Product p = null;
    Produce(Product p){
        this.p = p;
    }
    
    public void run(){
        while(true){
            p.setName("Apple II ");
        }
    }
}

//消费者
class Consumer implements Runnable{
    private Product p = null;
    
    Consumer(Product p){
        this.p = p;
    }
     public void run(){
         while(true){
             p.getName();
         }
     }
}

4、使用Lock和Condition替换synchronized

从JDK1.5开始,java提供了新的线程管理方式,就是接口Lock和类Condition。

  • 首先是用Lock代替synchronized实现同步代码块的加锁和上锁,Lock的实现类是ReentrantLock

    之前的同步代码块是这样写的:

    synchronized(Object o){
      ...
    }
    

    与之对应的Lock是这么实现的:

    lock();
      ...//中间部分就是以前同步代码块中的代码
    unLock(); 
    
  • Conditon接口替换了原来的线程监视器方法

    原来线程的等待和唤醒是这么写的:

    //假设o是锁对象
    o.wait();
    o.notify();
    o.notifyAll();
    

    那现在的使用Conditon接口的实现类是这么写的:

    //假设con是Conditon接口的实现类对象、
    con.await();//替换原来的wait
    con.signal();//替换原来的notify
    con.signalAll();//替换原来的notifyAll
    

    那Condition的实现类其实不用自己实现,也不用知道是谁,直接使用Lock接口方法中的newCondition()获取,然后多态调用即可。

    demo:

    package lock;
    
    import java.util.concurrent.locks.*;
    /*
     * Lock和Condition替换synchronized方式
     * 直接来改造之前的多生产和多消费的代码
     */
    public class LockDemo {
      public static void main(String[] args) {
          Product p = new Product();
           
          new Thread(new Produce(p)).start();
          new Thread(new Produce(p)).start();
          new Thread(new Produce(p)).start();
          new Thread(new Produce(p)).start();
          new Thread(new Consumer(p)).start();
          new Thread(new Consumer(p)).start();
          new Thread(new Consumer(p)).start();
          new Thread(new Consumer(p)).start();
          
      }
    }
    
    //产品类
    class Product {
      private String name;
      private int count;
      private boolean flag = true;
      
      private Lock lock = new ReentrantLock(); 
      
      //创建两个锁的管理器,一个管生产,一个管消费
      private Condition pro = lock.newCondition();
      private Condition con = lock.newCondition();
      
      //生产方法
      public void setName(String name){
          //上锁
          lock.lock();
          
          while(!this.flag)
              //注意管理器的调用:等待是等待自己方的线程,唤醒是唤醒对方线程
              try{pro.await();}catch(Exception e){}
          this.name = name + count++;
          System.out.println(Thread.currentThread().getName() +" 生产:" + this.name );
          flag = false;
          
          //一次唤醒对方等待线程的一个即可,避免的资源的浪费
          con.signal();
          //解锁
          lock.unlock();
      }
      
      //消费方法
      public void getName(){  
          //上锁
          lock.lock();
    
          while(this.flag)
              try{con.await();}catch(Exception e){}
          System.out.println(Thread.currentThread().getName() +" 消费了产品:" + name);
          flag = true;
          
          //一次唤醒对方等待线程的一个即可,避免的资源的浪费
          pro.signal();
          //解锁
          lock.unlock();
          
      }
    }
    
    //生产者类
    class Produce implements Runnable{
      
      private Product p = null;
      
      Produce(Product p){
          this.p = p;
      }
      
      public void run(){
          while(true){
              p.setName("Apple II ");
          }
      }
    }
    
    //消费者
    class Consumer implements Runnable{
    private Product p = null;
      
      Consumer(Product p){
          this.p = p;
      }
       public void run(){
           while(true){
               p.getName();
           }
       }
    }
    

五、线程停止和线程方法

线程停止有两种方法:

  • 停止线程的第一种方式,设置标志位,传递false给死循环的true
  • 第二种是调用中断方法interrupt() ,若是此时线程处于wait状态,报出异常后传递false停止

线程的方法:

  • 线程的toString()方法,会打印三个信息【线程名,线程优先级,线程组】
  • 改变线程的优先级setPriority(int i),范围是1-10
  • setDeamon()设置线程为守护线程,必须写在start方法之前

守护线程就是当主线程结束后,其守护线程也都会跟随结束。如果进程内的线程全部变为守护线程,那所有的线程都会结束退出。

demo:

package thread;
/*
 * 线程的方法:
 * 1.停止线程的第一种方式,设置标志位,传递false给死循环的true
 * 2.第二种是调用中断方法,interrupt(),若是此时出于wait状态,报出异常后传递false
 * 3.线程的toString()方法
 *   会打印三个信息【线程名,线程优先级,线程组】
 * 4.改变线程的优先级setPriority,范围是1-10
 * 5.setDeamon设置线程为守护线程
 */
public class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();
        
        Thread t = new Thread(st);
        t.start();
        t.setPriority(1);   
        System.out.println(t);
        
        //循环跑产生延迟,之后结束进程
//      for(int i = 0 ; i < 40000 ; i++){
//          if(i == 39999){
//              st.setFalse(false);
//              
//          }
//      }
        
        //中断线程结束方法,线程必须处于wait状态中
        t.interrupt();
        
    }
}

//第一种停止方式,传递false给死循环的true
//调用中断方法,interrupt(),若是此时出于wait状态,报出异常后抓住结束方法

class StopThread implements Runnable{
    
    private boolean flag = true;
    
    public void run(){
         while(flag){
             synchronized (this) {
                try{this.wait();}catch(Exception e ){
                    System.out.println(e + "中断异常");
                    flag = false;
                }
             System.out.println("running");
             }
         }
    }
    
    public void setFalse(boolean flag){
        this.flag = flag;
    }
}

六、Timer类

java.util.Timer是java中的定时器。可以让程序与指定的时间和时间间隔后运行。

构造方法:

Timer的构造方法可以传递String类型的线程名,将指定线程变为定时执行的线程,当然不指定,那该方法在哪里执行,就默认是哪个线程。还可以传递boolean类型的参数,指定时候讲该线程变成守护线程。

类方法:

schedule(TimerTask task, Date firstTime, long period);

TimerTask参数是需要运行的代码程序,Date开始时间,long类型的period则是间隔时间。

TimerTask是接口,实际传参的时候必须传递实现类对象。现实使用中都是直接写匿名内部类来进行。

demo:

package thread.timer;

/*
 * timer的练习使用
 */
import java.util.Timer;
import java.util.TimerTask;
import java.util.Date;
import java.io.IOException;
import java.text.SimpleDateFormat;

public class TimerDemo {
    public static void main(String[] args) {
        
        SimpleDateFormat sdf = new SimpleDateFormat("yy年MM月dd日  HH:mm:ss");
        Timer t = new Timer();
        
        t.schedule(new TimerTask(){
            public void run(){
                System.out.println(sdf.format(new Date()));
            }
        }, new Date(), 1000);
    }
}

七、线程中的其它方法

  • join(),等待该线程终止,也就是该线程执行完了,其他线程开始争抢资源
  • yield(),线程让出执行的机会,让其他线程先运行,必须写在run方法里

demo:

package thread;

/*
 * 线程类的方法
 * 
 * join()
 * 等待该线程终止,也就是该线程执行完了,其他线程开始争抢资源
 * 
 * yield()  不演示了就
 * 线程让出执行的机会,让其他线程先运行,必须写在run方法里
 */

class JoinThread implements Runnable{
    public void run(){
        for(int i = 1 ; i < 10 ; i ++){
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}

public class ThreadJoinDemo {
    public static void main(String[] args) throws Exception{
        JoinThread jt = new JoinThread();
        
        Thread t0 = new Thread(jt);
        Thread t1 = new Thread(jt);
        t0.start();
        t0.join();
        t1.start();
        
        for(int i = 1 ; i < 10 ; i ++){
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}

你可能感兴趣的:(java多线程)