资讯专栏INFORMATION COLUMN

Java 多线程

zorro / 998人阅读

摘要:当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程所有运行中的任务通常对应一个进程。线程也被称作轻量级进程,线程是进程的执行单元。在线程的生命周期中,它要经过新

线程概述 线程和进程

几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程

所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能。进程是系统进行资源分配和调度的一个独立单位

进程包含如下3个特征:

独立性:进程是系统中独立存在的实体,它拥有独立的资源,每一个进程都拥有自己私有的地址空间,没有经过进程本身运行的情况下,一个用户进程不可以访问其他进程的地址空间

动态性:进程与程序的区别在于,程序只是一个静态指令集合,而进程是一个正在系统中活动的指令集合,在进程中加入了时间概念

并发性:多个进程可以在单个处理器上并发执行,多个线程之间不会相互影响

并发性(concurrency)与并行性(parallel)

并发:在同一时刻,只能有一条指令得到执行,但多个进程指令块快速轮换执行,使得在宏观上具有多个进程同时执行的效果

并行:在同一时刻,有多条指令在多个处理器上同时执行

多线程则是扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源

线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务

线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行时抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行

从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成

简而言之,一个程序运行过程中至少有一个进程,一个进程中可以包含多个线程,但至少要包含一个线程

总结:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程

多线程的优势

线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中线程之间的隔离程度要小。它们共享内存、文件句柄和其他每个进程应有的状态

使用多线程编程具有如下优点:

进程之间不能共享内存,但线程之间共享内存非常容易

系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小很多,因此使用多线程来实现多任务并发比多进程的效率高

Java语言内置了多线程功能的支持,而不是简单的底层操作系统调度,从而简化了多线程编程

线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例

继承Thread类创建线程类

通过继承Thread类创建线程类步骤如下:

定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体

创建Thread子类的实例,即创建线程对象

调用线程对象的start()方法来启动该线程

// 通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
    private int i ;
    // 重写run方法,run方法的方法体就是线程执行体
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 当线程类继承Thread类时,直接使用this即可获取当前线程
            // Thread对象的getName()返回当前该线程的名字
            // 因此可以直接调用getName()方法返回当前线程的名
            System.out.println(getName() +  " " + i);
        }
    }
    public static void main(String[] args)
    {
        for (int i = 0; i < 100;  i++)
        {
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName()
                +  " " + i);
            if (i == 20)
            {
                // 创建、并启动第一条线程
                new FirstThread().start();
                // 创建、并启动第二条线程
                new FirstThread().start();
            }
        }
    }
}

程序显式地创建并启动了2个线程,实际上程序有3个线程,即程序显式创建的2个线程和主线程

Thread-0和Thread-1两个线程输出的i变量不连续。因为i变量是FirstThread的实例变量,而不是局部变量,程序每次创建一个FirstThread对象,所以Thread-0和Thread-1不共享该实例变量

进行多线程编程时,不要忘记Java程序运行时默认的主线程,main()方法的方法体代表主线程的线程执行体

Thread.currentThread():currentThread()是Thread类的静态方法,该方法总是返回当前正在执行的线程对象

getName():该方法是Thread类的实例方法,该方法返回调用该方法的线程名字

setName():该方法设置线程的名字

默认情况下,主线程的名字为main,用户启动的多个线程的名字依次被Thread-0、Thread-1、Thread-2、...、Thread-n等

使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量

实现Runnable接口创建线程类

实现Runnable接口创建线程类步骤如下:

定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体

创建Runnable实现类的实例,并以此实现作为Thread的target来创建Thread对象,该对象才是线程的真正对象
Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法

// 创建Runnable实现类的对象
ThreadTest tt = new ThreadTest();
// 以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
new Thread(tt);

调用该线程对象的start()方法来启动线程

通过Thread获取当前线程对象比较简单,直接通过this就可以,但是通过Runnable接口获得当前线程对象,则必须使用Thread.currentThread()

// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
    private int i ;
    // run方法同样是线程执行体
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 当线程类实现Runnable接口时,
            // 如果想获取当前线程,只能用Thread.currentThread()方法。
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }

    public static void main(String[] args)
    {
        for (int i = 0; i < 100;  i++)
        {
            System.out.println(Thread.currentThread().getName() + "  " + i);
            if (i == 20)
            {
                SecondThread st = new SecondThread();
                // 通过new Thread(target, name)方法创建新线程
                new Thread(st , "新线程1").start();
                new Thread(st , "新线程2").start();
            }
        }
    }
}

采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。程序所创建的Runnable对象只是线程的target,所以多个线程可以共享同一个现场了(实际上应该是线程的target类)的实例变量

使用Callable和Future创建线程

Callable接口类似Runnable接口的增强版,提供了一个call()方法可以作为线程执行体,call()比run()方法更强大

call()可以有返回值

call()方法可以声明抛出异常

Callable接口并不是Runnable接口的子接口,所以Callable接口不能直接做为Thread的target。而且call()方法还有一个返回值——call()方法不是直接调用,是作为线程执行体被调用的

Future接口来代表Callable接口里call()方法里的返回值,Future接口提供了FutureTask实现类,该接口实现了Future接口,并实现了Runnable接口,可以作为Thread类的target

在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:

boolean cancel(Boolean mayInterruptlfRunning):试图取消该Future里关联的Callable任务

V get():返回Callable任务里的call方法的返回值,调用该方法将导致线程阻塞,必须等到子线程结束才得到返回值

V get(long timeout, TimeUnit unit):返回Callable任务里的call方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间。如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException

boolean isCancelled:如果在Callable任务正常完成前被取消,则返回true

boolean isDone:如果Callable任务已经完成,则返回true

Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同。而且Callable接口是函数式接口,因此可以使用Lambda表达式创建Callable对象

创建并启动有返回值的线程步骤如下:

创建Callable接口的实现类,并实现call()方法,该call()方法作为线程的执行体,且有返回值,再创建Callable实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象

使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值

使用FutureTask对象作为Thread对象的target创建并启动新线程

调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

import java.util.concurrent.*;

public class ThirdThread
{
    public static void main(String[] args)
    {
        // 创建Callable对象
        ThirdThread rt = new ThirdThread();
        // 先使用Lambda表达式创建Callable对象
        // 使用FutureTask来包装Callable对象
        FutureTask task = new FutureTask((Callable)() -> {
            int i = 0;
            for ( ; i < 100 ; i++ )
            {
                System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            }
            // call()方法可以有返回值
            return i;
        });
        for (int i = 0 ; i < 100 ; i++)
        {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            if (i == 20)
            {
                // 实质还是以Callable对象来创建、并启动线程
                new Thread(task , "有返回值的线程").start();
            }
        }
        try
        {
            // 获取线程返回值
            System.out.println("子线程的返回值:" + task.get());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

程序使用Lambda表达式直接创建Callable对象,这样就无须先创建Callable实现类,再创建Callable对象

创建线程的三种方式对比

采用实现Runnable、Callable接口的方式创建多线程的优缺点:

线程类只是实现了Runnable接口或Callable,还可以继承其他类

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想

劣势是:编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法

采用继承Thread类的方式创建多线程的优缺点:

劣势是:因为线程类已经继承了Thread类,所以不能再继承其他父类

优势是:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程

线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态

新建和就绪状态

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体

当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度

启动线程使用start()方法,而不是run()方法。永远不要调用线程对象的run()方法。调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直按调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体

public class InvokeRun extends Thread
{
    private int i ;
    // 重写run方法,run方法的方法体就是线程执行体
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 直接调用run方法时,Thread的this.getName返回的是该对象名字,
            // 而不是当前线程的名字。
            // 使用Thread.currentThread().getName()总是获取当前线程名字
            System.out.println(Thread.currentThread().getName() +  " " + i);   // ①
        }
    }
    public static void main(String[] args)
    {
        for (int i = 0; i < 100;  i++)
        {
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName() +  " " + i);
            if (i == 20)
            {
                // 直接调用线程对象的run方法,
                // 系统会把线程对象当成普通对象,run方法当成普通方法,
                // 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法
                new InvokeRun().run();
                new InvokeRun().run();
            }
        }
    }
}

程序运行结果是整个程序只有一个线程:主线程。如果直接调用线程对象的run()方法,则run()方法里不能直接通过getName()方法来获得当前执行线程的名字,而是需要Thread.currentThread()方法先获得当前线程,再调用线程对象的getName()方法来获得线程名字

只能对处于新建状态的线程调用start()方法,否则将引发IllegaIThreadStateExccption异常

调用线程对象的start()方法之后,该线程立即进入就绪状态——就绪状态相当于"等待执行",但该线程并未真正进入运行状态。如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.slepp(1)来让当前运行的线程(主线程)睡眠1毫秒,因为在这1毫秒内CPU不会空闲,它回去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行

运行和阻塞状态

当发生如下情况时,线程将会进入阻塞状态

线程调用sleep()方法主动放弃所占用的处理器资源

线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有

线程在等待某个通知(notify)

程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它

当发生如下特定情况时可以解除上述阻塞,该让线程重新进入就绪状态:

调用sleep()方法的线程经过了指定时间

线程调用的阻塞式IO方法已经返回

线程成功地获得了试图取得的同步监视器

线程正在等待某个通知时,其他线程发出了个通知

处于挂起状态的线程被调甩了resdme()恢复方法

线程状态转换图

线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态

线程死亡

线程会以如下3种方式结束,结束后就处于死亡状态:

run()或call()方法执行完成,线程正常结束

线程抛出一个未捕获的Exception或Error

直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响

为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;当线程处于新建、死亡状态时,该方法将返回false

不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行。如下程序尝试对处于死亡状态的线程再次调用start()方法,将引发IllegaIThreadStateException异常,这表明处于死亡状态的线程无法再次运行了

public class StartDead extends Thread
{
    private int i ;
    // 重写run方法,run方法的方法体就是线程执行体
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            System.out.println(getName() +  " " + i);
        }
    }
    public static void main(String[] args)
    {
        // 创建线程对象
        StartDead sd = new StartDead();
        for (int i = 0; i < 300;  i++)
        {
            // 调用Thread的currentThread方法获取当前线程
            System.out.println(Thread.currentThread().getName() +  " " + i);
            if (i == 20)
            {
                // 启动线程
                sd.start();
                // 判断启动后线程的isAlive()值,输出true
                System.out.println(sd.isAlive());
    

    }
        // 只有当线程处于新建、死亡两种状态时isAlive()方法返回false。
        // 当i > 20,则该线程肯定已经启动过了,如果sd.isAlive()为假时,
        // 那只能是死亡状态了。
        if (i > 20 && !sd.isAlive())

        {
            // 试图再次启动该线程
            sd.start();
        }
    }
}

}

控制线程 join线程

Thread提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止

join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作

public class JoinThread extends Thread
{
    // 提供一个有参数的构造器,用于设置该线程的名字
    public JoinThread(String name)
    {
        super(name);
    }
    // 重写run()方法,定义线程执行体
    public void run()
    {
        for (int i = 0; i < 100 ; i++ )
        {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args)throws Exception
    {
        // 启动子线程
        new JoinThread("新线程").start();
        for (int i = 0; i < 100 ; i++ )
        {
            if (i == 20)
            {
                JoinThread jt = new JoinThread("被Join的线程");
                jt.start();
                // main线程调用了jt线程的join()方法
                // main线程必须等jt执行结束才会向下执行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName()
                + "  " + i);
        }
    }
}


主方法开始时就启动了名为“新线程”的子线程,该子线程将会和main线程并发执行。当主线程的循环变量i等于20时,启动了名为“被Join的线程”的线程,该线程不会和main线程并发执行,main线程必须等该线程执行结束后才可以向下执行。在名为“被Join的线程”的线程执行时,实际上只有2个子线程并发执行,而主线程处于等待状态

join()方法有如下三种重载形式:

join():等待被join的线程执行完成

join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join的线程还没有执行结束,则不再等待

join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫微秒

后台线程

后台线程:在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程

后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡

调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程

以下程序将执行线程设置成后台线程,可以看到当所有的前台线程死亡时,后台线程随之死亡。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也退出

public class DaemonThread extends Thread
{
    // 定义后台线程的线程执行体与普通线程没有任何区别
    public void run()
    {
        for (int i = 0; i < 1000 ; i++ )
        {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args)
    {
        DaemonThread t = new DaemonThread();
        // 将此线程设置成后台线程
        t.setDaemon(true);
        // 启动后台线程
        t.start();
        for (int i = 0 ; i < 10 ; i++ )
        {
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
        // -----程序执行到此处,前台线程(main线程)结束------
        // 后台线程也应该随之结束
    }
}

先将t设置成后台线程,然后启动该线程,本来该线程应该执行到i等于999时才会结束,但运行程序时不难发现该后台线程无法运行到999,因为主线程(程序中唯一的前台线程)运行结束后,JVM主动退出,因此后台线程也就结束

Thread类提供一个isDaemon()方法,用来判断指定线程是否为后台线程

前台线程创建的子线程默认就是前台线程,后台线程创建的子线程默认是后台线程

前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常

线程睡眠:sleep

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。sleep()方法有两种重载形式

static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响

static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度影响(不建议)

当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()的线程也不会执行,因此sleep()方法常用来暂停程序的执行

import java.util.*;
public class SleepTest
{
    public static void main(String[] args) throws Exception
    {
        for (int i = 0; i < 10 ; i++ )
        {
            System.out.println("当前时间: " + new Date());
            // 调用sleep方法让当前线程暂停1s。
            Thread.sleep(1000);
        }
    }
}
线程让步:yield

它可以让当前正在执行的线程暂停,但不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行

实际上,当某个线程调用yield()方法暂停之后,只要优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会

public class YieldTest extends Thread
{
    public YieldTest(String name)
    {
        super(name);
    }
    // 定义run方法作为线程执行体
    public void run()
    {
        for (int i = 0; i < 50 ; i++ )
        {
            System.out.println(getName() + "  " + i);
            // 当i等于20时,使用yield方法让当前线程让步
            if (i == 20)
            {
                Thread.yield();
            }
        }
    }
    public static void main(String[] args)throws Exception
    {
        // 启动两条并发线程
        YieldTest yt1 = new YieldTest("高级");
        // 将ty1线程设置成最高优先级
        yt1.setPriority(Thread.MAX_PRIORITY);
        yt1.start();
        YieldTest yt2 = new YieldTest("低级");
        // 将yt2线程设置成最低优先级
        yt2.setPriority(Thread.MIN_PRIORITY);
        yt2.start();
    }
}

关于sleep()方法和yield()方法的区别如下:

sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会

sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行

sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常

sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行

改变线程优先级

每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级

Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下三个静态常量:

MAX_PRIORITY:其值是10

MIN_PRIORITY:其值是1

NORM_PRIORITY:其值是5

public class PriorityTest extends Thread
{
    // 定义一个有参数的构造器,用于创建线程时指定name
    public PriorityTest(String name)
    {
        super(name);
    }
    public void run()
    {
        for (int i = 0 ; i < 50 ; i++ )
        {
            System.out.println(getName() +  ",其优先级是:" + getPriority() + ",循环变量的值为:" + i);
        }
    }
    public static void main(String[] args)
    {
        // 改变主线程的优先级
        Thread.currentThread().setPriority(6);
        for (int i = 0 ; i < 30 ; i++ )
        {
            if (i == 10)
            {
                PriorityTest low  = new PriorityTest("低级");
                low.start();
                System.out.println("创建之初的优先级:" + low.getPriority());
                // 设置该线程为最低优先级
                low.setPriority(Thread.MIN_PRIORITY);
            }
            if (i == 20)
            {
                PriorityTest high = new PriorityTest("高级");
                high.start();
                System.out.println("创建之初的优先级:" + high.getPriority());
                // 设置该线程为最高优先级
                high.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }
}
线程同步 同步代码块

Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

synchronized(obj)
{
    ...
    //此处的代码就是同步代码块
}

其中,obj是一个同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码执行完成后,该线程会释放对该同步监视器的锁定

同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器

public class DrawThread extends Thread
{
    // 模拟用户账户
    private Account account;
    // 当前取钱线程所希望取的钱数
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount)
    {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
    public void run()
    {
        // 使用account作为同步监视器,任何线程进入下面同步代码块之前
        // 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
        // 这种做法符合:“加锁→修改→释放锁”的逻辑
        synchronized (account) 
        {
            // 账户余额大于取钱数目
            if (account.getBalance() >= drawAmount)
            {
                // 吐出钞票
                System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改余额
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("	余额为: " + account.getBalance());
            }
            else
            {
                System.out.println(getName() + "取钱失败!余额不足!");
            }
        }
        // 同步代码块结束,该线程释放同步锁
    }
}

使用synchronize将run()方法里的方法体修改成同步代码块,该同步代码块的同步监视器是account对象,该做法符合“加锁→修改→释放锁”的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任何一个时刻只有一条线程可以进入修改共享资源的代码区(也称为临界区),所以同一个时刻最多只有一条线程处于临界区内,从而保证了线程的安全

同步方法

同步方法:使用synchronized关键字来修饰的某个方法。对于同步方法而言,无需显式指定同步监视器,同步方法的同步监视器是this,也就是该对象本身

通过使用同步方法可以非常方便地将某类变成线程安全的类,线程安全的类具有如下特性:

该类的对象可以被多个线程安全的访问

每个线程调用该对象的任何方法之后都将得到正确的结果

每个线程调用该对象的任何方法之后,该对象状态依然保持合理状态

public class Account
{
    // 封装账户编号、账户余额两个成员变量
    private String accountNo;
    private double balance;
    public Account(){}
    // 构造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }
    // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
    public double getBalance()
    {
        return this.balance;
    }

    // 提供一个线程安全draw()方法来完成取钱操作
    public synchronized void draw(double drawAmount)
    {
        // 账户余额大于取钱数目
        if (balance >= drawAmount)
        {
            // 吐出钞票
            System.out.println(Thread.currentThread().getName()
                + "取钱成功!吐出钞票:" + drawAmount);
            try
            {
                Thread.sleep(1);
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
            // 修改余额
            balance -= drawAmount;
            System.out.println("	余额为: " + balance);
        }
        else
        {
            System.out.println(Thread.currentThread().getName()
                + "取钱失败!余额不足!");
        }
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    public int hashCode()
    {
        return accountNo.hashCode();
    }
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if (obj !=null
            && obj.getClass() == Account.class)
        {
            Account target = (Account)obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

以上程序中增加了一个代表取钱操作的draw方法,并使用了synchronized关键字修饰了该方法,把该方法变成同步方法。同步方法的同步监视器是this,因此对于同一个Account账户来说,任何时刻只能有一条线程获得对Account对象的锁定,然后进入draw方法执行取钱操作——这样也可以保证多条线程并发存钱的线程安全

synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造方法、成员变量等

在面向对象里有一种流行的设计方式:Domain Driven Design(即领域驱动设计,简称DDO),这种方式认为每个类都应该是完备的领域对象,例如Account它代表用户账户,它应该提供账户的相关方法,例如通过draw()方法来执行取钱操作,而不是直接将setBalance()方法暴露出来任人操作,这样才可以更好地保证Account对象的完整性和一致性。可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采取如下策略:

不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步,例如上面的Account类中accountNo属性就无需同步,所以程序只对draw方法进行同步控制、

如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本

释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:

当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器

当线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器

当线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器

当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器

在下面的情况下,线程不会释放同步监视器:

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器

线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,应该尽量避免使用suspend()和resume()方法来控制线程

同步锁(Lock)

Java5提供了功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当

Lock是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该首先获Lock对象

某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类

Java8之后新增了新型的StampedLock。它在大多数场景中可以替代传统的ReentrantReadWriteLock。为读写操作提供三种锁模式:Writing、ReadingOptimistic、Reading

在实现线程安全的控制中,比较常用的是ReadWriteLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,示例如下:

class LockTest
{
    // 定义锁对象
    private final ReadWriteLock lock = new ReadWriteLock();
    // ...
    // 定义需要保证线程安全的方法
    public void m()
    {
        // 加锁
        lock.lock();
        try
        {
            // 需要保证线程安全的代码
            // ... method body
        }
        // 使用finally块来保证释放锁
        finally
        {
            lock.unlock();
        }
    }
}

通常建议使用finally块来确保在必要时释放锁

使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,都符合“加锁——修改——释放锁”的操作模式

同步方法和同步代码块使用与竞争资源相关的,隐式的同步监视器,并且强制要求加锁和释放锁要出现在同一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在所有锁被获取时相同的范围内释放锁

Lock提供了同步方法和同步代码块没有的功能,包括用于非块结构的tryLock方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效所的tryLock(long, TimeUnit)方法

ReentrantLock锁具有重入性,也就是线程可以对已经加锁的ReentrantLock锁再次加锁,ReentrantLock会维持一个计数器来跟踪Lock方法的嵌套调用,线程在每次lock()加锁后,必须显式调用unLock()来释放锁,所一段被锁保护的代码可以调用另一个被相同锁保护的方法

死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续

class A
{
    public synchronized void foo( B b )
    {
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo()方法" );     // ①
        try
        {
            Thread.sleep(200);
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last()方法");    // ③
        b.last();
    }
    public synchronized void last()
    {
        System.out.println("进入了A类的last()方法内部");
    }
}
class B
{
    public synchronized void bar( A a )
    {
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar()方法" );   // ②
        try
        {
            Thread.sleep(200);
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last()方法");  // ④
        a.last();
    }
    public synchronized void last()
    {
        System.out.println("进入了B类的last()方法内部");
    }
}
public class DeadLock implements Runnable
{
    A a = new A();
    B b = new B();
    public void init()
    {
        Thread.currentThread().setName("主线程");
        // 调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }
    public void run()
    {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }
    public static void main(String[] args)
    {
        DeadLock dl = new DeadLock();
        // 以dl为target启动新线程
        new Thread(dl).start();
        // 调用init()方法
        dl.init();
    }
}

由图可知,程序既无法向下执行,也不会抛出任何异常,就一直“僵着”。因为A、B对象的方法都是同步方法,A、B对象都是同步锁。程序中两个线程执行,一个线程的线程执行体是DeadLock类的run()方法,另一个线程的线程执行体是DeadLock的init()方法(主线程调用了init()方法)。其中run()方法让B对象调用bar()方法,而init()方法让A对象调用foo()方法

如图所示,init()方法先执行,调用了A对象的foo()方法,进入foo()方法之前,该线程对A对象加锁——当程序执行到①代码时,主线程暂停200ms

CPU切换到执行另一个线程,让B对象执行bar()方法,所以看到副线程开始执行B实例的bar()方法,进入bar()方法之前,对B对象加锁——当程序执行到②代码时,副线程也暂停200ms

接下来主线程会先醒过来,继续向下执行,直到③代码处希望调用B对象的last()方法——执行该方法之前必须先对B对象加锁,但此时副线程正保持着B对象的锁,所以主线程阻塞

接下来副线程应该醒过来了,继续向下执行,直到④代码处希望调用A对象的last()方法——————执行此方法之前必须先对A对象加锁,但此时主线程没有释放对A对象的锁——至此,就出现了主线程保持着A对象的锁,等待B对象加锁,而副线程保持着B对象的锁,等待着A对象的锁,等待对A对象加锁,两个线程互相等待对方先释放,所以就出现了死锁

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66691.html

相关文章

  • Java线程学习(一)Java线程入门

    摘要:最近听很多面试的小伙伴说,网上往往是一篇一篇的多线程的文章,除了书籍没有什么学习多线程的一系列文章。将此线程标记为线程或用户线程。 最近听很多面试的小伙伴说,网上往往是一篇一篇的Java多线程的文章,除了书籍没有什么学习多线程的一系列文章。但是仅仅凭借一两篇文章很难对多线程有系统的学习,而且面试的时候多线程这方面的知识往往也是考察的重点,所以考虑之下决定写一系列关于Java多线程的文章...

    Donne 评论0 收藏0
  • Java线程专题一:并发所面临的问题

    摘要:但是并不是什么多线程就可以随便用,有的时候多线程反而会造成系统的负担,而且多线程还会造成其他的数据问题,下面就来介绍一下多线程面临的问题。下面这张图是多线程运行时候的情况,我们发现上下文切换的次数暴增。 并发的概念: 在Java中是支持多线程的,多线程在有的时候可以大提高程序的速度,比如你的程序中有两个完全不同的功能操作,你可以让两个不同的线程去各自执行这两个操作,互不影响,不需要执行...

    madthumb 评论0 收藏0
  • Java线程可以分组,还能这样玩!

    摘要:如图所示,带有的所有线程构造方法都可以定义线程组的。线程组还能统一设置组内所有线程的最高优先级,线程单独设置的优先级不会高于线程组设置的最大优先级。 前面的文章,栈长和大家分享过多线程创建的3种方式《实现 Java 多线程的 3 种方式》。 但如果线程很多的情况下,你知道如何对它们进行分组吗? 和 Dubbo 的服务分组一样,Java 可以对相同性质的线程进行分组。 来看下线程类 Th...

    biaoxiaoduan 评论0 收藏0
  • JAVA 线程和并发基础

    摘要:线程可以被称为轻量级进程。一个守护线程是在后台执行并且不会阻止终止的线程。其他的线程状态还有,和。上下文切换是多任务操作系统和多线程环境的基本特征。在的线程中并没有可供任何对象使用的锁和同步器。 原文:Java Multi-Threading and Concurrency Interview Questions with Answers 翻译:并发编程网 - 郑旭东 校对:方腾飞 多...

    vboy1010 评论0 收藏0
  • Java线程学习(七)并发编程中一些问题

    摘要:相比与其他操作系统包括其他类系统有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。因为多线程竞争锁时会引起上下文切换。减少线程的使用。很多编程语言中都有协程。所以如何避免死锁的产生,在我们使用并发编程时至关重要。 系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)syn...

    dingding199389 评论0 收藏0
  • 学习Java线程的一些总结

    摘要:多线程环境下的一些问题安全性问题在没有正确同步的情况下,多线程环境下程序可能得出错误的结果。一些相关概念竞争条件多线程的环境下,程序执行的结果取决于线程交替执行的方式。而线程的交替操作顺序是不可预测的,如此程序执行的结果也是不可预测的。 入口 Java多线程的应用复杂性之如jvm有限的几个内存方面的操作和规范,就像无数纷繁复杂的应用逻辑建立在有限的指令集上。 如何写出线程安全的程序,有...

    coolpail 评论0 收藏0

发表评论

0条评论

zorro

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<