《Java并发编程之美》学习笔记(一):基础知识

  • 什么是进程?       

  进程是系统进行资源分配和调度的基本单位。一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

  • 什么是线程?

 线程是进程中的一个实体,操作系统在分配资源时是把资源分配给进程的, 但是CPU 资源比较特殊, 它是被分配到线程的, 因为真正要占用CPU 运行的是线程, 所以也说线程是CPU 分配的基本单位。

进程与线程的关系如下图:

《Java并发编程之美》学习笔记(一):基础知识_第1张图片

1. 线程共享内存:堆和方法区

Java堆存放:new对象

方法区存放:JVM加载的class类,常量,静态变量(jdk 1.8中移除了方法区,用元数据区)

2.线程私有内存:程序计数器,本地方法栈,栈

程序计数器:用来记录线程当前要执行的指令地址。由于CPU使用时间片轮转的方式让线程轮询占有的,所以当时间片用完后,线程要让出CPU,等下次轮转到自己的时候在继续执行,这个时候就需要程序计数器记录上一次执行的位置。另外需要注意的是,如果执行的是native方法,那么pc计数器记录的是undefined 地址,只有执行的是Java 代码时pc计数器记录的才是下一条指令的地址。

本地方发栈:java调用内部非java语言开发库的接口,一般是c++;

java栈存放:局部变量,调用栈帧,方法出口

 

  • 创建线程的方式

1.实现Runnable接口

    class RunnableTask implements Runnable

2.继承Thread类

    class TestExtendsThread extends Thread

3.实现Callable

    class CallableTask implements Callable
  • 线程通知与等待

Java 中的Object 类是所有类的父类,鉴于继承机制, Java 把所有类都需要的方法放到了Object 类里面,其中就包含本节要讲的通知与等待系列函数。

1.wait(): 当一个线程调用一个共享变量的wait()方法时,会阻塞挂起当前线程,直到发生其他线程调用该共享变量的notify()或者notifyAll()f方法,或者其他线程调用了该线程的interrupt() 中断方法,该线程会抛出InterruptedException异常返回。

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

一个线程如何获取一个共享变量的监控器锁呢?

(1). 使用 synchronized同步代码块时,共享变量作为参数:synchronized (共享变量){}

(2).使用synchronized修饰方法:synchronized void add ( int a , int b ) {}

2.wait(long time)

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

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

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

5.notityAll() 会唤醒所有在该共享变量上由于调用wait 系列方法而被挂起的线程。一个需要注意的地方是,在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait 系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后一个线程调用了该共享变量的wait() 方法而被放入阻塞集合, 则该线程是不会被唤醒的。

  • 其他方法

1. join()  阻塞自己,等待其他线程执行完成,主函数线程阻塞自己,等待子线程执行完毕后,才继续执行自己。《Java并发编程之美》学习笔记(一):基础知识_第2张图片

2. sleep(long time)  Thread 类中有一个静态的s leep 方法,当一个执行中的线程调用了Thread 的sleep 方法后,调用线程会暂时让出指定时间的执行权;但该线程所拥有的监视器资源,比如锁还是持有不让出的

指定的睡眠时间到期后,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep 方法的地方抛出IntermptedException 异常而返回。

3.yield() Thread 类中有一个静态的yield 方法,当一个线程调用yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU 使用,但是线程调度器可以无条件忽略这个暗示。

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

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

  • 线程中断方法

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

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

3.static boolean interrupted() 方法: 检测当前线程是否被中断, 如果是返回true , 否则返回false 。

interrupted与islnterrupted 不同的是,该方法如果发现当前线程被中断, 则会清除中断标志。

  • 什么是线程上下文切换?

在多线程编程中,线程个数一般都大于CPU 个数,而每个CPU 同一时一刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU 执行任务。当前线程使用完时间片后,就会处于就绪状态并让出C PU 让其他线程占用, 这就是上下文切换。

这个时候就用到程序计数器了,保存自己执行现场,等待下次分配时间后,继续执行。上下文切换会造成线程阻塞挂起和再次调度的性能消耗。线程上下文切换时机有: 当前线程的CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。

  • 什么是线程死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

(1)造成死锁的四个必要条件:

1.互斥等待:共享资源只能被一个线程持有,其他线程只能等待该资源被释放,此时其他线程处于阻断挂起状态。

2.请求并持有条件:一个线程自己已经持有一个共享资源,但是又需要请求被其他线程持有的资源,此时该线程只能等待别人释放资源。但是在阻塞等待的同时,不会释放自己已经持有的资源。

3.不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。

4.环路等待条件:指在发生死锁时, 必然存在一个线程资源的环形链, 即线程集合{TO , T1, T2 ,…, Tn }中的TO 正在等待一个Tl 占用的资源, Tl 正在等待T2 占用的资源,……Tn 正在等待己被TO 占用的资源。

(2)如何避免死锁?

目前只有请求并持有和环路等待条件是可以被破坏的。造成死锁的原因其实和申请资源的顺序有很大关系, 使用资源申请的有序性原则就以避免死。线程A 获取资源的顺序为,锁1,锁2,线程B也按锁1,锁2的顺序申请锁。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件, 因此避免了死锁。

  • 什么是守护线程和用户线程?

(1)daemon 线程(守护线程〉

在JVM内部同时还启动了好多守护线程, 比如垃圾回收线程

(2)user 线程(用户线程)

在JVM 启动时会调用main 函数, main 函数所在的钱程就是一个用户线程

(3)用户线程和守护线程的区别是什么?

当最后一个用户线程结束时, JVM会正常退出,不会关心当前有没有守护线程;也就是说守护线程是否结束并不影响JVM 的退出。

设置线程为守护线程:thread.setDaemon(true);

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

总结: 如果你希望在主线程结束后JVM 进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让口JVM 进程结束,那么就将子线程设置为用户线程。

  • ThreadLocal

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。其一,加锁。

ThreadLocal 是JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存。

 

《Java并发编程之美》学习笔记(一):基础知识_第3张图片《Java并发编程之美》学习笔记(一):基础知识_第4张图片

注意:同一个ThreadLocal 变量在父线程中被设置值后, 在子线程中是获取不到的。因为在子线程thread 里面调用get 方法时当前线程为thread 线程,而这里调用set 方法设置线程变量的是main 线程,两者是不同的线程,自然子线程访问时返回null。

为了解决ThreadLocal不能继承问题, InheritableThreadLocal 应运而生。InheritableThreadLocal继承自ThreadLocal , 其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

 

以上内容来自(翟陆续-java并发编程之美)的学习笔记。写此篇文章的目的,一是加深记忆,二是日后复习。

 

你可能感兴趣的:(Java并发编程)