Java--多线程

Java–多线程

1.对多线程的基础理解

        线程是进程执行的一个路线。在同一个进程中的,线程共享进程的内存空间,线程间可以自由切换,并发执行。在Java开发的过程中,设计的应用程序,往往都是多个线程同时工作的,这样能够提高程序的执行效率,但是也带来了编程的复杂,需要解决多线程之间资源分配的问题。在本篇的博客中,主要记录的是Java实现多线程的几个方式,以及基础的概念和常用的方法操作。

2.创建线程的方式

  1. 继承Thread类,重写run方法

  2. 实现Runnable接口,编写run方法

  3. 实现Callable接口,编写call方法

    1) 继承Threead的方式,参考例子如下:

    // 定义MyThread继承Thread类型,并重写run方法,在run方法内编写创建的线程要执行的任务。
    public class MyThread extends Thread {
        // 线程要执行的方法
        // 通过thread对象的start方法执行,是一条新的执行路径
        @Override
        public void run() {
            for (int i=0;i<100;i++){
                System.out.println("窗前明月光"+i);
            }
        }
    }
    
    // 创建MyThread类,并调用start方法,然后线程会开始执行run方法
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i=0;i<100;i++){				
            System.out.println("疑是地上霜"+i);
        }
    }
    

    通过上面运行的程序可以观察到,myThread执行的任务和主线程打印任务是有交叉执行的,说明Java中线程之间的运行是抢占式的,线程抢到资源后,可以执行,没有抢到足够资源的线程需要等待下一次的抢占。

    2)实现Runnable的方式,可以参考下面的例子

    // 定义MyRunnable实现Runnable接口,并实现run方法,run方法内编写线程执行的任务。
    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i=0;i<20;i++){
                System.out.println("锄禾日当午"+i);
            }
        }
    }
    
    // 将MyRunnable对象作为创建Thread对象的参数,然后调用start方法执行线程
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        for (int i=0;i<20;i++){
            System.out.println("汗滴禾下土"+i);
        }
    }
    

    特点:

    1. 通过创建任务,给线程分配任务,更适合多个线程同时执行相同的任务;
    2. 可以避免单继承的局限性;
    3. 任务和线程本身是分离,提高程序的健壮性
    4. 线程池的操作,只支持实现Runnable接口的类型的任务,不接受继承Thread的线程
    

3)实现Callable方法

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 1. 创建MyCallable对象
    MyCallable myCallable = new MyCallable();
    // 2. 创建FutureTask对象,并将MyCallable对象作为创建对象的参数
    FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
    // 3. 调用Thread对象的start方法执行线程
    new Thread(futureTask).start();   // 执行线程
    // 4. 可以通过get方法获取线程执行完成时的返回值
    System.out.println(futureTask.get()); // 输出任务返回值
}

特点: 可以携带线程任务执行完成后返回的参数

3. 线程的一些常用方法

  1. 设置和获取线程的名字

    // 设置当前线程的名字
    Thread.currentThread().setName("线程1");
    // 获取当前线程的名字
    Thread.currentThread().getName();
    
  2. 线程休眠函数

    Thread.sleep(1000);   // 单位是ms
    
  3. 线程中断

    // 终端线程执行可以调用interrupt()函数,通过中断可以终止执行的线程
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for (int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            Thread.sleep(1000);
        }
        t1.interrupt();         // 中断t1线程,通知他进行其他操作,可以是终止自身线程,或者是进行其他的操作
    }
    
    static class MyRunnable implements Runnable{
    
        @Override
        public void run() {
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //   e.printStackTrace();
                    // 接收到中断信号时,终止任务/或者开始其他的任务
                    System.out.println("接收到中断信号时,终止任务");
                    return;		// 终止任务
                }
            }
        }
    }
    
  4. 设置守护线程

    通过调用线程对象的setDaemon()方法,并传入true可以设置线程为守护线程

    Java中的线程可以分成用户线程和守护线程,下面是这两种线程的特点;
    用户线程,当进程中没有一个用户线程的时候,进程结束
    守护线程,守护用户线程,当最后一个用户线程结束时,所有的守护线程死亡

    举例如下:

    public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new MyRunnable());
            t1.setDaemon(true);				// 设置t1为守护线程,如果所有的用户线程结束了,t1守护线程也会停止执行
            t1.start();
            for (int i=0;i<5;i++){			// 比较快结束
                System.out.println(Thread.currentThread().getName()+":"+i);
                Thread.sleep(1000);
            }
        }
        static class MyRunnable implements Runnable{
            @Override
            public void run() {
                for (int i=0;i<10;i++){		// 在所有用户线程结束时,作为守护线程的它也会结束
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
    

4.线程同步

​ 在多线程的环境下,线程的执行往往是多样的,当这多个线程在同时对一个数据进行更新操作的时候,可能会因为同时操作的原因,导致数据出现错误,不满足实际的情况。在这种情况下,如果需要保证数据的安全可靠,就要是每个对数据进行更新的线程排队执行,这样才能保证数据不会超出合理的范围,保证每次操作的合理性。这种排队的行为是线程同步的一种具体解释.

Java中为了保证线程安全,使线程同步执行的方式有以下三种:

  1. 同步代码块

  2. 同步方法

  3. 显示锁Lock

下面详细介绍这三种的使用方法,并给出一些例子:

首先给出一个会出现线程不安全的情况,这个例子演示的是多个线程同时进行买票,最后会出现剩余票数是负数的情况,这是不合理的现象。示例如下:

// 模拟线程不安全的情景:买票线程
public static void main(String[] args) {
    Ticekt ticekt = new Ticekt();	// 一个对象
    new Thread(ticekt).start();		// 三个买票线程
    new Thread(ticekt).start();
    new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
    private int num = 10;					// 余票数量
    @Override
    public void run() {
        while (num>0){						// 票数>0,继续卖
            System.out.println("准备买出一张票");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("出票成功!");
            num--;
            System.out.println("当前余票数:"+num);
        }
    }
}

1.同步代码块

同步代码块使用关键字synchronize,参数是一个锁对象,然后将同步执行的代码块用{}包围。当遇到同一个锁对象的时候,这些线程会同步执行,如果使不同的锁对象的时候没有任何影响。因此,如果希望同步执行的话,要注意锁对象要是同一个对象。

修改上面不安全的买票程序,做出下面的例子:

    // synchronize,利用锁对象,
    // 当要进入同一个锁的代码块的时候会先检查这个锁有没有被占用,
    // 被占用则等待,否则占用锁进入代码块
    public static void main(String[] args) {
        Ticekt ticekt = new Ticekt();
        new Thread(ticekt).start();
        new Thread(ticekt).start();
        new Thread(ticekt).start();
    }
    static class Ticekt implements Runnable{
        private int num = 10;
        private Object o  =new Object();            // 锁对象
        @Override
        public void run() {
            while (true){
                synchronized (o){				  // 同步代码块开始
                    if (num>0){
                        System.out.println("准备买出一张票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("出票成功!");
                        num--;
                        System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
                    }else
                        break;
                }								// 同步代码块结束
            }
        }
    }
  1. 同步方法

    同步方法的实现和上面的同步代码块类似,也使用synchronized,但是它修饰的对象变成了方法。当修饰的是普通方法的时候,锁对象是当前对象本身,即this;如果修饰的是静态方法的时候,锁对象则是这个类的字节码文件对象。

    下面只做修饰普通方法的举例,修饰静态方法的情景类似:

    public static void main(String[] args) {
        Ticekt ticekt = new Ticekt();
        new Thread(ticekt).start();
        new Thread(ticekt).start();
        new Thread(ticekt).start();
    }
    static class Ticekt implements Runnable{
        private int num = 10;
        @Override
        public void run() {
            while (true){
                boolean flag = sale();
                if (!flag)
                    break;
            }
        }
        // 买票方法,同步执行,
        // 不是静态方法,锁对象为this,即调用的对象
        // 是静态方法,锁对象为Ticke.class,字节码文件对象
        // 同一个类的对象会共用this这个锁,可以会影响其他同步方法的执行
        public synchronized boolean sale(){
            if (num>0){
                System.out.println(Thread.currentThread().getName()+":准备买出一张票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":出票成功!");
                num--;
                System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
                return true;
            }
            return false;
        }
    }
    
    1. 使用显示锁Lock

      同步代码块和同步方法实现都是基于隐式锁。

      使用实现接口Lock的类ReentrantLock的方式是基于显示锁。

      显示锁比较适合自定义加锁和解锁,带来个性化的同时也可能会带来加锁后忘解锁的情况,在使用的过程中,需要充分考虑到加锁后在哪个地方再解锁,防止程序进入死锁,无法释放资源。下面是使用案例,也是基于买票程序的:

      public static void main(String[] args) {
          Ticekt ticekt = new Ticekt();
          new Thread(ticekt).start();
          new Thread(ticekt).start();
          new Thread(ticekt).start();
      }
      static class Ticekt implements Runnable{
          private int num = 10;
          //  用实现接口Lock的类ReentrantLock创建对象
          private Lock l= new ReentrantLock();
          @Override
          public void run() {
              while (true){
                  l.lock();				// 上锁资源访问
                  if (num>0){
                      System.out.println(Thread.currentThread().getName()+":准备买出一张票");
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName()+":出票成功!");
                      num--;
                      System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
                      l.unlock();                  // 操作成功,解锁
                  }else{
                      l.unlock();                 // 没有买票,break前需要释放锁,防止后面的线程被锁住不能释放
                      break;
                  }
              }
          }
      }
      

5.总结

        以上的介绍,总结了Java中使用多线程的基本内容,包括线程的创建的方式,线程常用的方法,实现线程同步的方法。在我们实际开发中,情景往往都是复杂的,可能会有大量的请求会同时发出,这个时候就需要合理的处理号这些请求,利用多线程的思想去解决这些问题。认识并使用多线程后,能够使自己开发的程序更加符合实际开发的需求,提高程序的执行效率。Java中的多线程的知识还很多,比如线程池。上面的介绍只是基础入门,想要深入学习的话,可以继续查找资料,或者找些项目实践。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值