Java基础知识——8、多线程

8 多线程

8.1 什么是线程?为何使用多线程?

线程是指程序在执行过程中,能够执行程序代码的一个执行单元。进程是指一段正在执行的程序。

在Java语言中线程有四种状态:运行、就绪、挂起、结束。

线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程能拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源,各个线程用友自己的栈空间。

  在操作系统中,程序的执行都是以进程为单位的,而每个进程中都有多个线程,这些线程会互不影响的并发执行。多线程的使用为程序带来了巨大的便利:

1)可以减少程序响应时间:如果某个操作很耗时或者陷入长时间等待的情况下,可以把这个耗时的线程分配到一个独立的线程去执行,从而使程序具备了更好的交互性。

2)较线程创建和切换的开销小:由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,
而运行于同一进程的线程共享代码段、数据段,所以线程的启动和切换开销会小。同时多线程在数据共享方面效率非常高。

3)多线程可以提高CPU的利用率:多CPU或多核计算机本身就具备执行多线程的能力,如果使用单线程,将无法重复利用计算机资源,造成资源浪费。

4)多线程能简化程序结构:一个非常复杂的进程可以拆分为多个线程来执行。



8.2 同步与异步

  同步
  在多线程环境中经常会碰到数据共享问题,即当多个线程需要同时访问一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则,程序结果将是不可预料的,在这种情况下就必须对数据进行同步。
  比如 线程A 正在读取资源,此时 线程B 需要使用这个资源,同步机制此时就会让线程B一直等下去,直到线程A结束对该资源的使用。

  想要实现同步操作就必须要获取每一个线程对象的锁。获得它可以确保在同一时刻只有一个线程能够进入临界区,并且在这个锁被释放之前,其他线程不可以进入这个临界区。如果还有其他线程想要获取到该对象的锁,那么只能进入等待队列等待,当拥有该对象锁的线程退出临界区后,锁会被释放,此时等待队列中优先级最高的线程可以去获取到对象锁。从而进入临界区。

  在Java语言中可以使用 synchronized 关键字来实现同步,但是同步控制并非越多越好,它是以很大的系统开销作为代价的,有时候甚至会造成死锁。
实现同步的方式有两种:1、利用同步代码块来实现同步      2、利用同步方法来实现同步


  异步
  异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此在进行输入输出处理时,不必关心其他线程的状态和行为,也不需要等到输入输出处理完毕后才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法返回时,就可以使用异步。异步可以提高程序的效率


8.3 实现多线程

  Java虚拟机允许程序并发的运行多个线程。在Java语言中多线程有三种实现方式。

1)继承 Thread 类,重写 run() 方法。
Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例,并且启动线程的唯一方法就是通过 Thread 类的 start() 方法。start() 方法是一个本地方法,它将启动一个新的线程并执行 run() 方法。
例:

class MyThread extends Thread{
    public void run(){
        System.out.println("线程体");
    }
}

public class Main{
    public static void main(String[] args) {
       MyThread thread = new MyThread();
       thread.start();
    }
}

结果:
线程体

2)实现 Runnable 接口,并实现该接口的 run() 方法。
1、自定义类实现 Runnable 接口,并实现该接口的 run() 方法。
2、创建 Thread 对象,实现 Runnable 接口的对象作为参数实例化该 Thread 对象。
3、调用 Thread 的 start() 方法。
例:

class MyThread implements Runnable{
    public void run(){
        System.out.println("线程体");
    }
}

public class Main{
    public static void main(String[] args) {
       MyThread myThread = new MyThread();
       Thread thread = new Thread(myThread);
       thread.start();
    }
}

结果:
线程体

3)实现 Callable 接口,重写 call() 方法。
Callable 接口其实是属于Executor 框架中的功能类, Callable 接口与 Runnable 接口的功能相似,但 Callable 提供了比 Runnable 更强大的功能。
1、 Callable 可以在任务结束后提供一个返回值
2、 Callable 接口中的 call() 方法可以抛出异常
3、运行 Callable 可以拿到一个 Future 对象, Future 对象表示异步计算的结果,它提供了检查计算是否完成的方法。
例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


public class Main {

    //创建线程类
    public static class CallableTest implements Callable<String> {
        public String call() throws Exception {
            return "线程体";
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //启动线程
        Future<String> future = executorService.submit(new CallableTest());
        try {
            System.out.println("线程等待结束");
            System.out.println(future.get());
        } catch ( Exception e) {
            e.printStackTrace();
        }

    }
}

结果:
线程等待结束
线程体

一般推荐实现 Runnable 接口的方式。
原因:首先,Thred 类定义了多种方法可以被派生类使用或者重写。但是只有 run() 方法是必须被重写的,在 run() 方法中实现这个线程的主要功能。其次,很多开发人员认为一个类仅在它们需要被加强或修改时才会被继承,因此如果没有必要重写 Thred 类中其他方法,那么通过继承 Thred 的实现方式和实现 Runnable 接口的效果是相同的,在这种情况下最好通过实现 Runnable 接口的方式来创建线程。


8.4 run() 方法与 start() 方法

  系统调用线程类的 start() 方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的 run() 方法来完成实际操作,当 run() 方法结束后这个线程就会终止。
  如果直接调用线程类的 run() 方法,这只会被当作一个普通函数被调用,程序中仍然只有主线程这一个线程,也就是说 start() 方法可以异步调用 run() 方法,但直接调用 run() 方法却是同步的,因此无法达到多线程的目的。、
例:

class MyThred extends Thread {
    @Override
    public void run() {
        System.out.println("线程:启动");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("线程:结束");
    }
}


public class Main {

    public static void test1() {
        System.out.println("测试1:启动");
        Thread thread1 = new MyThred();
        thread1.start();
        System.out.println("测试1:结束");
    }

    public static void test2() {
        System.out.println("测试2:启动");
        Thread thread1 = new MyThred();
        thread1.run();
        System.out.println("测试2:结束");
    }

    public static void main(String[] args) {
        test1();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println();
        test2();
    }
}

结果:
测试1:启动
测试1:结束
线程:启动
线程:结束

测试2:启动
线程:启动
线程:结束
测试2:结束



8.5 多线程同步的实现方法

当使用多线程来访问同一个资源时,非常容易出现线程安全问题,因此需要采用同步机制来解决这种问题。

1)synchronized 关键字
  在Java语言中,每个对象都有相对应的锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当以线程调用对象的一段synchronized 代码时,需要先获取这个锁,然后去执行相应代码,执行结束后释放锁。
  synchronized 关键字主要有两种用法,此外该关键字还可以作用于静态方法、类或某个实例,但是这都对程序的效率会造成很大的影响。
①:synchronized 方法,在方法声明前加入 synchronized 关键字
  例:public synchronized void MyThred();
  只要把多个线程对类需要被同步的资源的操作放到 MyThred() 方法中,就可以实现这个方法在同一时刻只能被同一个线程访问,从而保证多线程访问的安全性。

②:synchronized 块,synchronizd 块既可以把任意代码声明为 synchronized ,也可以指定上锁的对象。
  例:synchronized (syncObject){//访问syncObject的代码}

2)wait() 方法与 notify() 方法
  当使用 synchronized 来修饰某个共享资源时,如果线程A1在执行 synchronized 代码,此时线程A2也要同时执行同一对象的同一 synchronized 代码时,线程A2就需要等到线程A1执行完成后,才能继续执行。此时就可以用到wait() 方法与 notify() 方法。
  在 synchronized 代码被执行期间,线程可以调用 wait() 方法释放对象锁,进入等待状态,并且可以调用 notify() 或者 notifyAll() 通知正在等待的其他线程。

notify() 方法仅唤醒一个线程并允许它去获取锁。
notifyAll() 方法唤醒全部线程并允许它们去获取锁。

3)Lock
jdk1.5新增了Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步。

①lock()
以阻塞的方式获取锁,即如果获得了锁,立即返回;如果别的线程持有锁,当前线程等待,直到获得锁后返回。

②tryLock()
以非阻塞的方式获得锁,只是常识性去获取一下锁,如果获得锁,立即返回true,否则,立即返回false。

③tryLock(long timeout,TimeUnit unit)
如果获得了锁,立即返回true,否则会等待参数给定的时间单元,在等待的过程中,如果获得了锁,就返回true,如果等待超时,返回false。

④lockInterruptibly()
如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获得锁。
或者当前线程被别的线程中断(会收到InterruptedException异常)
它与lock方法最大的区别是如果lock方法获取不到锁,就会一直处于阻塞状态,且会忽略interrupt方法
例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        final Lock lock = new ReentrantLock();
        lock.lock();
        //Thread(Runnable target) 分配新的 Thread 对象
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                try {
                    lock.lockInterruptibly();
                    //lock.lock();
                } catch (Exception e) {
                    System.out.println("interrupt.");
                }
            }
        });
        t1.start();
        t1.interrupt();
        Thread.sleep(1);
    }
}

结果:
interrupt.

如果把lock.lockInterruptibly();替换lock.lock(),编译器将会提示catch块代码无效。这是因为lock.lock()不会抛出异常,由此可见lock()方法会忽略interrupt()引发的异常。



8.6 sleep() 方法与 wait() 方法

sleep() 方法是使线程暂停执行一段时间的方法
wait() 方法是一种使线程暂停执行的方法。

例如,当线程交互时,如果线程对一个同步对象A发出一个 wait() 调用请求,那么该线程会暂停执行,被调用对象进入等待状态,直到被唤醒或等待时间超时。

区别:

1)原理不同:
sleep() 方法是Thread类的静态方法,是线程用来控制自身的,时间一到,线程就会自动苏醒。
wait() 方法是Object类的方法,用于线程间的通信。使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法才醒来,开发人员也可以指定醒来时间。
与 wait() 方法配套的方法还有 notify() 和 notifyAll()

2) 对锁的处理机制不同
sleep() 方法不会释放锁
wait() 方法会释放锁

3)使用区域不同
sleep() 方法任何地方可以使用,必须捕获异常,有可能被其他对象调用它的 interrupt() 方法,产生 InterruptedException 异常。
wait() 方法必须放在同步控制方法或同步语句块中使用,而wait(),notify(),notifyAll()不需要捕获异常。
sleep() 方法不会释放锁,容易死锁,推荐wait() 方法。

引申:sleep() 方法和 yield() 方法有什么区别?

1)sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给优先级低的线程运行的机会
   yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。
   
2)线程执行 sleep() 方法后会转入阻塞状态,所以执行 sleep() 方法的线程在指定的时间内肯定不会被执行
   yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield() 方法的线程有可能在进入到可执行状态后马上被执行。
   
3)sleep() 方法申明抛出 InterruptedException ,
   yield() 方法没有申明任何异常。

4)sleep() 方法比 yield() 方法具有更好的移植性。



8.7 终止线程的方法

在java语言中,可以使用stop方法与suspend方法来终止线程的执行。

当用Thread.stop()来终止线程时,可能会导致程序执行的不确定性。
当调用suspend()方法容易发生死锁,因为不会释放锁。

死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成互相等待的现象,如果无外力作用,他们都无法推进。建议采用让线程自动结束进入Dead状态,一个线程进入Dead状态,即执行完run方法。即想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run方法的执行。在实现时,可以通过设置一个flag标志来控制循环是否执行,通过这种方法来让线程离开run()方法从而中止线程。

public class Main implements Runnable {
    private volatile Boolean flag;//有默认类型,是false

    public void stop() {
        flag = false;
    }

    @Override
    public void run() {
        while (flag) {
            ;//do something
        }
    }
}

上例中,通过调用MyThread的stop方法虽然能够中止线程,但是同样存在问题当线程处于非运行状态时,(当sleep方法被调用,wait方法被调用,当被I/O阻塞),上面的方法就不可用了。此时可以使用interrupt()方法来打破阻塞的情况,当interrupt方法被调用时候,抛出InterruptedException 异常,可以通过在run方法中捕获这个异常来让线程安全退出。

/*
 * 当sleep()方法被调用或当wait()方法被调用-线程处于非运行状态
 * 此时可以使用interrupt()方法来打破阻塞的情况
 * 当interrupt()方法被调用时,会抛出InterruptedException异常
 */
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程休眠");
                try {
                    //用休眠来模拟线程被阻塞
                    Thread.sleep(5000);
                    System.out.println("线程结束");
                } catch (InterruptedException e) {
                    // TODO: handle exception
                    System.out.println("线程中断");
                }
            }
        });
        thread.start();
        thread.interrupt();
    }
}

结果:
线程休眠
线程中断

说明:
**如果程序因为I/O而停滞,进入非运行状态,基本上要等I/O完成才能离开这个状态。**在这种情况下,无法使用interrupt()方法来使程序离开run()方法。需要使用一个替代的方法,基本思路也是触发一个异常,而这个异常与所使用的I/O相关



8.8 synchronized 与 Lock

Java语言提供了两种锁机制来实现对某个共享资源的同步:
1)synchronized:使用Object对象本身的notify,wait,notifyAll调度机制
2)Lock:使用Condition进行线程之间的调度,完成synchronized实现的所有功能。


区别:

1)用法不一样
在需要同步的对象中加入synchronized控制,synchronized既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
synchronized是托管给JVM执行
Lock需要显示地指定起始位置了中止位置。Lock的锁定需要通过代码来实现。它有比synchronized更精确的线程定义。

2)性能不一样
竞争不激烈的情况下,synchronized性能好
竞争激烈的情况下,synchronized性能下降很快,ReentrantLock性能基本不变。

3)锁机制不一样
synchronized获得锁和释放锁都是在块结构中,当获得多个锁时候,必须以相反的顺序释放,并且是自动解锁
不会因为出现了异常而导致锁没有被释放从而引发死锁。

Lock需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁的问题
此外Lock还提供了更强大的功能,它的tryLock()方法可以采用非阻塞的方式去获取锁。

注:不要同时使用这两种同步机制,因为synchronized和ReentrantLock所使用的机制不同,运行是独立的,相当于两种类型的锁,在使用时互不影响。


8.9 守护线程

java提供了两种线程:守护线程和用户线程。

守护线程又被称为"服务进程",“精灵线程"或"后台线程”,是指程序运行时在后台提供的一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。即任何一个守护线程都是整个JVM中所有非守护线程的保姆。

用户线程和守护线程几乎一样,唯一不同的地方就是如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。
因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就中止了,同时会杀死所有守护线程。即只要有任何非守护线程还在运行,程序就不会终止。

在java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程。
例如将一个用户线程设置成守护线程的方法就是在调用start()方法启动线程之前调用对象的setDaemon(true)方法,若将以上参数设置为false,则表示的是用户进程模式。
当在一个守护线程中产生了其他线程,那么这些新产生大的线程默认还是守护线程,用户线程也是如此。

public class MyThred extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
        System.out.println(Thread.currentThread().getName() + ":结束");
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("测试1:开始");
        Thread t1 = new MyThred();
        t1.setDaemon(true);
        t1.start();
        System.out.println("测试1:结束");

    }
}

结果:
测试1:开始
测试1:结束

守护线程的一个典型例子就是垃圾回收器。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。


8.10 join() 方法的作用

在java语言中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join之后的代码。

简单点说:就是将两个线程合并,用于实现同步功能。

具体而言,可以通过线程A的 join() 方法来等待线程A的结束,或者使用线程A的 join(2000) 方法来等待线程A的结束,但是最多只等待2s.

public class MyThred implements Runnable {

    @Override
    public void run() {
        try {
            System.out.println("开始");
            Thread.sleep(5000);
            System.out.println("结束");
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThred());
        thread.start();
        try {
            thread.join(1000);//让主程序等待线程 Thread 结束,只等1s
            if (thread.isAlive()) {
                System.out.println("线程未结束");
            } else {//已经结束
                System.out.println("线程结束");
            }
            System.out.println("加入完成");
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }
    }
}

结果:
开始
线程未结束
加入完成
结束







注:

此文来源于《Java程序员面试笔试宝典》一书

仅作为本人学习过程的记录

填写原创是因为找不到相关链接

如有不妥请联系本人,会立即删除

此书对于我这种小白来说非常有用,讲解详细,知识点全面,目前正在根据此书学习,陆续会记录更多知识点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值