chapter12 多线程(二)

多线程的新增创建方式

实现Callable接口实现多线程的创建

基本的步骤:
1、新建一个类Test,从而实现Callable这个接口
2、在Test这个类中,重写Callable这个接口的call方法,从而设置线程的执行内容。注意这个call方法有返回值,其返回值类型为Object,并且声明了异常
3、在主函数中,新建Test类(即Callable接口的子类)的对象test
4、新建FutureTask这个类的对象f,并且它的构造方法的参数就是上面刚刚新建的Test类的对象test
5、新建Thread类的对象thread,并且它的构造方法的参数是FutureTask类对象f
6、通过调用Thread类的对象thread的start方法,从而启动线程,然后执行Callable子类中的call方法。

代码实例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//步骤1、新建一个子类,使得这个类实现Callable接口
class Test implements Callable {

    @Override
    //步骤2、重写Callable接口的call方法,从而设置线程的内容,注意这个call方法有返回值,并且声明了异常
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 0; i<=10; i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+": "+i);
                sum += i;
            }
        }
        return sum;
    }
}
public class CallableImpl {
    public static void main(String[] args){
        //步骤3、在主函数中,新建Callable子类对象
        Test test = new Test();
        //步骤4、新建FutureTask类对象,并且它的构造方法的参数是上面刚刚新建的Callable子类对象
        FutureTask f = new FutureTask(test);
        //步骤5、新建Thread类对象,并且它的构造方法的参数是上面刚刚新建的FutureTask对象
        Thread thread = new Thread(f);
        thread.start();//步骤6、通过Thread类对象,调用start方法,从而启动线程
        //步骤7、如果要获取callable子类中的call方法的返回值,那么就要调用上面FutureTask对象的get方法获取,不过
        //需要注意要用try/catch包住,因为call方法声明了异常
        try {
            Object result = f.get();//获取结果
            System.out.println("结果为:"+result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

结果为:
在这里插入图片描述

注意这里实现Callable接口创建线程和实现Runnable接口创建线程相比,前者功能更强大:
前者中的call方法含有返回值,但是如果通过实现Runnable接口创建线程,那么run方法则没有返回值
前者在方法声明后抛出了异常,表明了可能会出现异常,但是后者并没有声明可能会抛出异常,只能在run方法内部用try/catch异常。
Callable接口支持泛型,但是后者不支持泛型

Runnable接口:

public interface Runnable {//不支持泛型
    public abstract void run();//没有返回值,没有声明异常
}

Callable接口:

public interface Callable<V> {//Callable接口支持泛型
    V call() throws Exception;//有返回值,并且声明了异常
}

通过线程池实现多线程

线程1代码(通过实现Runnable接口创建线程):

class RunnableSubClass implements Runnable{

    @Override
    public void run() {
        for(int i = 0; i<10; i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }
    }
}

线程2(通过实现Callable接口创建线程):

class CallableSubClass implements Callable {

    @Override
    public Object call() throws Exception {
        for(int i = 0; i<10; i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }
        return null;
    }
}

线程3(通过继承创建线程):

class ThreadSubClass extends Thread{
    @Override
    public void run(){
        for(int i = 0; i<10; i++){
            if(i % 3 == 0){
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }
    }
}

测试类代码:

public class PoolThread {
    public static void main(String[] args){
        //步骤一:新建线程池,newFixedThreadPool()里的参数表示线程池中的线程数
        ExecutorService service = Executors.newFixedThreadPool(3);

        //步骤二:创建线程,将线程存放到线程池中,并进行执行
        //通过实现Runnable接口创建线程,然后将线程放到线程池中
        RunnableSubClass subClass1  = new RunnableSubClass();//创建Runnable接口的子类
        Thread thread = new Thread(subClass1);//新建线程
        thread.setName("线程1");

        //通过实现Callable接口创建线程,然后将线程放到线程池中
        CallableSubClass subClass2 = new CallableSubClass();
        FutureTask f = new FutureTask(subClass2);
        Thread thread1 = new Thread(f);
        thread1.setName("线程2");//将线程2添加到线程之后,如果通过这样的方式修改线程的名字,实际上没有修改

        //通过继承创建线程
        ThreadSubClass thread2 = new ThreadSubClass();

        //将线程存放到线程池中,并执行
        service.execute(thread);//调用execute方法,适用于Runnable接口
        service.submit(thread1);//调用submit方法,适用于Callable接口
        service.submit(thread2);

        //步骤三:线程执行完毕之后,将线程池关闭,如果不关闭,虽然没出现异常,也没有错误提示,但是会一直运行
        service.shutdown();
    }
}

结果为:
在这里插入图片描述
这里有几点不太明白:execute()适用Runnable接口,submit()适用于Callable接口,那么为什么execute里面的参数可以是通过实现Callable接口的线程?为什么可以是通过继承实现的线程?同样的,为什么submit()里的参数可以是通过实现Runnable接口的线程?为什么可以是通过继承实现的线程?如果有大佬知道的,请大家指教。

线程的同步

线程的同步,用到了一个关键字synchronized

同步代码块

对于通过实现Runnable接口创建的线程来说,synchronized()括号里的参数,即同步监视器(俗称锁)可以用this来表示,因为这个类就是被所有的线程所共享的,因此可以用this来表示。但是如果是通过继承来创建的线程来说,则需要慎用this,因为无法保证这把锁能够被多个线程所共用,所以要慎用,但是它可以用子类的类名.class,即synchronized(类名.class)
格式:

synchronized(同步监视器){
    //操作共享数据的代码,不可以多,也不可以少
 } 
 所谓的共享数据:就是多个线程共同操作一个变量,就好像下面的实例中的票
 同步监视器:俗称锁,任何一个类的对象都可以作为锁,要求是多个线程必须共同使用同一个锁。

同步方法

对于通过实现Runnable接口创建的线程来说,同步方法只要将需要被同步的方法被synchronized修饰即可,但是对于通过继承实现的线程来说,不单单是要求这个方法被synchronized修饰就可以了,还要求他是静态的,只有这样才能保证被所有的对象所共享。注意在实现Runnable接口创建的线程中,同步监视器默认为this,但是通过继承创建的线程来说,同步监视器则默认为类名.class

实例:多窗口卖票
方法一:同步方法实现,从而解决线程的安全问题:
1、通过继承来创建的线程:

class ThreadTest5 extends Thread{
    private static int ticket = 10;//共享数据,所以定义为静态的,所以涉及到ticket的代码一定要在同步方法中,否则会出现只有一个线程或者出现错票的问题
    @Override
    public void run(){
        while(true){
            show();
        }
    }
 /**
  * 如果要通过同步方法处理Thread类的线程安全问题(即通过继承创建的线程),
  * 那么需要将这个方法设置为静态的,否则这个方法是不唯一的,线程依旧不安全
 */
    public static synchronized void show(){//同步监视器:ThreadTest5.class
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName()+": "+ticket);
                ticket--;
            }else{
                System.exit(0);//退出虚拟机
            }
    }
}
public class DemoThread6 {
    public static void main(String[] args){
        ThreadTest5 test1 = new ThreadTest5();
        ThreadTest5 test2 = new ThreadTest5();
        ThreadTest5 test3 = new ThreadTest5();

        test1.setName("窗口1");
        test2.setName("窗口2");
        test3.setName("窗口3");

        test1.start();
        test2.start();
        test3.start();
    }
}

结果为:
在这里插入图片描述
2、通过实现Runnable创建的线程:

class ThreadTest3 implements Runnable{
    private int ticket = 10;
    @Override
    public void run () {
        while (true) {
//            if(ticket > 0)//如果是这样的,那么操作共享数据代码就会少,所以会出现错码
//                show();
//            else
//                break;
            show();
        }
    }
    //在返回值之前添加synchronized修饰,从而成为同步方法,使用synchronized时,操作共享数据的代码不可以多,也不可以少
    public synchronized void show(){//同步监视器:this
        if(ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ": " + ticket);
            ticket--;
        }else{
            System.exit(0);
        }
    }
}
public class DemoThread2 {
    public static void main(String[] args){
        ThreadTest3 test3 = new ThreadTest3();
        Thread thread = new Thread(test3);
        Thread thread1 = new Thread(test3);
        Thread thread2 = new Thread(test3);
        thread.setName("窗口1");
        thread1.setName("窗口2");
        thread2.setName("窗口3");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

结果:
在这里插入图片描述
方法二:同步代码块实现,从而解决线程的安全问题:
1、通过继承创建的线程:

class Window extends Thread{
    private  static  int ticket = 10;//定义一个私有的静态变量,从而能够被所有对象所共享
    static Object obj = new Object();//要将这个同步监视器设置为静态的才可以被多个线程所共享
    @Override
    public void run(){
           //synchronized()括号后面是一个同步监视器,俗称锁,任何一个类的对象都可以作为锁,从而解决了线程的安全问题
           while(true) {
              synchronized(obj){//同步监视器:obj(任何一个类的对象都可以充当同步监视器)
        //synchronized()里面的代码是操作共享数据的代码,涉及到了ticket的判断、赋值等
                  if (ticket > 0) {
                      System.out.println(getName() + ": 票数 " + ticket);
                      ticket--;
                  } else {
                      break;
                  }
              }
           }
    }
}
public class DemoThread5 {
    public static void main(String[] args){
        Window window1 = new Window();
        Window window2 = new Window();
        Window window3 = new Window();//定义三个窗口

        window1.setName("窗口1 ");
        window2.setName("窗口2 ");
        window3.setName("窗口3 ");
        window1.start();//启动线程,调用run方法
        window2.start();
        window3.start();


    }
}

结果:
在这里插入图片描述
2、通过实现Runnable接口创建的线程:

class ThreadTest2 implements  Runnable{
    private int ticket = 10;//由于是只有这个一类作为参数,那么就可以不用共享了
    @Override
    //重写Runnable类的run方法
    public void run() {
        while(true){
            synchronized(this) {//同步监视器:this
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ": " + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class DemoThreadTest {
    public static void main(String[] args){
        //新建Runnable类子类对象
        ThreadTest2 test = new ThreadTest2();
        //构建Thread类对象,其中test作为它的构造方法的参数
        Thread thread1 = new Thread(test);
        Thread thread2 = new Thread(test);
        Thread thread3 = new Thread(test);
        thread1.setName("线程1");//调用方法setName,从而设置线程名字
        thread2.setName("线程2");//调用方法setName,从而设置线程名字
        thread3.setName("线程3");//调用方法setName,从而设置线程名字
        //调用thread的start方法,从而启动线程,调用Thread类中的Runnable类型的target的run方法
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

结果:
在这里插入图片描述

线程的通信

实例:两个线程交替打印0-10的数字
线程的通信中涉及到几个方法:
①wait():一旦执行这个方法,就会将线程1处于阻塞的状态,并且释放同步监视器,从而让其他的线程参与进来
②notify():一旦执行这个方法,那么就会唤醒被wait的一个线程,如果有多个线程,那么优先级高的线程先被唤醒
③notifyAll():一旦执行这个方法,那么就会唤醒被wait的所有线程。注意notify()和notifyAll()的区别,从而如果有两个线程的话,那么执行notify()方法就可以实现交替,但是如果有超过3个线程(包括3)那么就要执行notifyAll()方法才可以实现交替

注意:
①这三个方法都是Object类中的,而不是Thread类的
②这三个方法必须是在同步代码块或者同步方法中使用
③这三个方法是被同步监视器所调用的,也正因如此,更加论证了①的观点,因为同步监视器是任何一个类的对象都可以的,所以无论是哪一个类的对象都可以调用这些方法,因为他们都是Object的子类。

代码:

同步方法中的线程通信:

class Number5 extends Thread{
    private static int number = 1;//因为是通过继承实现线程,因此这个变量要被所有的对象共享,就要定义为静态的
    @Override
    public void run(){
        while(true) {
            show();
        }
    }
    //在通过继承的线程中,如果要写同步方法,那么这个方法要被static修饰才可以被所有的对象所共享
    public static synchronized void show(){//同步监视器:Number5.class
        //在通过继承的线程中同步监视器慎用this,但是可以用类名.class充当同步监视器
            //调用notify方法,从而唤醒被wait的一个线程,如果有多个被wait的线程,那么优先级高的线程会被唤醒
            Number5.class.notifyAll();//如果只有两个线程,那么调用notify就可以实现线程的交替,但是如果有超过3(包括3)个线程的时候,则要调用notifyAll才可以
            if(number <= 10){
                System.out.println(Thread.currentThread().getName()+": "+ number);
                number++;
            }else{
                System.exit(0);//退出虚拟机
            }
            try {
                //调用wait方法的时候,会抛出异常,因此要用try/catch进行捕获处理
                //如果这里没有写同步监视器的话,那么就会默认为this,此时this不是同步监视器,因为同步监视器必须要求是所有的线程共用一个锁,因此从而报错
                Number5.class.wait();//同步监视器调用wait方法,从而会是当前的线程处于阻塞的状态

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

    }
}
public class CommunicationTestIherit2 {
    public static void main(String[] args){
        Number5 num1 = new Number5();
        Number5 num2 = new Number5();

        num1.setName("线程1");
        num2.setName("线程2");

        num1.start();
        num2.start();
    }
}

结果:
在这里插入图片描述
同步代码块中的线程通信:

class Number4 extends Thread{
    private static int number = 1;//因为是通过继承实现线程,因此这个变量要被所有的对象共享,就要定义为静态的
    @Override
    public void run(){
        while(true){
            //在通过继承的线程中同步监视器慎用this,但是可以用类名.class充当同步监视器
            synchronized(Number4.class){//同步监视器:Number5.class
//调用notify方法,从而唤醒被wait的一个线程,如果有多个被wait的线程,那么优先级高的线程会被唤醒
                Number4.class.notify();//如果只有两个线程,那么调用notify就可以实现线程的交替,但是如果有超过3(包括3)个线程的时候,则要调用notifyAll才可以
                if(number <= 10){
                    System.out.println(Thread.currentThread().getName()+": "+ number);
                    number++;
                }else{
                    break;
                }
                try {
                    //调用wait方法的时候,会抛出异常,因此要用try/catch进行捕获处理
                    //如果这里没有写同步监视器的话,那么就会默认为this,此时this不是同步监视器,因为同步监视器必须要求是所有的线程共用一个锁,因此从而报错
                    Number4.class.wait();//同步监视器调用wait方法,从而会是当前的线程处于阻塞的状态

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}
public class CommunicationTestIherit {
    public static void main(String[] args){
        Number4 num1 = new Number4();
        Number4 num2 = new Number4();

        num1.setName("线程1");
        num2.setName("线程2");

        num1.start();
        num2.start();
    }
}

结果:
在这里插入图片描述

sleep() 和wait()的异同

相同之处:一旦被调用,那么两者都会使线程处于阻塞的状态。
不同之处:
①声明的位置不同:sleep()是在Thread类中生命的,wait()是在Object类中声明的
②调用的要求不同:sleep()可以在任何的位置中调用,但是wait()方法只能在同步代码块或者同步方法中声明
③是否释放同步监视器:如果两个方法都在同步方法或者同步代码块中,那么sleep()不会释放同步监视器,而wait()会释放同步监视器,从而让其他的线程参与进来

如果有说的不好的或者错误的地方,请大家指正,会改正的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值