二刷Java多线程:并发编程线程基础

1、线程简介

1)、什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。现代操作系统调度的最小单位是线程,也叫轻量级进程,在一个进程里可以创建多个线程

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正占用CPU运行的是线程,所以也说线程是CPU分配的基本单位
在这里插入图片描述
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。如果执行的native方法,那么pc计数器记录的是undefined地址,只有执行的是Java代码时pc计数器记录的才是下一条指令的地址

每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例

方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的

2)、线程的状态

java.lang.Thread.State中定义Java中线程的6种状态
在这里插入图片描述在这里插入图片描述

3)、线程优先级

现代操作系统基本采用分时的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或少分配一些处理器资源的线程属性

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int newPriority)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定

2、线程创建与运行

Java中有三种线程创建方式,分别为实现Runnable接口run()方法,继承Thread类并重写run()方法,使用FutureTask方式

调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run()方法执行完毕,该线程就处于终止状态

使用继承Thread类方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无需使用Thread.currentThread()方法;不好的地方是Java不支持多继承,另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码

前两种方式都没有办法拿到任务的返回结果,但是FutureTask方式可以

public class ThreadTest {
    public static class CallerTask implements Callable<String> {
        public String call() throws Exception {
            return "hello";
        }
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> futureTask = new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(futureTask).start();
        try {
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

3、线程间通信

1)、volatile和synchronized关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝,所以程序在执行过程中,一个线程看到的变量不一定是最新的

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性

对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只有一个线程获取到由synchronized所保护对象的监视器

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态
在这里插入图片描述
任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取

2)、等待/通知机制

等待/通知机制的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上

在这里插入图片描述

1)wait()函数

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:其他线程调用了该共享对象的notify()或者notifyAll()方法;其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回

另外,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常

一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是虚假唤醒

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件

        synchronized (obj) {
            while (条件不满足) {
                obj.wait();
            }
        }

如上代码是经典的调用共享变量wait()方法的实例,首先通过同步块获取obj上面的监视器锁,然后在while循环内调用obj的wait()方法

当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的

2)wait(long timeout)函数

该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait方法效果一样,因为在wait方法内部就是调用了wait(0)。需要注意的是, 如果在调用该函数时,传递了一个负的timeout则会抛出IllegalArgumentException异常

3)wait(long timeout, int nanos)函数

在其内部调用的是wait(long timeout)函数,如下代码只有在nanos>0时才使参数timeout递增1

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

4)notify()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的

此外,被唤醒的线程不能马上从wait()方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛岀IllegalMonitorStateException异常

5)notifyAll()函数

notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作

public class WaitNotify {
	static boolean flag = true;
	static Object lock = new Object();

	static class Wait implements Runnable {
		@Override
		public void run() {
			synchronized (lock) {
				while (flag) {
					try {
						System.out.println(Thread.currentThread() + " flag is true.wait@ "
								+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println(Thread.currentThread() + " flag is false.running@ "
						+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
			}
		}
	}

	static class Notify implements Runnable {
		@Override
		public void run() {
			synchronized (lock) {
				System.out.println(Thread.currentThread() + " hold lock.notify@ "
						+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
				lock.notifyAll();
				flag = false;
				SleepUtils.second(5);
				synchronized (lock) {
					System.out.println(Thread.currentThread() + " hold lock again.sleep@ "
							+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
					SleepUtils.second(5);
				}
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Thread waitThread = new Thread(new Wait(), "WaitThread");
		waitThread.start();
		TimeUnit.SECONDS.sleep(1);
		Thread notifyThread = new Thread(new Notify(), "NotifyThread");
		notifyThread.start();
	}
}

运行结果:

Thread[WaitThread,5,main] flag is true.wait@ 17:29:37
Thread[NotifyThread,5,main] hold lock.notify@ 17:29:37
Thread[NotifyThread,5,main] hold lock again.sleep@ 17:29:42
Thread[WaitThread,5,main] flag is false.running@ 17:29:47

这里需要注意以下几点:

  • 使用wait()、notify()和notifyAll()时需要先对调用对象加锁
  • 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列
  • notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回
  • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED
  • 从wait()方法返回的前提是获得了调用对象的锁

等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改
在这里插入图片描述
WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行

5、Thread类中其他常用方法

1)、等待线程终止的join方法

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)join(long millis, int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回

线程A调用线程B的join()方法后会被阻塞,当其他线程调用了线程A的interrupt()方法中断了线程A时,线程A会抛出InterruptedException异常而返回

public class ThreadTest2 {
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("child threadOne over");
            }
        });
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("child threadTwo over");
            }
        });
        threadOne.start();
        threadTwo.start();
        System.out.println("wait all child thread over");
        //等待子线程执行完毕返回
        threadOne.join();
        threadTwo.join();
    }
}
2)、让线程睡眠的sleep方法

Thread类中有一个静态的sleep()方法,当一个执行中的线程调用了Thread的sleep()方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep()方法的地方抛出InterruptedException异常而返回

sleep()与wait()方法的区别:

  • wait()会释放当前共享变量上的锁,而sleep()不会释放锁资源.
  • wait()方法调用时需要事先获取该对象的监视器锁
  • wait()无需捕获异常,而sleep()需要
  • sleep()是Thread类的方法,而wait()是Object类的方法
  • sleep()方法调用的时候必须指定时间
3)、让出CPU执行权的yield方法

Thread类中有一个静态的yield()方法,当一个线程调用yield()方法时,实际就是在暗示 线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield()时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度

当一个线程调用yield()方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权

sleep()与yield()方法的区别在于,当线程调用sleep()方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield()方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行

4)、线程中断

Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理

  • void interrupt()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程 A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回

  • boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false

    public boolean isInterrupted() {
        //传递false,说明不清除中断标志
        return isInterrupted(false);
    }
  • boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用。另外从下面的代码可以知道,在interrupted()内部是获取当前调用线程的中断标志而不是调用 interrupted()方法的实例对象的中断标志
    public static boolean interrupted() {
        //清除中断标志
        return currentThread().isInterrupted(true);
    }

案例

public class ThreadTest3 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread() + " hello");
                }
            }
        });
        thread.start();

        TimeUnit.SECONDS.sleep(1);

        //中断子线程
        System.out.println("main thread interrupt thread");
        thread.interrupt();

        //等待子线程执行完毕
        thread.join();
        System.out.println("main is over");
    }
}

运行结果:

...
Thread[Thread-0,5,main] hello
main thread interrupt thread
Thread[Thread-0,5,main] hello
Thread[Thread-0,5,main] hello
main is over

在如上代码中,子线程thread通过检查当前线程中断标志来控制是否退出循环,主线程在休眠Is后调用thread的interrupt()方法设置了中断标志,所以线程thread退出了循环

6、理解线程上下文切换

在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场

线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时;当前线程被其他线程中断时

7、线程活跃性问题

1)、死锁

1)什么是线程死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象, 在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去
在这里插入图片描述
上图中,线程A已经持有了资源2,它同时还想申请资源1,线程B已经持有了 资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态

死锁的案例

public class DeadLockTest {
    private static Object objA=new Object();
    private static Object objB=new Object();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                synchronized (objA){
                    System.out.println(Thread.currentThread().getName()+" get ResourceA");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (objB){
                        System.out.println(Thread.currentThread().getName()+" get ResourceB");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                synchronized (objB){
                    System.out.println(Thread.currentThread().getName()+" get ResourceB");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (objA){
                        System.out.println(Thread.currentThread().getName()+" get ResourceA");
                    }
                }
            }
        }).start();
    }
}

2)如何避免线程死锁

死锁的产生必须具备以下四个条件

  • 互斥:共享资源X和Y只能被一个线程占用
  • 占用且等待:线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
  • 不可抢占:其他线程不能强行抢占线程T1占有的资源
  • 循环等待:线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待

只要破坏其中一个,就可以成功避免死锁的发生

互斥这个条件无法破坏,因为我们用锁为的就是互斥

  • 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了
  • 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了
  • 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了
2)、活锁

1)什么是线程活锁?

活锁是指有时线程虽然没有发生阻塞,但依然会存在执行不下去的情况。线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源

活锁的案例:

public class Account {
    private int balance;
    private final Lock lock = new ReentrantLock();

    //转账
    void transfer(Account tar, int amt) {
        while (true) {
            if (this.lock.tryLock()) {
                try {
                    if (tar.lock.tryLock()) {
                        try {
                            this.balance -= amt;
                            tar.balance += amt;
                        } finally {
                            tar.lock.unlock();
                        }
                    }
                } finally {
                    this.lock.unlock();
                }
            }
        }
    }
}

上述案例有可能出现活锁,A、B两账户相互转账,各自持有自己lock的锁,都一直在尝试获取对方的锁,形成了活锁

2)如何避免线程活锁

解决活锁可以在线程谦让时,等待一个随机的时间

3)、饥饿

1)什么是线程饥饿?

线程饥饿指的是线程因无法访问所需资源而无法执行下去的情况。线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求…,T2可能永远等待

2)如何避免线程饥饿

解决线程饥饿有三种方案:一是保证资源充足,二是公平地分配资源(使用公平锁),三就是避免持有锁的线程长时间执行

8、守护线程与用户线程

Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。 在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。守护线程和用户线程的区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程

创建守护线程只需要将线程的daemon参数设置为true即可

public class ThreadTest4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {

            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}

main线程运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值