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方法的对象)上等待的线程,这个方法同样不会释放对象锁。

 

你可能感兴趣的:(Java笔记:多线程)