zoukankan      html  css  js  c++  java
  • Java笔记:多线程

    1. Java线程理解

    进程:进程就相当于一个应用程序,而线程是进程中的执行场景或者说执行单元,一个进程可以启动多个线程。

    线程并发:对于电脑的CPU,例如4核的CPU,表示在同一个时间点上,可以真正做到有4个进程并发执行。而对于单核CPU,是不能做到真正的多线程并发的,只是由于CPU在线程之间切换太快,让我们人在使用时产生了多个线程在同时运行的假象,在主观感觉上多个线程是并发的,但其实单核的CPU是不能做到真正的并发的。

    JVM进程:运行Java程序,首先会先启动一个JVM,JVM就是一个进程,然后JVM再启动一个主线程调用main方法,与此同时,再启动一个垃圾回收线程负责看护main主线程并回收其产生的垃圾。所以,一个Java程序中至少会有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

    线程的内存使用:Java中堆内存和方法区内存在线程间是共享的,也就是它们在程序运行期间都只有“一块”,但是栈是独立的,每一个线程拥有一个自己的栈,启动了多少线程就会有多少块栈内存。

    2. 创建线程的三种方式:Thread,Runnable,Callable

    Thread方式:定义一个类,继承java.lang.Thread,并重写run方法即可。运行时,调用线程对象的start方法,然后JVM就会自动创建一个分支线程(分支栈)来运行run方法中的代码。这种方式也是最核心的,其他两种方式都是基于这个Thread来实现的。

    Thread中的常用方法:

    • void start():start()方法的作用是启动一个分支线程,调用时会在JVM中开辟出一个新的栈空间,这个栈空间开辟出来后start()方法就结束了,表示线程启动成功了。注意,start()方法本身并不属于新的分支线程,而是属于调用者线程。start()方法结束后,启动成功的线程会自动调用run方法,并且run方法处于分支栈的底部(压栈),其作用和意义就相当于是分支栈的main方法,即主线程的main方法和分支线程的run方法对于各自的线程来讲是意义一样的。
    • void run():如果在当前线程中直接调用run方法,那它就是线程对象中的一个普通方法,并不会启动一个新的分支线程,所以想要启动一个新的分支线程,必须要通过调用start方法来运行run方法中的代码。
    • String getName():获取线程的名称,默认为“Thread-[n]”,n表示数字。
    • void setName(String name):设置线程的名称。
    • static Thread currentThread():获取当前线程的线程对象,当前线程指的是正在执行currentThread()这个方法的线程。(注意这是个静态方法)
    • static void sleep(long millis):使当前线程暂停执行指定毫秒数。(注意这是个静态方法)
    • void interrupt():中断sleep的睡眠。原理是调用sleep方法进行睡眠时,会产生一个InterruptedException的编译时异常,代码中通常会使用try块将sleep方法包裹起来,当调用interrupt方法时,就会主动抛出一个InterruptedException异常,此时的sleep睡眠就被中断了。
    • static void yield():线程让位,让当前线程短暂的暂停一下,以便让其他线程得以有更多时间执行。(注意这是个静态方法)
    • void join():线程合并,让当前线程阻塞,直到调用join方法的线程执行完毕,即让其他线程合入当前线程。
    • void setDaemon(boolean on):将on设置true传入,表示在线程调用start之前将其设置为守护线程,注意,这个方法需要在线程启动之前调用进行设置。Java中线程分为两类,用户线程和守护线程,守护线程也称为后台线程,而且守护线程通常是一个死循环程序,并且所有的用户线程结束之后,守护线程就会自动结束,不用程序员手动去结束。主线程main线程是属于用户线程,而垃圾回收机制的线程则属于守护线程。

    Thread简单示例:

    public class ThreadTest{
        public static void main(String[] args){
            // main方法中的代码属于主线程,在主栈中运行
            MyThread myThread = new MyThread();
            // 调用线程对象的start方法会启动一个新的分支线程,并执行线程对象中run方法的代码
            // 此时主线程的main方法并不会等myThread的run方法运行完毕,而是会直接往下继续执行
            // 因为它们属于两个独立的线程,它们的运行是并行执行的
            myThread.start();
            System.out.println("主线程正在运行...");
        }
    }
    
    
    class MyThread extends Thread {
        public void run(){
            // run方法中的代码会运行在创建的分支线程中
            System.out.println("分支线程正在执行...");
        }
    }

    Runnable方式:定义一个类,实现java.lang.Runnable接口,并重写接口的run方法,这个类也称之为可运行的类。然后再创建一个Thread对象,在创建Thread对象时,构造方法中将这个自定义的可运行类对象传入即可。

    注:这种实现接口的方式其实更加常用,因为定义的可运行类在将来还可以继承别的类,但定义Thread子类的方式因为Java只支持单继承的原因就没有机会再继承别的类了,即无法通过继承的方式扩展功能了。

    Runnable简单示例:

    public class ThreadTest{
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread t = new Thread(myRunnable);
            // 启动分支线程,并在分支线程中运行myRunnable对象中的run方法
            t.start();
            System.out.println("主线程正在运行...");
        }
    }
    
    // 这只是一个实现了Runnable接口的普通类
    // 只有将它传入Thread对象才能在单独的线程中运行
    class MyRunnable implements Runnable{
        public void run(){
            System.out.println("分支线程正在执行...");
        }
    }

    Callable方式:实现java.util.concurrent.Callable接口,并重写call()方法,具体使用方法见示例。这种方式的特点是可以获取线程的返回值。但是,也有一个缺点,调用get方法获取返回值时会阻塞当前线程。

    Callable简单示例:

    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    public class CallableTest {
        public static void main(String[] args) throws Exception{
            // FutureTask使用了泛型,使用时可以传入自己需要的类型
            // 这里采用了匿名内部类的实现方式
            FutureTask task = new FutureTask(new Callable(){
                // 需要重写call方法,就相当于Thread中的run方法
                @Override
                public Object call() throws Exception {
                    System.out.println("Callable线程正在运行...");
                    return new Object();
                }
            });
    
            Thread t = new Thread(task);
            t.start();
            // 执行get方法是会阻塞当前线程(这里是主线程main),
            // 直到线程t执行完毕
            Object obj = task.get();
            System.out.println("线程执行的结果:" + obj);
        }
    }

    3. 使用布尔标记终止线程

    终止线程的方法具体的使用场景可能有所不同,以下示例只是常用方法之一。

    // 终止线程的一种方式:定义一个布尔标记
    public class ThreadTest{
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread t = new Thread(myRunnable);
            t.start();
    
            // 主线程暂停5秒
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            // 主线程暂停5秒之后,手动去终止t线程
            myRunnable.run = false;
        }
    }
    
    
    class MyRunnable implements Runnable{
        // 定义一个布尔标记
        boolean run = true;
        public void run(){
            // 让当前线程sleep 10秒,模拟程序执行10秒
            for (int i = 0; i < 10; i++) {
                if (run) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                } else {
                    // 终止线程
                    return;
                }
            }
        }
    }

    4. 线程生命周期

    线程生命周期的状态通常有以下几个状态:新建状态、就绪状态、运行状态、阻塞状态和死亡状态。

    新建状态:创建线程对象之后,调用start()方法之前,线程就处于新建状态,此时线程还没有被创建,因为一旦创建线程成功之后就会立马进入就绪状态。

    就绪状态:调用start()方法启动线程之后,线程就处于就绪状态,就绪状态表示此时的线程拥有抢夺CPU的时间片的权利(CPU执行权),即我这个线程可以占用多少时间的CPU。当抢到时间片之后就会进入运行状态。

    运行状态:当线程抢到时间片之后就会去占用CPU,并使用CPU执行run()方法中的代码,一直到这个时间片使用完毕。时间片使用完毕之后,run方法中的代码会暂停执行并进入就绪状态,等下一次再次抢到时间片的时候就会继续运行run方法中的代码了。当在使用时间片的过程中,程序阻塞了,例如需要等待用户输入、程序sleep等,就会立马释放掉拥有的时间片,并进入阻塞状态。

    阻塞状态:当进入阻塞状态后,直到用户输入完毕、sleep时间到等,此时会解除程序的阻塞状态,并使程序进入就绪状态,继续参与CPU执行权的抢夺以便运行后续的代码。

    死亡状态:当run方法中的代码执行完毕之后,线程就进入死亡状态了。

    注:对于单核CPU来说,主线程和分支线程并发时,它们都在抢夺CPU的时间片,由于它们的运行状态交替太快,导致了我们主观感觉上的并行,但其实并没有真正的并发执行。

    5. synchronized关键字

    可以使用synchronized语法来实现线程之间的同步,以给某个代码块、方法或者类添加锁的方式,以达到数据安全的目的。

    代码块中的synchronized:在代码块中使用synchronized,语法如下:

    /* 例如线程t1、t2、t3之间共享对象testShare,而需要同步的代码正好是testShare中的一个方法,
       那么synchronized就需要用在testShare中,
       小括号中的"线程之间共享的对象"就可以写this,而方法体中的代码就可以放在synchronized
       的大括号中来执行。这样,同一个类new出来的不同对象就可以实现各自的线程间同步,互不干扰。
       注意:线程之间共享需要共享的对象可能是不同的,而大括号中的代码和共享对象之间不一定是有关系的,
       这两个部分可以是没有关系的,所以这里不一定是this。这个共享对象只是给线程获取锁提供了一个对象,
       多个线程之间只有需要获取相同对象的锁的时候,才会发生线程的同步。
    */
    synchronized(线程之间共享的对象){
        需要同步的代码
    }

    synchronized原理:synchronized语法实现线程之间同步的原理其实就是线程对对象锁的占有和释放,每一个Java对象都有一个锁(其实就是一个标记,我们称之为锁而已),当第一个线程遇到synchronized之后就会占有小括号中“共享对象”的锁,然后执行大括号中需要同步的代码块,如果在执行过程中,第二个线程也来到了这里,遇到了synchronized,也会去占有这个“共享对象”的锁,但是发现它已经被占有了,那么就只好排队等待,直到第一个线程执行完毕,释放这个“共享对象”的锁,然后第二个线程才能占有锁并继续执行后面的代码。以此类推,后面的线程也会来占有锁,如果锁已经被占有了,就停止执行并等待,直到“有锁可占”,如此,也就达到了这段代码的线程间同步。

    synchronized效率提升:

    • 大括号中的内容越多,范围越大,执行效率越低,所以应该尽量保证大括号中的内容少一点,范围小一点。
    • 对于局部变量,因为它始终都在栈中,而各自的线程都有自己的栈,所以局部变量是不存在线程安全问题的,因此,对于Java中的某些引用数据类型,在局部变量的使用中,应该使用非线程安全的数据类型,比如ArrayList、HashSet、StringBuilder等,它们虽然本身不是线程安全的,但是因为是局部变量,所以不存在线程安全问题,也就不用去考虑它们本身的线程安全问题了。

    方法定义中的synchronized:synchronized可以在方法定义上使用,此时共享对象默认为this,同步的代码为整个方法体的代码。但是注意,如果是静态方法,那么执行这个方法时查找的锁就是类锁了,而不是对象锁了。这种用法虽然直接使用了this和整个方法体中的代码,但是也可以看情况使用,满足这个使用条件的就可以使用这个方式,代码也会更简洁。

    public synchronized void myFunc(){
        ....
    }

    类定义中的synchronized:如果synchronized关键字出现在类的定义修饰符中,那么表示这个类创建的所有对象都拥有同一个锁,也称之为类锁,类锁的定义主要是为了保证静态变量的线程安全。

    6. 死锁

    当线程之间某个线程想要获取的锁被对方线程占有了,与此同时,对方线程想要获取的锁也被自己获取了,此时两个线程就都会处于一直等待的状态,而不往下执行的情况,这种场景称之为死锁。

    注:synchronized在开发中应该尽量避免嵌套使用,因为嵌套synchronized有可能会造成死锁问题,而死锁问题大多时候很难定位。

    死锁示例:

    public class ThreadTest{
        public static void main(String[] args){
            Object o1 = new Object();
            Object o2 = new Object();
            MyThread1 t1 = new MyThread1(o1, o2);
            MyThread2 t2 = new MyThread2(o1, o2);
            t1.start();
            t2.start();
        }
    }
    
    class MyThread1 extends Thread{
        Object o1;
        Object o2;
    
        public MyThread1(Object o1, Object o2){
            this.o1 = o1;
            this.o2 = o2;
        }
    
        public void run(){
            synchronized(o1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 此时想要去获取o2的锁,但是已经被MyThread2的线程获取了,只能暂定并等待
                synchronized(o2){
                    System.out.println("MyThread1 run...");
                }
            }
        }
    }
    
    class MyThread2 extends Thread{
        Object o1;
        Object o2;
    
        public MyThread2(Object o1, Object o2){
            this.o1 = o1;
            this.o2 = o2;
        }
    
        public void run(){
            synchronized(o2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 此时想要去获取o1的锁,但是已经被MyThread1的线程获取了,只能暂定并等待
                synchronized(o1){
                    System.out.println("MyThread2 run...");
                }
            }
        }
    }

    7. 定时器

    使用java.util.Timer就可以自己实现一个定时器。

    可以从构造方法指定定时器的名称,以及该定时器是否作为守护线程运行。注意,这里说的定时器,包括了定时器本身线程和在定时器中运行的任务线程。

    常用方法:

    • void schedule(TimerTask task, Date firstTime, long period):从指定的时间firstTime开始,每隔period(毫秒)时间执行一次任务task。注意,TimerTask是一个抽象类,使用时需要自己新写一个类,继承TimerTask并重写它的run方法。调用这个方法之后,定时器就开始工作了。

    8. wait和notify

     wait和notify方法不是线程Thread类的方法,而是Object的方法,即任何类都有这两个方法。但是注意,这两个方法是建立在线程同步的机制上的。

    • wait():让正在该对象(调用wait方法的对象)上活动的线程(当前线程)进入无限期等待的状态,直到被唤醒为止。并且该线程会释放在该对象上占有的锁。
    • notify():唤醒正在该对象(调用notify方法的对象)上等待的线程。但是注意,这个方法只是起到通知的作用,并不会释放在该对象上占有的锁,通常是线程执行完毕就自动释放了。
    • notifyAll():唤醒所有正在该对象(调用notify方法的对象)上等待的线程,这个方法同样不会释放对象锁。
  • 相关阅读:
    JavaWeb--HttpSession案例
    codeforces B. Balls Game 解题报告
    hdu 1711 Number Sequence 解题报告
    codeforces B. Online Meeting 解题报告
    ZOJ 3706 Break Standard Weight 解题报告
    codeforces C. Magic Formulas 解题报告
    codeforces B. Sereja and Mirroring 解题报告
    zoj 1109 Language of FatMouse 解题报告
    hdu 1361.Parencodings 解题报告
    hdu 1004 Let the Balloon Rise 解题报告
  • 原文地址:https://www.cnblogs.com/guyuyun/p/13174715.html
Copyright © 2011-2022 走看看