Java基础——多线程

在这之前可先参照之前的文章,初识多线程

目录

1.为什么使用多线程?

2.同步和异步有什么区别

3.如何实现Java多线程

4.run()方法和start()方法有何区别

5.多线程同步的实现方法

6.sleep()方法和wait()方法有何区别

7.终止线程的方法有哪些

8.synchronization与Lock有何异同

9.什么是守护线程

10.join()方法的作用


1.为什么使用多线程?

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

多线程的使用为程序研发带来了巨大便利。

1)使用多线程可减少程序的响应时间,同时使程序具备更好的交互性。

2)与进程相比,线程的创建和切换开销更小。运行于同一进程内的线程共享代码段、数据段,线程的启动或切换比进程要少很多。同时多线程在数据共享方面效率非常高。

3)在多CPU计算机上使用多线程能提高CPU利用率。

4)使用多线程能简化程序结构,使程序便于理解和维护。

2.同步和异步有什么区别

之前一篇文章举了一个买书的例子,这里谈一下线程的例子。

何种情况需对数据进行同步?假设多个线程同时对同一数据进行“写”操作,当线程A需要某资源时,如果这个资源正被资源B使用,同步机制就会让线程A一直等待下去,直到线程B结束对该资源的使用后,线程A才能使用该资源。因此同步机制可保证资源的安全。

要想实现同步操作,必须要获得每一个线程对象的锁。同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在锁被释放之前,其他资源不能进入临界区。如果想要进入,只能在队列等待,当锁释放后,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码块。

Java语言中可使用synchronization关键字实现同步,但它是以很大的系统开销作为代价的,有时甚至可能造成死锁。因此同步控制并非越多越好,避免多余的同步控制。实现同步方式有两种:一种利用同步代码块;另一种利用同步方法。

异步与非阻塞类似,进行输入输出时,不必关心其他线程的状态和行为,也不必等到数据处理完毕才返回。当应用程序在一个对象调用一个需要花费很长时间的方法,而并不希望程序等待方法返回,应该使用异步编程,能提高程序效率。

3.如何实现Java多线程

多线程地实现一般有以下三种方法,前两种最常用:

1)继承Thread类,重写run()方法

class MyThread extends Thread{ //创建线程类
    public void run(){
        System.out.println("Thread body");  //线程的函数体
    }
}
public class Test{
    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("Thread body");
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread t = new Thread(thread);
        t.start();  //开启线程
    }
}

不管是通过继承Thread类还是使用Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程。

3)实现Callable接口,重写call()方法

Callable接口属于Executor框架中的功能类,提供了比Runnable更强大的功能,主要有3点:

1.Callable可在任务结束提供一个返回值,Runnable无法提供。

2.Callable中的call()方法可抛出异常,Runnable的run()方法不能。

3.运行Callable可以拿到一个Future对象,表示异步计算结果,提供检查计算是否完成的方法。

以上3种方式种,前两种线程执行完没有返回值,第三种是带返回值的。

当需要实现多线程时,一般推荐实现Runnable接口的方式。原因:

1.Thread类定义了多种方法可被派生类使用或重写,但只有run()方法必须被重写,在run()方法种实现这个线程的主要功能,这当然是实现Runnable接口所需的方法。

2.一个类仅在需要被加强或修改时才会被继承,因此如果没有必要重写Thread类中的其他方法,通过继承Thread的实现方式和实现Runnable接口效果相同,这种情况最好通过第二种方式创建线程。

4.run()方法和start()方法有何区别

通常,系统通过调用线程类的start()方法来启动一个线程,此时线程处于就绪状态,非运行状态,也意味着这个线程可被JVM调度执行。在调度过程中,JVM调用run()方法完成实际操作,run()方法结束,线程终止。

如果直接调用线程类的run()方法,会被当作一个普通函数调用,程序中仍只有主线程这一线程。因此start()方法能够异步调用run()方法,但直接调用run()方法确实同步的,也就无法达到多线程的目的。

因此只有通过调用线程类的start()方法才能真正达到多线程的目的。

5.多线程同步的实现方法

当多个线程同时对一数据进行修改,会导致修改丢失,需要采用同步机制解决。Java主要提供3种方法:

1)synchronization关键字

每个对象都有一个对象锁与之关联,该锁表明对象在任何时候只允许被一个线程所拥有。当一个线程调用对象的一段synchronization代码时,需要先获取这个锁,然后执行相应代码,执行结束,释放锁。

synchronization关键字有两种用法:synchronization 方法和 synchronization块

1. synchronization方法:在方法声明前加入synchronization关键字

public synchronization void mutiThreadAccess();

只需把多个线程对于类需要被同步的资源的操作放到mutiThreadAccess()方法种。就能保证这个方法在同一时刻只能被一个线程访问,保证了多线程访问的安全性。但当一个方法的方法体规模非常大时,把该方法声明为synchronization会大大影响执行效率。因此Java提供了synchronization块。

2. synchronization块:既可以把任意代码声明为synchronization,也可以指定上锁的对象,有很高的灵活性。

用法如下:

synchronization(syncObject){
       //访问syncObject的代码
}

2)wait() 方法和 notify() 方法

当使用synchronization修饰某个共享资源,如果线程T1在执行synchronization代码,另外一个线程T2也要同时执行同一对象的同一synchronization代码时,线程T2将要等到线程T1执行完成,才能继续执行。这种情况需要使用wait()方法和notify()方法。

在synchronization代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并可调用notify()方法和notifyAll()方法通知正在等待的其他线程。notify()方法唤醒等待队列中的第一个线程并允许它去获取锁;notifyAll()方法唤醒所有线程,并让它们去竞争获得锁。

3)Lock

JDK 5新增了Lock接口及它的实现类ReentrantLock(重入锁),Lock提供如下方法实现多线程同步:

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

2. tryLock() 。以非阻塞方式获取锁。只是尝试性去获取下锁,若获取到锁,返回true,否则返回false。

3. tryLock(long timeout,TimeUnit unit)。如果获取到锁,立即返回true,否则会等待参数给定的时间单元,等待时,获取到锁返回true,等待超时返回false。

4. lockInterruptibly()。若获取到锁,立即返回;若没有获取到锁,当前线程进入休眠状态,直到获取锁,或者当前线程被别的线程中断(收到InterruptedException异常)。它与lock()方法的区别在于如果lock()方法获取不到锁,会一直处于阻塞状态,且会忽略interrupt()方法。

6.sleep()方法和wait()方法有何区别

都是使线程暂停执行的方法,区别以表格的形式呈现:

sleep()和wait()方法区别
 sleep() 方法wait() 方法
原理不同是Thread类的静态方法,是线程用来控制自身流程的,它会使线程暂停执行一段时间,把执行机会留给其他线程,等计时时间一到,线程自动苏醒。是Object类的方法,用于线程间通信,此方法会使当前拥有该对象锁的进程等待,直到其他线程调用 notify() 方法或 notifyAll() 时才会醒来,不过开发人员可指定时间使其自动醒来。               
对锁的处理机制不同由于sleep()方法不涉及线程间的通信,因此调用sleep()方法并不会释放锁调用wait()方法后,线程会释放掉它所占用的锁,使线程所在对象中的其他synchronization数据可被其他线程使用。
使用区域不同可放在任何地方使用必须放在同步控制方法或同步语句块中使用

sleep() 方法必须捕获异常,而wait()、notify() 和 notifyAll() 不需要捕获异常。在sleep的过程中,有可能被其他对象调用它的interrupt(),产生InterruptedException异常

由于sleep不会释放锁,容易导致死锁问题.一般情况不推荐使用sleep()方法,而是使用 wait() 方法。

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

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

7.终止线程的方法有哪些

在Java语言中,存在stop()和suspend()方法来终止线程的执行。但两种方法均存在不安全性,Java语言已经不建议使用这两种方法。

一般建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,即执行完run()方法。换种说法就是想要停止一个线程执行,就要提供某种方法让线程能够自动结束run()方法的执行。在实现时,可设置一个flag标志来控制循环是否执行,让线程离开run()方法从而终止线程。

public class MyThread implements Runnable{
    private volatile Boolean flag;
    public void stop(){
        flag = false;
    }
    public void run(){
        while(flag)
            ;  //do something
    }
}

通过调用MyThread的stop()方法队能够终止线程,但存在问题:当线程处于非运行状态(当sleep()方法被调用或当wait()方法被调用或当被I/O阻塞时),此方法不可用了。

此时可以使用interrupt()方法来打破阻塞情况,当interrupt()方法被调用,会抛出InterruptedException异常,可在run()方法中捕获这个异常来让线程安全退出。具体实现方式:

public class MyThread {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread go to sleep");
                try{
                    //用休眠模拟线程阻塞
                    Thread.sleep(5000);
                    System.out.println("thread finish");
                }catch(InterruptedException e){
                    System.out.println("thread is interrupted!!!");
                }
            }
        });
        thread.start();
        thread.interrupt();
    }
}

运行结果:

thread go to sleep
thread is interrupted!!!

如果程序因为 I/O 停滞,进入非运行状态,基本要等到 I/O 完成才能离开这个状态,此时无法使用interrupt()方法使线程离开run()方法。此时需要一个替代方法,基本思路也是触发一个异常,这个异常与所使用的 I/O 相关。

举个例子,使用readLine()方法在等待网络上一个消息,此时线程处于阻塞状态。让程序离开run()方法就是用close()方法关闭流,这种情况会引发IOException异常,run()方法可捕获这个异常安全结束线程。

8.synchronization与Lock有何异同

synchronization与Lock是Java提供的两种锁机制实现对某个共享资源的同步。

synchronization使用Object对象本身的notify、wait、notifyAll调度机制;Lock使用Condition进行线程之间的调度,完成synchronization实现的所有功能。

二者区别主要有以下一个方面:

synchronization与Lock区别
 synchronizationLock
用法不一样

在需要同步的对象中加入synchronization控制。synchronization既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

synchronization是托管给JVM执行的

需要显式指定起始位置和终止位置。

Lock的锁定是通过代码实现的,它有比synchronization更精确的线程语义。

性能不一样在竞争不是很激烈时,synchronization性能要优于ReetrantLock;竞争激烈的情况下,synchronization性能下降非常快,ReetrantLock性能基本保持不变。

在JDK 5中加入Lock接口的实现类ReentrantLock。它拥有和synchronization相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。

 

锁机制不一样synchronization获得锁和释放都是在块结构中,当获取多个锁,必须以相反顺序释放,且是自动解锁,不会因为出了异常导致锁没有被释放而引发死锁。Lock需要开发人员手动释放,且在finally块中释放,否则就会引起死锁问题。此外Lock的tryLock()方法可采用非阻塞方式去获得锁。

虽然两种方法都可以实现多线程同步,但最好不要同时使用这两种同步机制。因为ReetrantLock与synchronization所使用的机制不同,它们的运行是独立的,相当于两种类型的锁,使用时互不影响。

9.什么是守护线程

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

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

用户线程几乎和守护线程一样,唯一的不同就是如果用户线程已经全部退出运行,只剩守护线程,JVM也就退出了。因为当非守护线程结束,守护线程也就没有工作可以做了,程序也就终止了,同时会杀死所有守护线程。换句话说,只要存在非守护线程,程序就不会终止。

守护线程一般优先级较低,它并非只由JVM内部提供,用户也可自己设置守护线程。方法就是在调用start()方法启动线程之前调用对象的setDaemon(true)方法,若将以上参数设置为false,则表示的是用户进程模式。

当在一个守护线程中产生了其他线程,那么默认还是守护线程,用户线程也是如此。

class ThreadDemo extends Thread{
    public void run(){
        System.out.println(Thread.currentThread().getName()+":begin");
        try{
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":end");
    }
}

public class Test{
    public static void main(String[] args) {
        System.out.println("test:begin");
        Thread chs = new ThreadDemo();
        chs.setDaemon(true);
        chs.start();
        System.out.println("test:end");
    }
}

运行结果:

test:begin
test:end
Thread-0:begin

可以发现,结果没有输出Thread-0:end。因为在启动线程之前将其设为守护线程,当程序中只有守护线程时,JVM是可以退出的。因此当test方法调用结束后,main线程将退出,线程chs还处于休眠状态没有结束,但由于此时守护线程在运行,JVM将会关闭,因此不会输出“Thread-0:end”。

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

10.join()方法的作用

让调用该方法的线程在执行完run方法之后,再执行join方法后面的代码。换句话说,就是将两个线程合并,用于实现同步功能。

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

class ThreadImp implements Runnable{
    @Override
    public void run() {
        try{
            System.out.println("ThreadImp begin");
            Thread.sleep(6000);
            System.out.println("ThreadImp end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
public class JoinTest {
    public static void main(String[] args) {
        Thread chs = new Thread(new ThreadImp());
        chs.start();
        try{
            chs.join(3000);
            if(chs.isAlive())
                System.out.println("chs has not finished");
            else
                System.out.println("chs has finished");
            System.out.println("joinFinish");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

结果:

ThreadImp begin           //先出现
chs has not finished      //3秒后出现
joinFinish                       //3秒后出现
ThreadImp end              //6秒后出现

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值