java-多线程(下)

多线程简单入门(Java)(下篇:多线程Java中的使用)

目录

一、创建多线程

二、线程的安全

三、线程的通信

一、创建多线程

在Java中,多线程的创建有4种方式。

方式一:继承于Thread类;

方式二:实现Runnable接口;

方式三:实现Callable()接口;

方式四:使用线程池。

方式一:继承于Thread类。步骤如下:

1、创建一个继承于Thread类的子类;

2、重写Thread类的子类的run()方法;

3、创建一个Thread类的对象;

4、通过对象调用start()方法开启线程。

代码:

  //1、创建一个继承于Thread类的子类
  class Mythread extends Thread{ 
     //2、重写Thread类的run()
     @Override
  public void run() {
    super.run();
    for (int i = 0; i < 100; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);//Thread.currentThread().getName()是获取当前线程名的方法
            }
    }
  }
  }
  public class ThreadTest{
      public static void main(String[] args){
     //3、创建Thread类的子类的对象
  Mythread t1=new  Mythread();
   //4、通过此对象调用start():启动当前线程,调用当前线程的run()方法
  t1.start();
  }
  }

运行结果(打印了0到100的偶数):

方式二:实现Runnable接口。步骤如下:

(1)、创建一个实现Runnable接口的类;

(2)、(1)中的实现类去实现Runnable接口中的抽象方法run();

3)、创建实现类的对象;

(4)、创建Thread类的对象,并将(3)中的对象作为参数传递到Thread类的构造器中。

(5)、通过Thread类的对象调用start()方法。

代码:

  //1、创建一个实现了Runnable接口的类
  class Mthread implements  Runnable{
  //2、实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
    for (int i = 0; i < 100; i++) {
        if(i%2==0){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}
     }
public  class ThreadTest1 {
   public static void main(String[] args) {
    //3、创建实现类的对象
    Mthread mthread = new Mthread();
    // 4、此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    Thread t1 = new Thread(mthread);
    //5、通过Thread类的对象调用start()://此时的run()是调用Mthread重写的run()
    t1.setName("线程一");//为线程设置名字
    t1.start();
    //再启动一个线程,也是遍历100以内的偶数
    Thread t2 = new Thread(mthread);
    t2.setName("线程二");
    t2.start();

} 
  }

运行结果(线程一和线程二都会打印0-100的偶数):

方式一和方式二的比较

开发中如何选择?优先选择Runnable接口的方式。原因有两个:1、实现Runnable接口的方式突破了Thread类的单继承性的局限性。2、实现Runnable接口的方式更适合处理有多个线程共享数据的情况。

二者的联系:其实Thread类也实现了Runnable接口:public class Thread implements Runnable。

二者的相同点:都需要实现run()方法,run()方法中声明了线程需要执行的逻辑。

下面我们举个例子简单说明一下Thread类中常用的方法。
1、start():启动当前线程并调用当前线程的run()方法。

2、run():通常需要重写Thread类中的此方法,将创建的线程执行的操作声明在此方法中。

3、currentThread():静态方法,返回当前代码执行的线程。

4、getName():获取当前线程的名字。

5、setName():设置当前线程的名字。

6、yield():释放当前CPU的执行权。也叫做线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同的或者更高优先级的线程。若等待队列中没有同优先级的线程,忽略此
方法。

7、join():当某个程序执行流中调用了其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。举个例子:

在线程A中调用线程B的join()方法,此时线程A进入阻塞状态。直到线程B完全执行完以后,线程A才结束阻塞状态。

8、stop():已过时。当执行此方法时,强制结束当前线程。

9、sleep(long millitime):让当前线程睡眠指定的毫秒数。此段时间内,当前线程是阻塞状态。在必要的时候执行sleep()方法会让线程执行地慢一些。

10、isAlive():判断当前线程是否还存活。

11、线程的优先级:

(1)、线程的优先级分为三个等级:最大优先级,最小优先级,默认优先级

MAX_PRIORITY:10

MIN_PRIORITY:1

NORM_PRIORITY:5 默认优先级

2)、获取和设置当前线程的优先级

getPriority()

setPriority()

(3)、说明

高优先级的线程要抢占低优先级的线程,但是也只是从概率上这么说。高优先级的线程高概率地被执行,并不意味着”一定是高优先级先执行,结束后再执行低优先级“。

代码:

  class HelloThread  extends  Thread{
@Override
public void run() {
    super.run();
    for (int i = 0; i < 100; i++) {
        //sleep()方法的测试:这个时候线程会执行地慢一些。
        if (i % 2 == 0) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + ":" + i + "," + getPriority());//线程优先级默认是5
        //yield()方法的测试:在线程执行到20的时候,必定会交出CPU执行权,让其他线程先执行一次。
  //            if (i % 20 == 0) {
  //                Thread.currentThread().yield();//Thread.currentThread()相当于this.
  //           }
          }
      }
      public HelloThread(String  name) {//构造器
         super(name);
     }
  

  public class ThreadMethodTest {
      public static void main(String[] args) throws InterruptedException {
    HelloThread  h1=new HelloThread("Thread1");//本代码选择在构造器中设置线程的名字。
    HelloThread  h2=new HelloThread("Thread2");//本代码选择在构造器中设置线程的名字。
    //设置名字是主线程所做的事。主线程这里指的是main()主线程 。
    //h1.setName("线程一");
    //设置线程的优先级。优先级的测试:主线程优先级最低,但是实际中,主线程也可能先执行。尤其说明,优先级高低,只是个概率事件。
    h1.setPriority(Thread.MAX_PRIORITY);
    h1.start();
    h2.setPriority(Thread.MAX_PRIORITY);
    h2.start();
    //为主main()线程命名
    Thread.currentThread().setName("主线程");//当前线程就是主线程
    Thread.currentThread().setPriority(Thread.MIN_PRIORITY);//设置优先级
    for (int i = 0; i <100 ; i++) {
        System.out.println(Thread.currentThread().getName()+":"+i+","+Thread.currentThread().getPriority());
        //join()方法的测试:在主线程执行到20的时候,被阻塞,线程执行完了,主线程才继续执行。
  //          if(i==20){
  //              h1.join();//强行加入线程h1.
  //          }
    }
    System.out.println(h1.isAlive());//测试线程是否执行完,执行完就是false.未执行完:true.
      }
  }

运行结果(每个人可能不一样,我这里是主线程先运行完,线程一线程二才运行的。主线程运行完,线程1的状态为true.):

方式三:实现Callable()接口(JDK5.0新增。该方法获取call()方法的返回值时需要借助FutureTask类)

1、创建一个实现Callable()接口的实现类。

2、实现Call方法,将此线程需要执行的操作声明在call()中。

3、创建实现Callable实现类的对象。

4、创建FutureTask类的对象,并将3中创建的实现类的对象作为参数传递到FutureTask的参数中。

5、将4中FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();

6、获取Call()方法的返回值。

代码:

  //1、创建一个实现Callable的实现类
  class NumberThread implements Callable<Integer> {
//2、实现Call方法,将此线程需要执行的操作声明在call()中。
@Override
public Integer call() throws Exception {
    int sum=0;
    for (int i = 1; i <=100; i++) {
        if(i%2==0){
            System.out.println(i);
            sum+=i;
        }
    }
            
    return sum;//自动装箱
      }
  }
  public class ThreadNew {
public static void main(String[] args) {
    //3、创建Callable接口实现类的对象
    NumberThread  numberThread=new NumberThread();
    //4、将此Callable接口实现类的对象作为参数传递到FutureTask的构造器中。创建FutureTask的对象。
    FutureTask<Integer> futureTask = new FutureTask<Integer>(numberThread);
    //5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread 对象,并调用start();
    new Thread(futureTask).start();//futureTask实现了Runnable接口
    //6、获取call方法的返回值。
    Object sum = null;
    try {//get()方法的返回值即为futureTask构造器参数Callable实现类重写的call()的返回值。
        sum = futureTask.get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println("总和为:"+sum);

}
  }

运行结果(打印0-100的偶数,并返回0-100的所有偶数的和):

方式二和方式三的比较

相比于run()方法,Callable()更加强大。call()方法可以有返回值;可以抛出异常;并支持泛型的返回值。

方式四:使用线程池

思路:经常创建和销毁,使用特别大的资源,比如并发情况下的线程,对性能影响很大。如果能提前创建好多个线程放入线程池中,在需要的时候直接获取,使用完再放回池中就很方

便了。这样能避免频繁地创建销毁线程,实现重复利用。

好处:1、能提高响应速度(减少了创建新线程的时间);

2、降低资源消耗(重复利用线程池,不需每次创建);

3、便于线程管理。

1、提供指定线程数量的线程池。

2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。Runnable()用service.execute()调用;Callable接口用service.submit()调用。

3、关闭连接池。

方法:ExecutorService,真正的线程池API.常见的子类:ThreadPoolExecutor。

void execute(Runnable command):执行任务/命令,没有返回值。一般用来执行Runnable。

Future submit(Callable task):执行任务,有返回值,一般用来执行Callable.

void shutdown:关闭连接池。

Executors:工具类,线程池的工厂类。用于创建并返回不同类型的线程池。

Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程的线程池。

代码:

  class  NumberThreadPool  implements Runnable{
@Override
public void run() {
    for (int i = 0; i <= 100; i++) {
        if(i%2==0){
            System.out.println(Thread.currentThread().getName()+"-"+i);
        }
    }
}
  }
  class  NumberThreadPool1  implements Runnable{
@Override
public void run() {
    for (int i = 0; i <= 100; i++) {
        if(i%2==1){
            System.out.println(Thread.currentThread().getName()+"-"+i);
        }
    }
}
  }
  public class ThreadPool {
public static void main(String[] args) {
   //1、提供指定线程数量的线程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    //设置线程池的属性
    ThreadPoolExecutor service1=(ThreadPoolExecutor) service;//强转
    System.out.println(service.getClass());
    service1.setCorePoolSize(15);
   // service1.setKeepAliveTime();
    //2、执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类的对象。
    //service.submit(new NumberThreadPool());//适合使用于Callable()
    service.execute(new NumberThreadPool());//适合使用于Runnable()//打印偶数
    service.execute(new NumberThreadPool1());//适合使用Runnable()//打印奇数
    //关闭连接池
    service.shutdown();
}
  }

运行结果:

二、线程的同步

先看一个窗口售票的例子:创建三个窗口卖票。总票数为100张

代码如下:

  class Window extends Thread{
private  static int ticket=100;//三个线程共享同一个ticket——总票数,
// 三个线程卖100张票


@Override
public void run() {
    super.run();
    while(true){
        if(ticket>0){
            System.out.println(getName()+":卖票,票号为"+ticket);
            ticket--;
        }else{
            break;
        }
    }
}
  }

  public class WindowTest {
public static void main(String[] args) {
    Window t1=new Window();
    Window t2=new Window();
    Window t3=new Window();

    t1.setName("窗口一");
    t2.setName("窗口二");
    t3.setName("窗口三");


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

}
  }

运行结果:出现了重票,存在线程安全问题。

分析一下,为何存在线程安全问题?---因为存在共享数据ticket,三个线程操作一个变量。

1、问题一:出现了重票错票问题。

2、问题二:出现的原因:当某个线程来操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作了车票

3、问题三:如何解决?当一个线程在操作ticket的时候,其他线程不能参与进来,直到线程A操作完以后,其他线程才可以操作。即使线程A出现了阻塞也不能被改变。

在Java中,我们通过同步机制来解决线程的安全问题。有如下三种方式。分别是同步代码块,同步方法,LOCK锁。有了同步的方式,操作代码时,只能有一个线程参与,其他线程等

待。相当于是一个单线程的过程。同步解决了线程的安全问题。
方式一:同步代码块 ,使用synchronized关键字。

synchronized(同步监视器){
//需要被同步的代码
}

说明:1、操作共享数据的代码,即为需要被同步的代码。不能包含多了,也不能包含少了。

2、共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。

3、同步监视器:俗称:锁。任何一个类的对象都可以来充当这个锁。

注意:多个线程必须共用一把锁。在实现Runnable时,可以考虑用this充当这个锁。

代码:

  //继承类的方法,同步监视器****慎用this
  class Window2 extends Thread {
private static int ticket = 100;//三个线程共享同一个ticket——总票数,
// 三个线程卖100张票
private  static Object obj = new Object();//保证三个共享同一个数据

@Override
public void run() {
    super.run();
    while (true) {//Class class =Window2.class->
        //synchronized (Window2.class) {
        synchronized (obj) {//此时不能用this,因为this代表着t1,t2,t3.此时锁不唯一。
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + ":卖票,票号为" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}
  }
  public class WindowTest2 {
      public static void main(String[] args) {
    Window2 t1=new Window2();
    Window2 t2=new Window2();
    Window2 t3=new Window2();

    t1.setName("窗口一");
    t2.setName("窗口二");
    t3.setName("窗口三");


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

}
  }
  //实现Runnable接口的方法
  class Window1 implements  Runnable {
private int ticket = 100;//不用加static
Object obj = new Object();//obj就是一个锁。

@Override
public void run() {
    //Object obj = new Object();//obj只能是一个锁。唯一性
    while (true) {
        synchronized (this) {//此时this代表的就是唯一的window1对象。
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}
  }
    public  class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();//共用一个window1对象
        Thread t1 = new Thread(w);//w作为参数传递给线程Thread
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果(线程安全):

方式二:同步方法

如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的(使用synchronized关键字)。

关于同步方法的总结:

1、同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。

2、非静态的同步方法,同步监视器是:this。

静态的同步方法,同步监视器是:当前类本身。

代码:

  //使用同步方法来处理继承Thread类的方式中线程安全问题
  class Window4 extends Thread {
private static int ticket = 100;//三个线程共享同一个ticket——总票数,
@Override
public  void run() {
    super.run();
    while (true) {
       show();

        }
    }
private   static synchronized void show(){//同步监视器:t1,t2,t2
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
            ticket--;
        }
    }

}

  public class WindowTest4 {
      public static void main(String[] args) {
    Window4 t1=new Window4();
    Window4 t2=new Window4();
    Window4 t3=new Window4();


    t1.setName("窗口一");
    t2.setName("窗口二");
    t3.setName("窗口三");


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

}
  }
  //使用同步方法解决实现Runnable接口的线程安全问题
  class Window3 implements  Runnable {
      private int ticket = 100;//不用加static


@Override
public void run() {
    while (true) {
        show();
    }
}
//同步方法
private  synchronized void show(){
//synchronized(this){
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
            ticket--;
        }
        }
  }
  public  class WindowTest3 {
      public static void main(String[] args) {
    Window3 w = new Window3();//共用一个window1对象
    Thread t1 = new Thread(w);//w作为参数传递给线程Thread
    Thread t2 = new Thread(w);
    Thread t3 = new Thread(w);
    t1.setName("窗口一");
    t2.setName("窗口二");
    t3.setName("窗口三");
    t1.start();
    t2.start();
    t3.start();
}
  }

方式三:LOCK锁(JDK5.0新增)

方式一和方式二都使用到了synchronized关键字。那么,synchronized与LOCK有何异同?

相同:二者都可以解决线程安全的问题。

不同之处:synchronized机制在执行完相应的代码逻辑以后,自动地释放同步监视器。

另外需要注意的是LOCK需要手动地启动同步:lock(),同时结束也是需要手动的实现:unlock()。

实现LOCK锁步骤:

1、创建锁的对象;

2、上锁:调用lock()方法;

3、解锁:调用unlock()方法。

代码:

  class Window  implements Runnable{
     private int ticket=100;
     //实例化lock
    private ReentrantLock  lock=new ReentrantLock(true);//1、创建锁的对象


@Override
public void run() {
    while(true){
        try{
            //2、调用lock方法
            lock.lock();
            if (ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖票:"+"票号为"+ticket);
                ticket--;
            }else{
                break;
            }
        }finally{
            //3、解锁的方法:unlock

            lock.unlock();
        }

    }
}
  }

  public class LockTest {
      public static void main(String[] args) {
    Window  w=new Window();
    Thread t1=new Thread(w);
    Thread t2=new Thread(w);
    Thread t3=new Thread(w);

    t1.setName("窗口一");
    t2.setName("窗口二");
    t3.setName("窗口三");

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

三、线程通信

下面以一个简单的例子来理解一下线程通信的方法。

  //两个线程交替打印1到100
  class  Number implements Runnable{
      private  int number =1;//共享数据,线程安全问题

@Override
public void run() {
    while(true){
        synchronized (this) {
            //唤醒
            notify();//notifyAll():唤醒多个。
            if(number<=100){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+number);
                number++;
                //使得调用如下wait()方法的线程进入阻塞状态
                try {
                   wait();//wait完了以后会释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            else {
                break;
            }
        }
    }
}
  }
  public class CommunicationTest {
public static void main(String[] args) {
    Number number = new Number();
    Thread t1 = new Thread(number);
    Thread t2= new Thread(number);
    t1.setName("线程一");
    t2.setName("线程二");
    t1.start();
    t2.start();
  }
}

运行结果:

线程通信有三个方法:wait(),notify(),notifyAll()。

wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。wait()与sleep()不同,在被运用同步代码块中时,sleep()休眠以后,不会释放锁。但是wait()执

行完后会释放锁。

notify():一旦执行此方法,就会唤醒被wait()的一个线程。如果有多个线程,就会唤醒优先级高的线程。

notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明:

1、wait(),notify(),notifyAll()只能出现在同步块或同步方法中。

2、wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现MonitorStateException异常。

3、三个方法定义在Object类中。方便任何一个对象充当同步监视器的调用。

线程通信的应用:生产者消费者问题

分析:是多线程问题。生产者线程,消费者线程。

是否有共享数据?是,店员或产品的数量。

如何解决线程的安全问题?同步机制,三种方式。

代码:

  class Clerk {
      private int productCount = 0;//生产的产品数量

//以下两个方法需要同步,否则存在线程安全问题
//生产产品(同步方法)
public synchronized void produceProduct() {
    if (productCount < 20) {
        productCount++;
        System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
        notify();
    } else {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

//消费产品
public synchronized void consumerProduct() {
    if (productCount > 0) {
        System.out.println(Thread.currentThread().getName() + "开始消费第" + productCount + "个产品");
        productCount--;
        notify();
    } else {//等待
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  }

  class Producer extends Thread {
      private Clerk clerk;

      public Producer(Clerk clerk) {
    this.clerk = clerk;

      }

@Override
public void run() {
    super.run();
    System.out.println(Thread.currentThread().getName() + ":开始生产产品.......");
    while (true) {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        clerk.produceProduct();//生产

    }
}
  }

  class Consumer extends Thread {
      private Clerk clerk;

      public Consumer(Clerk clerk) {
      this.clerk=clerk;
}

@Override
public void run() {
    super.run();
    System.out.println(Thread.currentThread().getName() + ":开始消费产品.......");
    while (true) {
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        clerk.consumerProduct();//消费

    }
}
  }

  public class ProduceTest {
      public static void main(String[] args) {
    Clerk clerk = new Clerk();
    Producer p1 = new Producer(clerk);
    p1.setName("生产者1");

    Consumer c1 = new Consumer(clerk);
    c1.setName("消费者1");
    Consumer c2= new Consumer(clerk);
    c2.setName("消费者2");
    p1.start();
    c1.start();
    c2.start();
}
  }

运行结果:

posted @ 2020-11-13 17:53  heyhy  Views(46)  Comments(0Edit  收藏  举报
Title